]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 224208 Add a higher level of categorization (.ie departments, locations, etc.)
authorbugreport%peshkin.net <>
Sat, 21 Aug 2004 04:49:17 +0000 (04:49 +0000)
committerbugreport%peshkin.net <>
Sat, 21 Aug 2004 04:49:17 +0000 (04:49 +0000)
patch by Albert Ting
r=joel, glob
a=myk

38 files changed:
Bugzilla/Bug.pm
Bugzilla/Search.pm
buglist.cgi
bugzilla.dtd
checksetup.pl
colchange.cgi
defparams.pl
editclassifications.cgi [new file with mode: 0755]
editproducts.cgi
enter_bug.cgi
globals.pl
js/productform.js
long_list.cgi
query.cgi
report.cgi
template/en/default/admin/classifications/add.html.tmpl [new file with mode: 0644]
template/en/default/admin/classifications/del.html.tmpl [new file with mode: 0644]
template/en/default/admin/classifications/delete.html.tmpl [new file with mode: 0644]
template/en/default/admin/classifications/edit.html.tmpl [new file with mode: 0644]
template/en/default/admin/classifications/new.html.tmpl [new file with mode: 0644]
template/en/default/admin/classifications/reclassify.html.tmpl [new file with mode: 0644]
template/en/default/admin/classifications/select.html.tmpl [new file with mode: 0644]
template/en/default/admin/classifications/update.html.tmpl [new file with mode: 0644]
template/en/default/admin/products/groupcontrol/edit.html.tmpl
template/en/default/bug/edit.html.tmpl
template/en/default/bug/show-multiple.html.tmpl
template/en/default/filterexceptions.pl
template/en/default/global/choose-classification.html.tmpl [new file with mode: 0644]
template/en/default/global/field-descs.none.tmpl
template/en/default/global/useful-links.html.tmpl
template/en/default/global/user-error.html.tmpl
template/en/default/search/form.html.tmpl
template/en/default/search/search-advanced.html.tmpl
template/en/default/search/search-help.html.tmpl
template/en/default/search/search-report-graph.html.tmpl
template/en/default/search/search-report-select.html.tmpl
template/en/default/search/search-report-table.html.tmpl
template/en/default/search/search-specific.html.tmpl

index 8863f5432fe620492ea1209c80ed6b4afba8a55b..d5aa5fd171fe3ee32472226fe82a2e4835e3e28b 100755 (executable)
@@ -48,6 +48,7 @@ sub fields {
     # Keep this ordering in sync with bugzilla.dtd
     my @fields = qw(bug_id alias creation_ts short_desc delta_ts
                     reporter_accessible cclist_accessible
+                    classification_id classification
                     product component version rep_platform op_sys
                     bug_status resolution
                     bug_file_loc status_whiteboard keywords
@@ -137,7 +138,8 @@ sub initBug  {
 
   my $query = "
     SELECT
-      bugs.bug_id, alias, bugs.product_id, products.name, version,
+      bugs.bug_id, alias, products.classification_id, classifications.name,
+      bugs.product_id, products.name, version,
       rep_platform, op_sys, bug_status, resolution, priority,
       bug_severity, bugs.component_id, components.name, assigned_to,
       reporter, bug_file_loc, short_desc, target_milestone,
@@ -147,8 +149,9 @@ sub initBug  {
       reporter_accessible, cclist_accessible,
       estimated_time, remaining_time
     from bugs left join votes using(bug_id),
-      products, components
+      classifications, products, components
     where bugs.bug_id = $bug_id
+      AND classifications.id = products.classification_id
       AND products.id = bugs.product_id
       AND components.id = bugs.component_id
     group by bugs.bug_id";
@@ -159,7 +162,8 @@ sub initBug  {
   if ((@row = &::FetchSQLData()) && $self->{'who'}->can_see_bug($bug_id)) {
     my $count = 0;
     my %fields;
-    foreach my $field ("bug_id", "alias", "product_id", "product", "version", 
+    foreach my $field ("bug_id", "alias", "classification_id", "classification",
+                       "product_id", "product", "version", 
                        "rep_platform", "op_sys", "bug_status", "resolution", 
                        "priority", "bug_severity", "component_id", "component",
                        "assigned_to", "reporter", "bug_file_loc", "short_desc",
index 91785963fa5d445bf999f3a2134f63e07453dc91..23bb34eae7e0c3c4f351a9fb7850f44050c5e7bc 100644 (file)
@@ -96,6 +96,11 @@ sub init {
         push @wherepart, "bugs.product_id = map_products.id";
     }
 
+    if (lsearch($fieldsref, 'map_classifications.name') >= 0) {
+        push @supptables, "classifications AS map_classifications";
+        push @wherepart, "map_products.classification_id = map_classifications.id";
+    }
+
     if (lsearch($fieldsref, 'map_components.name') >= 0) {
         push @supptables, "components AS map_components";
         push @wherepart, "bugs.component_id = map_components.id";
@@ -152,7 +157,7 @@ sub init {
     
     my @legal_fields = ("product", "version", "rep_platform", "op_sys",
                         "bug_status", "resolution", "priority", "bug_severity",
-                        "assigned_to", "reporter", "component",
+                        "assigned_to", "reporter", "component", "classification",
                         "target_milestone", "bug_group");
 
     foreach my $field ($params->param()) {
@@ -761,6 +766,16 @@ sub init {
                                      $term);
          },
 
+         "^classification,(?!changed)" => sub {
+             # Generate the restriction condition
+             $f = $ff = "classifications.name";
+             $funcsbykey{",$t"}->();
+             $term = build_subselect("map_products.classification_id",
+                                     "classifications.id",
+                                     "classifications",
+                                     $term);
+         },
+
          "^keywords," => sub {
              &::GetVersionTable();
              my @list;
index eaca2561203be70afdd53bd60437e008ca7e91e8..3c575d2b836fcc7c554ee85b7f92f171424a7f9f 100755 (executable)
@@ -458,6 +458,7 @@ DefineColumn("short_desc"        , "bugs.short_desc"            , "Summary"
 DefineColumn("status_whiteboard" , "bugs.status_whiteboard"     , "Status Summary"   );
 DefineColumn("component"         , "map_components.name"        , "Component"        );
 DefineColumn("product"           , "map_products.name"          , "Product"          );
+DefineColumn("classification"    , "map_classifications.name"   , "Classification"   );
 DefineColumn("version"           , "bugs.version"               , "Version"          );
 DefineColumn("op_sys"            , "bugs.op_sys"                , "OS"               );
 DefineColumn("target_milestone"  , "bugs.target_milestone"      , "Target Milestone" );
@@ -554,6 +555,11 @@ if (grep('relevance', @displaycolumns) && !$fulltext) {
 my @selectcolumns = ("bug_id", "bug_severity", "priority", "bug_status",
                      "resolution");
 
+# if using classification, we also need to look in product.classification_id
+if (Param("useclassification")) {
+    push (@selectcolumns,"product");
+}
+
 # remaining and actual_time are required for precentage_complete calculation:
 if (lsearch(\@displaycolumns, "percentage_complete") >= 0) {
     push (@selectcolumns, "remaining_time");
index aecf9920d209d3cc75420dfa060f5cc1c8360365..82ccfff18c16e566af8dfe473e08eeedd132fc55 100644 (file)
@@ -5,7 +5,7 @@
        maintainer CDATA #REQUIRED
        exporter CDATA #IMPLIED
 >
-<!ELEMENT bug (bug_id, (alias?, creation_ts, short_desc, delta_ts, reporter_accessible, cclist_accessible, product, component, version, rep_platform, op_sys, bug_status, resolution?, bug_file_loc?, status_whiteboard?, keywords*, priority, bug_severity, target_milestone?, dependson*, blocked*, votes?, reporter, assigned_to, qa_contact?, cc*, (estimated_time, remaining_time, actual_time)?, groups*, long_desc*, attachment*)?)>
+<!ELEMENT bug (bug_id, (alias?, creation_ts, short_desc, delta_ts, reporter_accessible, cclist_accessible, classification_id, classification, product, component, version, rep_platform, op_sys, bug_status, resolution?, bug_file_loc?, status_whiteboard?, keywords*, priority, bug_severity, target_milestone?, dependson*, blocked*, votes?, reporter, assigned_to, qa_contact?, cc*, (estimated_time, remaining_time, actual_time)?, groups*, long_desc*, attachment*)?)>
 <!ATTLIST bug
        error (NotFound | NotPermitted | InvalidBugId) #IMPLIED
 >
@@ -16,6 +16,8 @@
 <!ELEMENT exporter (#PCDATA)>
 <!ELEMENT urlbase (#PCDATA)>
 <!ELEMENT bug_status (#PCDATA)>
+<!ELEMENT classification_id (#PCDATA)>
+<!ELEMENT classification (#PCDATA)>
 <!ELEMENT product (#PCDATA)>
 <!ELEMENT priority (#PCDATA)>
 <!ELEMENT version (#PCDATA)>
index 703358cec096cd5b42d2ddb5c28268ece9150fd9..807fa90164921144fc94e35e6a93b3fe52a8255d 100755 (executable)
@@ -1786,10 +1786,17 @@ $table{logincookies} =
 
     index(lastused)';
 
+$table{classifications} =
+   'id smallint not null auto_increment primary key,
+    name varchar(64) not null,
+    description mediumtext,
+
+    unique(name)';
 
 $table{products} =
    'id smallint not null auto_increment primary key,
     name varchar(64) not null,
+    classification_id smallint not null default 1,
     description mediumtext,
     milestoneurl tinytext not null,
     disallownew tinyint not null,
@@ -2153,6 +2160,7 @@ sub AddFDef ($$$) {
 # be created with their associated schema change.
 AddFDef("bug_id", "Bug \#", 1);
 AddFDef("short_desc", "Summary", 1);
+AddFDef("classification", "Classification", 1);
 AddFDef("product", "Product", 1);
 AddFDef("version", "Version", 1);
 AddFDef("rep_platform", "Platform", 1);
@@ -4021,6 +4029,7 @@ AddField("profiles", "extern_id", "varchar(64)");
 AddGroup('tweakparams', 'Can tweak operating parameters');
 AddGroup('editusers', 'Can edit or disable users');
 AddGroup('creategroups', 'Can create and destroy groups.');
+AddGroup('editclassifications', 'Can create, destroy, and edit classifications.');
 AddGroup('editcomponents', 'Can create, destroy, and edit components.');
 AddGroup('editkeywords', 'Can create, destroy, and edit keywords.');
 AddGroup('admin', 'Administrators');
@@ -4388,6 +4397,16 @@ if (GetFieldDef('bugs', 'short_desc')->[2]) { # if it allows nulls
 $dbh->do("UPDATE groups SET last_changed = NOW() WHERE name = 'admin'");
 
 
+# 2003-10-24 - alt@sonic.net, bug 224208
+# Support classification level and make sure there is a default classification
+AddField('products', 'classification_id', 'smallint DEFAULT 1');
+$sth = $dbh->prepare("SELECT name FROM classifications WHERE id=1");
+$sth->execute;
+if (! $sth->rows) {
+    $dbh->do("INSERT INTO classifications (id,name,description) " .
+             "VALUES(1,'Unclassified','Unassigned to any classifications')");
+}
+
 #
 # Final checks...
 
index 8d3ee49da96aa68243b38d9d55e309b646f3633f..03d5388bf66dffc3df06da2d86dc940d3514304a 100755 (executable)
@@ -47,8 +47,13 @@ my $cgi = Bugzilla->cgi;
 my @masterlist = ("opendate", "changeddate", "bug_severity", "priority",
                   "rep_platform", "assigned_to", "assigned_to_realname",
                   "reporter", "reporter_realname", "bug_status",
-                  "resolution", "product", "component", "version", "op_sys",
-                  "votes");
+                  "resolution");
+
+if (Param("useclassification")) {
+    push(@masterlist, "classification");
+}
+
+push(@masterlist, ("product", "component", "version", "op_sys", "votes"));
 
 if (Param("usebugaliases")) {
     unshift(@masterlist, "alias");
index 8260be9784dae9b6d69508659e29e6c72513a03c..c0e608a4dd117068242eecd1bc8503e1606709bc 100644 (file)
@@ -320,6 +320,23 @@ sub find_languages {
    checker => \&check_multi
   },
 
+  {
+   name => 'useclassification',
+   desc => 'If this is on, Bugzilla will associate each product with a ' .
+           'specific classification.  But you must have "editclassification" ' .
+           'permissions enabled in order to edit classifications',
+   type => 'b',
+   default => 0
+  },
+
+  {
+   name => 'showallproducts',
+   desc => 'If this is on and useclassification is set, Bugzilla will add a' .
+           '"All" link in the "New Bug" page to list all available products',
+   type => 'b',
+   default => 0
+  },
+
   {
    name => 'makeproductgroups',
    desc => 'If this is on, Bugzilla will associate a bug group with each ' .
diff --git a/editclassifications.cgi b/editclassifications.cgi
new file mode 100755 (executable)
index 0000000..c1186f7
--- /dev/null
@@ -0,0 +1,391 @@
+#!/usr/bin/perl -wT
+# -*- Mode: perl; indent-tabs-mode: nil; cperl-indent-level: 4 -*-
+#
+# 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 Albert Ting
+#
+# Contributor(s): Albert Ting <alt@sonic.net>
+#
+# Direct any questions on this source code to mozilla.org
+
+use strict;
+use lib ".";
+
+use Bugzilla;
+use Bugzilla::Constants;
+require "CGI.pl";
+require "globals.pl";
+
+my $cgi = Bugzilla->cgi;
+my $dbh = Bugzilla->dbh;
+
+use vars qw ($template $vars);
+
+# TestClassification:  just returns if the specified classification does exists
+# CheckClassification: same check, optionally  emit an error text
+
+sub TestClassification ($) {
+    my $cl = shift;
+
+    trick_taint($cl);
+    # does the classification exist?
+    my $sth = $dbh->prepare("SELECT name
+                             FROM classifications
+                             WHERE name=?");
+    $sth->execute($cl);
+    my @row = $sth->fetchrow_array();
+    return $row[0];
+}
+
+sub CheckClassification ($) {
+    my $cl = shift;
+
+    unless ($cl) {
+        ThrowUserError("classification_not_specified");
+    }
+    if (! TestClassification($cl)) {
+        ThrowUserError("classification_doesnt_exist", { name => $cl });
+    }
+}
+
+sub LoadTemplate ($) {
+    my $action = shift;
+
+    $action =~ /(\w+)/;
+    $action = $1;
+    print $cgi->header();
+    $template->process("admin/classifications/$action.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+}
+
+#
+# Preliminary checks:
+#
+
+Bugzilla->login(LOGIN_REQUIRED);
+
+print $cgi->header();
+
+ThrowUserError("auth_cant_edit_classifications") unless UserInGroup("editclassifications");
+ThrowUserError("auth_classification_not_enabled") unless Param("useclassification");
+
+#
+# often used variables
+#
+my $action = trim($cgi->param('action') || '');
+my $classification = trim($cgi->param('classification') || '');
+trick_taint($classification);
+$vars->{'classification'} = $classification;
+
+#
+# action='' -> Show nice list of classifications
+#
+
+unless ($action) {
+    my @classifications;
+    # left join is tricky
+    #   - must select "classifications" fields if you want a REAL value
+    #   - must use "count(products.classification_id)" if you want a true
+    #     count.  If you use count(classifications.id), it will return 1 for NULL
+    #   - must use "group by classifications.id" instead of
+    #     products.classification_id. Otherwise it won't look for all
+    #     classification ids, just the ones used by the products.
+    my $sth = $dbh->prepare("SELECT classifications.id,classifications.name,
+                                    classifications.description,
+                                    COUNT(classification_id) as total
+                             FROM classifications
+                             LEFT JOIN products ON classifications.id=products.classification_id
+                             GROUP BY classifications.id
+                             ORDER BY name");
+    $sth->execute();
+    while (my ($id,$classification,$description,$total) = $sth->fetchrow_array()) {
+        my $cl = {};
+        $cl->{'id'} = $id;
+        $cl->{'classification'} = $classification;
+        $cl->{'description'} = $description if (defined $description);
+        $cl->{'total'} = $total;
+
+        push(@classifications, $cl);
+    }
+
+    $vars->{'classifications'} = \@classifications;
+    LoadTemplate("select");
+}
+
+#
+# action='add' -> present form for parameters for new classification
+#
+# (next action will be 'new')
+#
+
+if ($action eq 'add') {
+    LoadTemplate($action);
+}
+
+#
+# action='new' -> add classification entered in the 'action=add' screen
+#
+
+if ($action eq 'new') {
+    if (TestClassification($classification)) {
+        ThrowUserError("classification_already_exists", { name => $classification });
+    }
+    my $description = trim($cgi->param('description')  || '');
+    trick_taint($description);
+
+    # Add the new classification.
+    my $sth = $dbh->prepare("INSERT INTO classifications (name,description)
+                            VALUES (?,?)");
+    $sth->execute($classification,$description);
+
+    # Make versioncache flush
+    unlink "data/versioncache";
+
+    LoadTemplate($action);
+}
+
+#
+# action='del' -> ask if user really wants to delete
+#
+# (next action would be 'delete')
+#
+
+if ($action eq 'del') {
+    CheckClassification($classification);
+    my $sth;
+
+    # display some data about the classification
+    $sth = $dbh->prepare("SELECT id, description
+                          FROM classifications
+                          WHERE name=?");
+    $sth->execute($classification);
+    my ($classification_id, $description) = $sth->fetchrow_array();
+
+    ThrowUserError("classification_not_deletable") if ($classification_id eq "1");
+
+    $sth = $dbh->prepare("SELECT name
+                          FROM products
+                          WHERE classification_id=$classification_id");
+    $sth->execute();
+    ThrowUserError("classification_has_products") if ($sth->fetchrow_array());
+
+    $vars->{'description'} = $description if (defined $description);
+
+    LoadTemplate($action);
+}
+
+#
+# action='delete' -> really delete the classification
+#
+
+if ($action eq 'delete') {
+    CheckClassification($classification);
+
+    my $sth;
+    my $classification_id = get_classification_id($classification);
+
+    if ($classification_id == 1) {
+        ThrowUserError("cant_delete_default_classification", { name => $classification });
+    }
+
+    # lock the tables before we start to change everything:
+    $dbh->do("LOCK TABLES classifications WRITE, products WRITE");
+
+    # delete
+    $sth = $dbh->prepare("DELETE FROM classifications WHERE id=?");
+    $sth->execute($classification_id);
+
+    # update products just in case
+    $sth = $dbh->prepare("UPDATE products 
+                          SET classification_id=1
+                          WHERE classification_id=?");
+    $sth->execute($classification_id);
+
+    $dbh->do("UNLOCK TABLES");
+
+    unlink "data/versioncache";
+
+    LoadTemplate($action);
+}
+
+#
+# action='edit' -> present the edit classifications from
+#
+# (next action would be 'update')
+#
+
+if ($action eq 'edit') {
+    CheckClassification($classification);
+
+    my @products = ();
+    my $has_products = 0;
+    my $sth;
+    
+
+    # get data of classification
+    $sth = $dbh->prepare("SELECT id,description
+                          FROM classifications
+                          WHERE name=?");
+    $sth->execute($classification);
+    my ($classification_id,$description) = $sth->fetchrow_array();
+    $vars->{'description'} = $description if (defined $description);
+
+    $sth = $dbh->prepare("SELECT name,description
+                          FROM products
+                          WHERE classification_id=?
+                          ORDER BY name");
+    $sth->execute($classification_id);
+    while ( my ($product, $prod_description) = $sth->fetchrow_array()) {
+        my $prod = {};
+        $has_products = 1;
+        $prod->{'name'} = $product;
+        $prod->{'description'} = $prod_description if (defined $prod_description);
+        push(@products, $prod);
+    }
+    $vars->{'products'} = \@products if ($has_products);
+
+    LoadTemplate($action);
+}
+
+#
+# action='update' -> update the classification
+#
+
+if ($action eq 'update') {
+    my $classificationold   = trim($cgi->param('classificationold')   || '');
+    my $description         = trim($cgi->param('description')         || '');
+    my $descriptionold      = trim($cgi->param('descriptionold')      || '');
+    my $checkvotes = 0;
+    my $sth;
+
+    CheckClassification($classificationold);
+
+    my $classification_id = get_classification_id($classificationold);
+    trick_taint($description);
+
+    # Note that we got the $classification_id using $classificationold
+    # above so it will remain static even after we rename the
+    # classification in the database.
+
+    $dbh->do("LOCK TABLES classifications WRITE");
+
+    if ($description ne $descriptionold) {
+        $sth = $dbh->prepare("UPDATE classifications
+                              SET description=?
+                              WHERE id=?");
+        $sth->execute($description,$classification_id);
+        $vars->{'updated_description'} = 1;
+    }
+
+    if ($classification ne $classificationold) {
+        unless ($classification) {
+            $dbh->do("UNLOCK TABLES");
+            ThrowUserError("classification_not_specified")
+        }
+        
+        if (TestClassification($classification)) {
+            $dbh->do("UNLOCK TABLES");
+            ThrowUserError("classification_already_exists", { name => $classification });
+        }
+        $sth = $dbh->prepare("UPDATE classifications
+                              SET name=? WHERE id=?");
+        $sth->execute($classification,$classification_id);
+        $vars->{'updated_classification'} = 1;
+    }
+    $dbh->do("UNLOCK TABLES");
+
+    unlink "data/versioncache";
+    LoadTemplate($action);
+}
+
+#
+# action='reclassify' -> reclassify products for the classification
+#
+
+if ($action eq 'reclassify') {
+    CheckClassification($classification);
+    my $sth;
+
+    # display some data about the classification
+    $sth = $dbh->prepare("SELECT id, description
+                          FROM classifications
+                          WHERE name=?");
+    $sth->execute($classification);
+    my ($classification_id, $description) = $sth->fetchrow_array();
+
+    $vars->{'description'} = $description if (defined $description);
+
+    $sth = $dbh->prepare("UPDATE products
+                          SET classification_id=?
+                          WHERE name=?");
+    if (defined $cgi->param('add_products')) {
+        if (defined $cgi->param('prodlist')) {
+            foreach my $prod ($cgi->param("prodlist")) {
+                trick_taint($prod);
+                $sth->execute($classification_id,$prod);
+            }
+        }
+    } elsif (defined $cgi->param('remove_products')) {
+        if (defined $cgi->param('myprodlist')) {
+            foreach my $prod ($cgi->param("myprodlist")) {
+                trick_taint($prod);
+                $sth->execute(1,$prod);
+            }
+        }
+    } elsif (defined $cgi->param('migrate_products')) {
+        if (defined $cgi->param('clprodlist')) {
+            foreach my $prod ($cgi->param("clprodlist")) {
+                trick_taint($prod);
+                $sth->execute($classification_id,$prod);
+            }
+        }
+    }
+
+    my @selected_products = ();
+    my @class_products = ();
+
+    $sth = $dbh->prepare("SELECT classifications.id,
+                                 products.name,
+                                 classifications.name,
+                                 classifications.id > 1 as unknown
+                           FROM products,classifications
+                           WHERE classifications.id=products.classification_id
+                           ORDER BY unknown, products.name, classifications.name");
+    $sth->execute();
+    while ( my ($clid, $name, $clname) = $sth->fetchrow_array() ) {
+        if ($clid == $classification_id) {
+            push(@selected_products,$name);
+        } else {
+            my $cl = {};
+            if ($clid == 1) {
+                $cl->{'name'} = "[$clname] $name";
+            } else {
+                $cl->{'name'} = "$name [$clname]";
+            }
+            $cl->{'value'} = $name;
+            push(@class_products,$cl);
+        }
+    }
+    $vars->{'selected_products'} = \@selected_products;
+    $vars->{'class_products'} = \@class_products;
+
+    LoadTemplate($action);
+}
+
+#
+# No valid action found
+#
+
+ThrowCodeError("action_unrecognized", $vars);
index bd71bdd6dc9f722cc7b2e8a5de88a33c07cd8482..74a62166e69ce946c39d97664da15b179d1c8ce7 100755 (executable)
@@ -86,20 +86,80 @@ sub CheckProduct ($)
     }
 }
 
+# TestClassification:  just returns if the specified classification does exists
+# CheckClassification: same check, optionally  emit an error text
+
+sub TestClassification ($)
+{
+    my $cl = shift;
+
+    # does the classification exist?
+    SendSQL("SELECT name
+             FROM classifications
+             WHERE name=" . SqlQuote($cl));
+    return FetchOneColumn();
+}
+
+sub CheckClassification ($)
+{
+    my $cl = shift;
+
+    # do we have a classification?
+    unless ($cl) {
+        print "Sorry, you haven't specified a classification.";
+        PutTrailer();
+        exit;
+    }
+
+    unless (TestClassification $cl) {
+        print "Sorry, classification '$cl' does not exist.";
+        PutTrailer();
+        exit;
+    }
+}
+
+sub CheckClassificationProduct ($$)
+{
+    my $cl = shift;
+    my $prod = shift;
+
+    CheckClassification($cl);
+    CheckProduct($prod);
+
+    # does the classification exist?
+    SendSQL("SELECT products.name
+             FROM products,classifications
+             WHERE products.name=" . SqlQuote($prod) .
+            " AND classifications.name=" . SqlQuote($cl));
+    my $res = FetchOneColumn();
+
+    unless ($res) {
+        print "Sorry, classification->product '$cl'->'$prod' does not exist.";
+        PutTrailer();
+        exit;
+    }
+}
+
 
 #
 # Displays the form to edit a products parameters
 #
 
-sub EmitFormElements ($$$$$$$$)
+sub EmitFormElements ($$$$$$$$$)
 {
-    my ($product, $description, $milestoneurl, $disallownew,
+    my ($classification, $product, $description, $milestoneurl, $disallownew,
         $votesperuser, $maxvotesperbug, $votestoconfirm, $defaultmilestone)
         = @_;
 
     $product = value_quote($product);
     $description = value_quote($description);
 
+    if (Param('useclassification')) {
+        print "  <TH ALIGN=\"right\">Classification:</TH>\n";
+        print "  <TD><b>",html_quote($classification),"</b></TD>\n";
+        print "</TR><TR>\n";
+    }
+
     print "  <TH ALIGN=\"right\">Product:</TH>\n";
     print "  <TD><INPUT SIZE=64 MAXLENGTH=64 NAME=\"product\" VALUE=\"$product\"></TD>\n";
     print "</TR><TR>\n";
@@ -197,23 +257,83 @@ unless (UserInGroup("editcomponents")) {
 #
 # often used variables
 #
+my $classification = trim($::FORM{classification} || '');
 my $product = trim($::FORM{product} || '');
 my $action  = trim($::FORM{action}  || '');
 my $headerdone = 0;
 my $localtrailer = "<A HREF=\"editproducts.cgi\">edit</A> more products";
+my $classhtmlvarstart = "";
+my $classhtmlvar = "";
+
+if (Param('useclassification') && (defined $classification)) {
+   $classhtmlvar = "&classification=" . url_quote($classification);
+   $classhtmlvarstart = "?classification=" . url_quote($classification);
+}
+
+if (Param('useclassification') && (defined $classification)) {
+    $localtrailer .= ", <A HREF=\"editproducts.cgi" . $classhtmlvarstart . "\">edit</A> in this classification";
+}
+
+#
+# product = '' -> Show nice list of products
+#
+
+if (Param('useclassification')) {
+    unless ($classification) {
+        PutHeader("Select classification");
+
+        SendSQL("SELECT classifications.name,classifications.description,COUNT(classification_id) as total
+             FROM classifications
+             LEFT JOIN products ON classifications.id=products.classification_id
+             GROUP BY classifications.id
+             ORDER BY name");
+        print "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0><TR BGCOLOR=\"#6666FF\">\n";
+        print "  <TH ALIGN=\"left\">Edit products of ...</TH>\n";
+        print "  <TH ALIGN=\"left\">Description</TH>\n";
+        print "  <TH ALIGN=\"left\">Total</TH>\n";
+        print "</TR>";
+        while ( MoreSQLData() ) {
+            my ($classification, $description, $count) = FetchSQLData();
+            $description ||= "<FONT COLOR=\"red\">missing</FONT>";
+            print "<TR>\n";
+            print "  <TD VALIGN=\"top\"><A HREF=\"editproducts.cgi?classification=",url_quote($classification),"\"><B>$classification</B></A></TD>\n";
+            print "  <TD VALIGN=\"top\">$description</TD>\n";
+            $count ||= "none";
+            print "  <TD VALIGN=\"top\">$count</TD>\n";
+        }
+        print "</TR></TABLE>\n";
+
+        PutTrailer();
+        exit;
+    }
+}
+
 
 #
 # action='' -> Show nice list of products
 #
 
 unless ($action) {
-    PutHeader("Select product");
+    if (Param('useclassification')) {
+        PutHeader("Select product in " . $classification);
+    } else {
+        PutHeader("Select product");
+    }
 
-    SendSQL("SELECT products.name,description,disallownew,
+    my $query="SELECT products.name,products.description,disallownew,
                     votesperuser,maxvotesperbug,votestoconfirm,COUNT(bug_id)
-             FROM products LEFT JOIN bugs ON products.id = bugs.product_id
-             GROUP BY products.name
-             ORDER BY products.name");
+             FROM products";
+    if (Param('useclassification')) {
+        $query .= ",classifications";
+    }
+    $query .= " LEFT JOIN bugs ON products.id = bugs.product_id";
+    if (Param('useclassification')) {
+        $query .= " WHERE classifications.name=" .
+          SqlQuote($classification) .
+            " AND classifications.id=products.classification_id";
+    }
+    $query .= " GROUP BY products.name ORDER BY products.name";
+    SendSQL($query);
     print "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0><TR BGCOLOR=\"#6666FF\">\n";
     print "  <TH ALIGN=\"left\">Edit product ...</TH>\n";
     print "  <TH ALIGN=\"left\">Description</TH>\n";
@@ -231,19 +351,19 @@ unless ($action) {
         $disallownew = $disallownew ? 'closed' : 'open';
         $bugs        ||= 'none';
         print "<TR>\n";
-        print "  <TD VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=edit&product=", url_quote($product), "\"><B>$product</B></A></TD>\n";
+        print "  <TD VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=edit&product=", url_quote($product), $classhtmlvar,"\"><B>$product</B></A></TD>\n";
         print "  <TD VALIGN=\"top\">$description</TD>\n";
         print "  <TD VALIGN=\"top\">$disallownew</TD>\n";
         print "  <TD VALIGN=\"top\" ALIGN=\"right\">$votesperuser</TD>\n";
         print "  <TD VALIGN=\"top\" ALIGN=\"right\">$maxvotesperbug</TD>\n";
         print "  <TD VALIGN=\"top\" ALIGN=\"right\">$votestoconfirm</TD>\n";
         print "  <TD VALIGN=\"top\" ALIGN=\"right\">$bugs</TD>\n";
-        print "  <TD VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=del&product=", url_quote($product), "\">Delete</A></TD>\n";
+        print "  <TD VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=del&product=", url_quote($product), $classhtmlvar, "\">Delete</A></TD>\n";
         print "</TR>";
     }
     print "<TR>\n";
     print "  <TD VALIGN=\"top\" COLSPAN=7>Add a new product</TD>\n";
-    print "  <TD VALIGN=\"top\" ALIGN=\"middle\"><A HREF=\"editproducts.cgi?action=add\">Add</A></TD>\n";
+    print "  <TD VALIGN=\"top\" ALIGN=\"center\"><A HREF=\"editproducts.cgi?action=add&classification=", url_quote($classification),"\">Add</A></TD>\n";
     print "</TR></TABLE>\n";
 
     PutTrailer();
@@ -262,12 +382,15 @@ unless ($action) {
 if ($action eq 'add') {
     PutHeader("Add product");
 
+    if (Param('useclassification')) {
+        CheckClassification($classification);
+    }
     #print "This page lets you add a new product to bugzilla.\n";
 
     print "<FORM METHOD=POST ACTION=editproducts.cgi>\n";
     print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n";
 
-    EmitFormElements('', '', '', 0, 0, 10000, 0, "---");
+    EmitFormElements($classification,'', '', '', 0, 0, 10000, 0, "---");
 
     print "</TR><TR>\n";
     print "  <TH ALIGN=\"right\">Version:</TH>\n";
@@ -282,6 +405,7 @@ if ($action eq 'add') {
     print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"new\">\n";
     print "<INPUT TYPE=HIDDEN NAME='subcategory' VALUE='-All-'>\n";
     print "<INPUT TYPE=HIDDEN NAME='open_name' VALUE='All Open'>\n";
+    print "<INPUT TYPE=HIDDEN NAME='classification' VALUE='",html_quote($classification),"'>\n";
     print "</FORM>";
 
     my $other = $localtrailer;
@@ -349,10 +473,15 @@ if ($action eq 'new') {
     $votestoconfirm ||= 0;
     my $defaultmilestone = $::FORM{defaultmilestone} || "---";
 
+    my $classification_id = 1;
+    if (Param('useclassification')) {
+        $classification_id = get_classification_id($classification);
+    }
+
     # Add the new product.
     SendSQL("INSERT INTO products ( " .
             "name, description, milestoneurl, disallownew, votesperuser, " .
-            "maxvotesperbug, votestoconfirm, defaultmilestone" .
+            "maxvotesperbug, votestoconfirm, defaultmilestone, classification_id" .
             " ) VALUES ( " .
             SqlQuote($product) . "," .
             SqlQuote($description) . "," .
@@ -366,9 +495,11 @@ if ($action eq 'new') {
             SqlQuote($votesperuser) . "," .
             SqlQuote($maxvotesperbug) . "," .
             SqlQuote($votestoconfirm) . "," .
-            SqlQuote($defaultmilestone) . ")");
+            SqlQuote($defaultmilestone) . "," .
+            SqlQuote($classification_id) . ")");
     SendSQL("SELECT LAST_INSERT_ID()");
     my $product_id = FetchOneColumn();
+
     SendSQL("INSERT INTO versions ( " .
           "value, product_id" .
           " ) VALUES ( " .
@@ -455,7 +586,8 @@ if ($action eq 'new') {
     PutTrailer($localtrailer,
         "<a href=\"editproducts.cgi?action=add\">add</a> a new product",
         "<a href=\"editcomponents.cgi?action=add&product=" .
-        url_quote($product) . "\">add</a> components to this new product");
+        url_quote($product) . $classhtmlvar .
+        "\">add</a> components to this new product");
     exit;
 }
 
@@ -470,15 +602,23 @@ if ($action eq 'new') {
 if ($action eq 'del') {
     PutHeader("Delete product");
     CheckProduct($product);
+    my $classification_id=1;
+    if (Param('useclassification')) {
+        CheckClassificationProduct($classification,$product);
+        $classification_id = get_classification_id($classification);
+    }
 
     # display some data about the product
-    SendSQL("SELECT id, description, milestoneurl, disallownew
-             FROM products
-             WHERE name=" . SqlQuote($product));
-    my ($product_id, $description, $milestoneurl, $disallownew) = FetchSQLData();
+    SendSQL("SELECT classifications.description,
+                    products.id, products.description, milestoneurl, disallownew
+             FROM products,classifications
+             WHERE products.name=" . SqlQuote($product) .
+            " AND classifications.id=" . SqlQuote($classification_id));
+    my ($class_description, $product_id, $prod_description, $milestoneurl, $disallownew) = FetchSQLData();
     my $milestonelink = $milestoneurl ? "<a href=\"$milestoneurl\">$milestoneurl</a>"
                                       : "<font color=\"red\">missing</font>";
-    $description ||= "<FONT COLOR=\"red\">description missing</FONT>";
+    $prod_description ||= "<FONT COLOR=\"red\">description missing</FONT>";
+    $class_description ||= "<FONT COLOR=\"red\">description missing</FONT>";
     $disallownew = $disallownew ? 'closed' : 'open';
     
     print "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0>\n";
@@ -486,13 +626,23 @@ if ($action eq 'del') {
     print "  <TH VALIGN=\"top\" ALIGN=\"left\">Part</TH>\n";
     print "  <TH VALIGN=\"top\" ALIGN=\"left\">Value</TH>\n";
 
+    if (Param('useclassification')) {
+        print "</TR><TR>\n";
+        print "  <TD VALIGN=\"top\">Classification:</TD>\n";
+        print "  <TD VALIGN=\"top\">$classification</TD>\n";
+
+        print "</TR><TR>\n";
+        print "  <TD VALIGN=\"top\">Description:</TD>\n";
+        print "  <TD VALIGN=\"top\">$class_description</TD>\n";
+    }
+
     print "</TR><TR>\n";
     print "  <TD VALIGN=\"top\">Product:</TD>\n";
     print "  <TD VALIGN=\"top\">$product</TD>\n";
 
     print "</TR><TR>\n";
     print "  <TD VALIGN=\"top\">Description:</TD>\n";
-    print "  <TD VALIGN=\"top\">$description</TD>\n";
+    print "  <TD VALIGN=\"top\">$prod_description</TD>\n";
 
     if (Param('usetargetmilestone')) {
         print "</TR><TR>\n";
@@ -548,7 +698,7 @@ if ($action eq 'del') {
     #
     if (Param('usetargetmilestone')) {
         print "</TD>\n</TR><TR>\n";
-        print "  <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote($product), "\">Edit milestones:</A></TH>\n";
+        print "  <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote($product), $classhtmlvar, "\">Edit milestones:</A></TH>\n";
         print "  <TD>";
         SendSQL("SELECT value
                  FROM milestones
@@ -603,6 +753,8 @@ one.";
     print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"delete\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"product\" VALUE=\"" .
         html_quote($product) . "\">\n";
+    print "<INPUT TYPE=HIDDEN NAME=\"classification\" VALUE=\"" .
+        html_quote($classification) . "\">\n";
     print "</FORM>";
 
     PutTrailer($localtrailer);
@@ -706,25 +858,32 @@ if ($action eq 'delete') {
 if ($action eq 'edit') {
     PutHeader("Edit product");
     CheckProduct($product);
+    my $classification_id=1;
+    if (Param('useclassification')) {
+        CheckClassificationProduct($classification,$product);
+        $classification_id = get_classification_id($classification);
+    }
 
     # get data of product
-    SendSQL("SELECT id,description,milestoneurl,disallownew,
+    SendSQL("SELECT classifications.description,
+                    products.id,products.description,milestoneurl,disallownew,
                     votesperuser,maxvotesperbug,votestoconfirm,defaultmilestone
-             FROM products
-             WHERE name=" . SqlQuote($product));
-    my ($product_id,$description, $milestoneurl, $disallownew,
+             FROM products,classifications
+             WHERE products.name=" . SqlQuote($product) .
+            " AND classifications.id=" . SqlQuote($classification_id));
+    my ($class_description, $product_id,$prod_description, $milestoneurl, $disallownew,
         $votesperuser, $maxvotesperbug, $votestoconfirm, $defaultmilestone) =
         FetchSQLData();
 
     print "<FORM METHOD=POST ACTION=editproducts.cgi>\n";
     print "<TABLE  BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n";
 
-    EmitFormElements($product, $description, $milestoneurl, 
+    EmitFormElements($classification, $product, $prod_description, $milestoneurl, 
                      $disallownew, $votesperuser, $maxvotesperbug,
                      $votestoconfirm, $defaultmilestone);
     
     print "</TR><TR VALIGN=top>\n";
-    print "  <TH ALIGN=\"right\"><A HREF=\"editcomponents.cgi?product=", url_quote($product), "\">Edit components:</A></TH>\n";
+    print "  <TH ALIGN=\"right\"><A HREF=\"editcomponents.cgi?product=", url_quote($product), $classhtmlvar, "\">Edit components:</A></TH>\n";
     print "  <TD>";
     SendSQL("SELECT name,description
              FROM components
@@ -744,7 +903,7 @@ if ($action eq 'edit') {
 
 
     print "</TD>\n</TR><TR>\n";
-    print "  <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editversions.cgi?product=", url_quote($product), "\">Edit versions:</A></TH>\n";
+    print "  <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editversions.cgi?product=", url_quote($product), $classhtmlvar, "\">Edit versions:</A></TH>\n";
     print "  <TD>";
     SendSQL("SELECT value
              FROM versions
@@ -767,7 +926,7 @@ if ($action eq 'edit') {
     #
     if (Param('usetargetmilestone')) {
         print "</TD>\n</TR><TR>\n";
-        print "  <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote($product), "\">Edit milestones:</A></TH>\n";
+        print "  <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote($product), $classhtmlvar, "\">Edit milestones:</A></TH>\n";
         print "  <TD>";
         SendSQL("SELECT value
                  FROM milestones
@@ -787,7 +946,7 @@ if ($action eq 'edit') {
     }
 
     print "</TD>\n</TR><TR>\n";
-    print "  <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=editgroupcontrols&product=", url_quote($product), "\">Edit Group Access Controls</A></TH>\n";
+    print "  <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=editgroupcontrols&product=", url_quote($product), $classhtmlvar,"\">Edit Group Access Controls</A></TH>\n";
     print "<TD>\n";
     SendSQL("SELECT id, name, isactive, entry, membercontrol, othercontrol, canedit " .
             "FROM groups, " .
@@ -820,10 +979,12 @@ if ($action eq 'edit') {
 
     print "</TD>\n</TR></TABLE>\n";
 
+    print "<INPUT TYPE=HIDDEN NAME=\"classification\" VALUE=\"" .
+        html_quote($classification) . "\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"productold\" VALUE=\"" .
         html_quote($product) . "\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"descriptionold\" VALUE=\"" .
-        html_quote($description) . "\">\n";
+        html_quote($prod_description) . "\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"milestoneurlold\" VALUE=\"" .
         html_quote($milestoneurl) . "\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"disallownewold\" VALUE=\"$disallownew\">\n";
@@ -843,7 +1004,6 @@ if ($action eq 'edit') {
     exit;
 }
 
-
 #
 # action='updategroupcontrols' -> update the product
 #
@@ -1325,6 +1485,7 @@ if ($action eq 'editgroupcontrols') {
     }
     $vars->{'header_done'} = $headerdone;
     $vars->{'product'} = $product;
+    $vars->{'classification'} = $classification;
     $vars->{'groups'} = \@groups;
     $vars->{'const'} = {
         'CONTROLMAPNA' => CONTROLMAPNA,
index cbe2929aa1c8821d3db5900a62b713946a5bdd47..a0291fae569312d6dfa2f75c98b121df587e6038 100755 (executable)
@@ -53,6 +53,7 @@ use vars qw(
   $userid
   %versions
   $proddesc
+  $classdesc
 );
 
 # If we're using bug groups to restrict bug entry, we need to know who the 
@@ -67,12 +68,47 @@ if (!defined $product) {
     GetVersionTable();
     Bugzilla->login();
 
-    my %products;
+   if ( ! Param('useclassification') ) {
+      # just pick the default one
+      $::FORM{'classification'}=(keys %::classdesc)[0];
+   }
+
+   if (!defined $::FORM{'classification'}) {
+       my %classdesc;
+       my %classifications;
+    
+       foreach my $c (GetSelectableClassifications()) {
+           $classdesc{$c} = $::classdesc{$c};
+           $classifications{$c} = $::classifications{$c};
+       }
+
+       my $classification_size = scalar(keys %classdesc);
+       if ($classification_size == 0) {
+           ThrowUserError("no_products");
+       } 
+       elsif ($classification_size > 1) {
+           $vars->{'classdesc'} = \%classdesc;
+           $vars->{'classifications'} = \%classifications;
+
+           $vars->{'target'} = "enter_bug.cgi";
+           $vars->{'format'} = $::FORM{'format'};
+           
+           print "Content-type: text/html\n\n";
+           $template->process("global/choose-classification.html.tmpl", $vars)
+             || ThrowTemplateError($template->error());
+           exit;        
+       }
+       $::FORM{'classification'} = (keys %classdesc)[0];
+       $::MFORM{'classification'} = [$::FORM{'classification'}];
+   }
 
+    my %products;
     foreach my $p (@enterable_products) {
-        if (CanEnterProduct($p))
-        {
-            $products{$p} = $::proddesc{$p};
+        if (CanEnterProduct($p)) {
+            if (IsInClassification($::FORM{'classification'},$p) ||
+                $::FORM{'classification'} eq "__all") {
+                $products{$p} = $::proddesc{$p};
+            }
         }
     }
  
@@ -81,7 +117,19 @@ if (!defined $product) {
         ThrowUserError("no_products");
     } 
     elsif ($prodsize > 1) {
+        my %classifications;
+        if ( ! Param('useclassification') ) {
+            @{$classifications{"all"}} = keys %products;
+        }
+        elsif ($::FORM{'classification'} eq "__all") {
+            %classifications = %::classifications;
+        } else {
+            $classifications{$::FORM{'classification'}} =
+                $::classifications{$::FORM{'classification'}};
+        }
         $vars->{'proddesc'} = \%products;
+        $vars->{'classifications'} = \%classifications;
+        $vars->{'classdesc'} = \%::classdesc;
 
         $vars->{'target'} = "enter_bug.cgi";
         $vars->{'format'} = $cgi->param('format');
@@ -252,6 +300,7 @@ SendSQL("SELECT name, description, login_name, realname
              ORDER BY name");
 while (MoreSQLData()) {
     my ($name, $description, $login, $realname) = FetchSQLData();
+
     push @components, {
         name => $name,
         description => $description,
index 9872dff709ede959fb0f2ba9d6c189a755ecd004..829417e44000fe9af9a260407de1225d21da0e6a 100644 (file)
@@ -55,6 +55,7 @@ sub globals_pl_sillyness {
     $zz = @main::legal_versions;
     $zz = @main::milestoneurl;
     $zz = %main::proddesc;
+    $zz = %main::classdesc;
     $zz = @main::prodmaxvotes;
     $zz = $main::template;
     $zz = $main::userid;
@@ -184,6 +185,19 @@ sub GenerateVersionTable {
         $carray{$c} = 1;
     }
 
+    SendSQL("SELECT products.name, classifications.name " .
+            "FROM products, classifications " .
+            "WHERE classifications.id = products.classification_id " .
+            "ORDER BY classifications.name");
+    while (@line = FetchSQLData()) {
+        my ($p,$c) = (@line);
+        if (!defined $::classifications{$c}) {
+            $::classifications{$c} = [];
+        }
+        my $ref = $::classifications{$c};
+        push @$ref, $p;
+    }
+
     my $dotargetmilestone = 1;  # This used to check the param, but there's
                                 # enough code that wants to pretend we're using
                                 # target milestones, even if they don't get
@@ -191,6 +205,13 @@ sub GenerateVersionTable {
                                 # about them anyway.
 
     my $mpart = $dotargetmilestone ? ", milestoneurl" : "";
+
+    SendSQL("select name, description from classifications ORDER BY name");
+    while (@line = FetchSQLData()) {
+        my ($n, $d) = (@line);
+        $::classdesc{$n} = $d;
+    }
+
     SendSQL("select name, description, votesperuser, disallownew$mpart from products ORDER BY name");
     while (@line = FetchSQLData()) {
         my ($p, $d, $votesperuser, $dis, $u) = (@line);
@@ -275,8 +296,10 @@ sub GenerateVersionTable {
                                    '*::legal_bug_status', '*::legal_resolution']));
 
     print $fh (Data::Dumper->Dump([\@::settable_resolution, \%::proddesc,
+                                   \%::classifications, \%::classdesc,
                                    \@::enterable_products, \%::prodmaxvotes],
                                   ['*::settable_resolution', '*::proddesc',
+                                   '*::classifications', '*::classdesc',
                                    '*::enterable_products', '*::prodmaxvotes']));
 
     if ($dotargetmilestone) {
@@ -494,6 +517,24 @@ sub CanEditProductId {
     return (!defined($result));
 }
 
+sub IsInClassification {
+    my ($classification,$productname) = @_;
+
+    if (! Param('useclassification')) {
+        return 1;
+    } else {
+        my $query = "SELECT classifications.name " .
+          "FROM products,classifications " .
+            "WHERE products.classification_id=classifications.id ";
+        $query .= "AND products.name = " . SqlQuote($productname);
+        PushGlobalSQLState();
+        SendSQL($query);
+        my ($ret) = FetchSQLData();
+        PopGlobalSQLState();
+        return ($ret eq $classification);
+    }
+}
+
 #
 # This function determines if a user can enter bugs in the named
 # product.
@@ -527,18 +568,21 @@ sub GetEnterableProducts {
     return (@products);
 }
 
+
 #
 # This function returns an alphabetical list of product names to which
 # the user can enter bugs.  If the $by_id parameter is true, also retrieves IDs
 # and pushes them onto the list as id, name [, id, name...] for easy slurping
 # into a hash by the calling code.
 sub GetSelectableProducts {
-    my ($by_id) = @_;
+    my ($by_id,$by_classification) = @_;
 
     my $extra_sql = $by_id ? "id, " : "";
 
-    my $query = "SELECT $extra_sql name " .
-                "FROM products " .
+    my $extra_from_sql = $by_classification ? ", classifications" : "";
+
+    my $query = "SELECT $extra_sql products.name " .
+                "FROM products $extra_from_sql " .
                 "LEFT JOIN group_control_map " .
                 "ON group_control_map.product_id = products.id ";
     if (Param('useentrygroupdefault')) {
@@ -551,7 +595,13 @@ sub GetSelectableProducts {
         $query .= "AND group_id NOT IN(" . 
                    join(',', values(%{Bugzilla->user->groups})) . ") ";
     }
-    $query .= "WHERE group_id IS NULL ORDER BY name";
+    $query .= "WHERE group_id IS NULL ";
+    if ($by_classification) {
+        $query .= "AND classifications.id = products.classification_id ";
+        $query .= "AND classifications.name = ";
+        $query .= SqlQuote($by_classification) . " ";
+    }
+    $query .= "ORDER BY name";
     PushGlobalSQLState();
     SendSQL($query);
     my @products = ();
@@ -609,6 +659,18 @@ sub GetSelectableProductHash {
     return $selectables;
 }
 
+#
+# This function returns an alphabetical list of classifications that has products the user can enter bugs.
+sub GetSelectableClassifications {
+    my @selectable_classes = ();
+
+    foreach my $c (keys %::classdesc) {
+        if ( scalar(GetSelectableProducts(0,$c)) > 0) {
+           push(@selectable_classes,$c);
+        }
+    }
+    return (@selectable_classes);
+}
 
 sub GetFieldDefs {
     my $extra = "";
@@ -740,6 +802,25 @@ sub DBNameToIdAndCheck {
                    { name => $name }, "abort");
 }
 
+sub get_classification_id {
+    my ($classification) = @_;
+    PushGlobalSQLState();
+    SendSQL("SELECT id FROM classifications WHERE name = " . SqlQuote($classification));
+    my ($classification_id) = FetchSQLData();
+    PopGlobalSQLState();
+    return $classification_id;
+}
+
+sub get_classification_name {
+    my ($classification_id) = @_;
+    die "non-numeric classification_id '$classification_id' passed to get_classification_name"
+      unless ($classification_id =~ /^\d+$/);
+    PushGlobalSQLState();
+    SendSQL("SELECT name FROM classifications WHERE id = $classification_id");
+    my ($classification) = FetchSQLData();
+    PopGlobalSQLState();
+    return $classification;
+}
 
 
 
index 0be0d49715e88d4da2c6c2133474a0726f1640cb..7cf07d732a38e20861f64e40028c97af73676212 100644 (file)
 /* this file contains functions to update form controls based on a
  * collection of javascript arrays containing strings */
 
+/* selectClassification reads the selection from f.classification and updates
+ * f.product accordingly
+ *     - f: a form containing classification, product, component, varsion and
+ *       target_milestone select boxes.
+ * globals (3vil!):
+ *     - prods, indexed by classification name
+ *     - first_load: boolean, specifying if it is the first time we load
+ *       the query page.
+ *     - last_sel: saves our last selection list so we know what has
+ *       changed, and optimize for additions.
+ */
+function selectClassification(classfield, product, component, version, milestone) {
+    /* this is to avoid handling events that occur before the form
+     * itself is ready, which could happen in buggy browsers.
+     */
+    if (!classfield) {
+        return;
+    }
+
+    /* if this is the first load and nothing is selected, no need to
+     * merge and sort all components; perl gives it to us sorted.
+     */
+    if ((first_load) && (classfield.selectedIndex == -1)) {
+        first_load = false;
+        return;
+    }
+    
+    /* don't reset first_load as done in selectProduct.  That's because we
+       want selectProduct to handle the first_load attribute
+    */
+
+    /* - sel keeps the array of classifications we are selected. 
+     * - merging says if it is a full list or just a list of classifications 
+     *   that were added to the current selection.
+     */
+    var merging = false;
+    var sel = Array();
+
+    /* if nothing selected, pick all */
+    var findall = classfield.selectedIndex == -1;
+    sel = get_selection(classfield, findall, false);
+    if (!findall) {
+        /* save sel for the next invocation of selectClassification() */
+        var tmp = sel;
+    
+        /* this is an optimization: if we have just added classifications to an
+         * existing selection, no need to clear the form controls and add 
+         * everybody again; just merge the new ones with the existing 
+         * options.
+        */
+        if ((last_sel.length > 0) && (last_sel.length < sel.length)) {
+            sel = fake_diff_array(sel, last_sel);
+            merging = true;
+        }
+        last_sel = tmp;
+    }
+    /* save original options selected */
+    var saved_prods = get_selection(product, false, true);
+
+    /* do the actual fill/update, reselect originally selected options */
+    updateSelect(prods, sel, product, merging);
+    restoreSelection(product, saved_prods);
+    selectProduct(product, component, version, milestone);
+}
+
+
 /* selectProduct reads the selection from the product control and
  * updates version, component and milestone controls accordingly.
  * 
@@ -68,7 +134,15 @@ function selectProduct(product, component, version, milestone) {
 
     /* if nothing selected, pick all */
     var findall = product.selectedIndex == -1;
-    sel = get_selection(product, findall, false);
+    if (useclassification) {
+        /* update index based on the complete product array */
+        sel = get_selection(product, findall, true);
+        for (var i=0; i<sel.length; i++) {
+           sel[i] = prods[sel[i]];
+        }
+    } else {
+        sel = get_selection(product, findall, false);
+    }
     if (!findall) {
         /* save sel for the next invocation of selectProduct() */
         var tmp = sel;
index 757d002396cd97716027e81c7164796d0b0d3682..515f4c2260d9e2256ccb367da360a28339455268 100755 (executable)
@@ -43,6 +43,7 @@ my $generic_query = "
   SELECT 
     bugs.bug_id, 
     COALESCE(bugs.alias, ''), 
+    classifications.name, 
     products.name, 
     bugs.version, 
     bugs.rep_platform,
@@ -63,9 +64,10 @@ my $generic_query = "
     bugs.estimated_time,
     bugs.remaining_time,
     date_format(creation_ts,'%Y.%m.%d %H:%i')
-  FROM bugs,profiles assign,profiles report, products, components
+  FROM bugs,profiles assign,profiles report, classifications, products, components
   WHERE assign.userid = bugs.assigned_to AND report.userid = bugs.reporter
-    AND bugs.product_id=products.id AND bugs.component_id=components.id";
+    AND bugs.product_id=products.id AND bugs.component_id=components.id
+    AND products.classification_id = classifications.id";
 
 my $buglist = $cgi->param('buglist') || 
               $cgi->param('bug_id')  || 
@@ -81,7 +83,8 @@ foreach my $bug_id (split(/[:,]/, $buglist)) {
     my %bug;
     my @row = FetchSQLData();
 
-    foreach my $field ("bug_id", "alias", "product", "version", "rep_platform",
+    foreach my $field ("bug_id", "alias", "classification", "product",
+                       "version", "rep_platform",
                        "op_sys", "bug_status", "resolution", "priority",
                        "bug_severity", "component", "assigned_to", "reporter",
                        "bug_file_loc", "short_desc", "target_milestone",
index bacb5cd3c264c16d44fd0fc309baeea32a9926e7..c22a11471fe06c59b62919ce5a890c5e10afc640 100755 (executable)
--- a/query.cgi
+++ b/query.cgi
@@ -127,7 +127,7 @@ sub PrefillForm {
     # Nothing must be undef, otherwise the template complains.
     foreach my $name ("bug_status", "resolution", "assigned_to",
                       "rep_platform", "priority", "bug_severity",
-                      "product", "reporter", "op_sys",
+                      "classification", "product", "reporter", "op_sys",
                       "component", "version", "chfield", "chfieldfrom",
                       "chfieldto", "chfieldvalue", "target_milestone",
                       "email", "emailtype", "emailreporter",
@@ -274,9 +274,24 @@ for (my $i = 0; $i < @products; ++$i) {
     # Assign hash back to product array.
     $products[$i] = \%product;
 }
-
 $vars->{'product'} = \@products;
 
+# Create data structures representing each classification
+if (Param('useclassification')) {
+    my @classifications = ();
+
+    foreach my $c (sort(GetSelectableClassifications())) {
+        # Create hash to hold attributes for each classification.
+        my %classification = (
+            'name'       => $c,
+            'products'   => [ GetSelectableProducts(0,$c) ]
+        );
+        # Assign hash back to classification array.
+        push @classifications, \%classification;
+    }
+    $vars->{'classification'} = \@classifications;
+}
+
 # We use 'component_' because 'component' is a Template Toolkit reserved word.
 $vars->{'component_'} = \@components;
 
@@ -300,7 +315,9 @@ push @chfields, "[Bug creation]";
 # This is what happens when you have variables whose definition depends
 # on the DB schema, and then the underlying schema changes...
 foreach my $val (@::log_columns) {
-    if ($val eq 'product_id') {
+    if ($val eq 'classification_id') {
+        $val = 'classification';
+    } elsif ($val eq 'product_id') {
         $val = 'product';
     } elsif ($val eq 'component_id') {
         $val = 'component';
index 66060723d0a0e01ae51daad269cc762f395cf511..fac2f0e7577bc5346b87fc8087f2afb83351c323 100755 (executable)
@@ -113,6 +113,7 @@ $columns{'bug_status'}       = "bugs.bug_status";
 $columns{'resolution'}       = "bugs.resolution";
 $columns{'component'}        = "map_components.name";
 $columns{'product'}          = "map_products.name";
+$columns{'classification'}   = "map_classifications.name";
 $columns{'version'}          = "bugs.version";
 $columns{'op_sys'}           = "bugs.op_sys";
 $columns{'votes'}            = "bugs.votes";
@@ -134,8 +135,13 @@ $columns{''}                 = "42217354";
   || ThrowCodeError("report_axis_invalid", {fld => "z", val => $tbl_field});
 
 my @axis_fields = ($row_field, $col_field, $tbl_field);
-
 my @selectnames = map($columns{$_}, @axis_fields);
+# add product if person is requesting classification
+if (lsearch(\@axis_fields,"classification") >= 0) {
+    if (lsearch(\@axis_fields,"product") < 0) {
+        push(@selectnames,($columns{'product'}));
+    }
+}
 
 # Clone the params, so that Bugzilla::Search can modify them
 my $params = new Bugzilla::CGI($cgi);
diff --git a/template/en/default/admin/classifications/add.html.tmpl b/template/en/default/admin/classifications/add.html.tmpl
new file mode 100644 (file)
index 0000000..d6a7c38
--- /dev/null
@@ -0,0 +1,45 @@
+[%# 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): Albert Ting <alt@sonic.net>
+  #%]
+
+[% PROCESS global/header.html.tmpl
+  title = "Add new classification"
+%]
+
+<form method=post action="editclassifications.cgi">
+  <table border=0 cellpadding=4 cellspacing=0>
+    <tr>
+      <th align="right">Classification:</th>
+      <td><input size=64 maxlength=64 name="classification"></td>
+    </tr>
+    <tr>
+      <th align="right">Description:</th>
+      <td><textarea rows=4 cols=64 wrap=virtual name="description"></textarea></td>
+    </tr>
+  </table>
+  <hr>
+  <input type=submit value="Add">
+  <input type=hidden name="action" value="new">
+</FORM>
+
+<p>Back to the <a href="./">main [% terms.bugs %] page</a>
+or <a href="editclassifications.cgi"> edit</a> more classifications.
+
+[% PROCESS global/footer.html.tmpl %] 
diff --git a/template/en/default/admin/classifications/del.html.tmpl b/template/en/default/admin/classifications/del.html.tmpl
new file mode 100644 (file)
index 0000000..0089715
--- /dev/null
@@ -0,0 +1,60 @@
+[%# 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): Albert Ting <alt@sonic.net>
+  #%]
+
+[% PROCESS global/header.html.tmpl
+  title = "Delete classification"
+%]
+
+<table border=1 cellpadding=4 cellspacing=0>
+<tr bgcolor="#6666ff">
+  <th valign="top" align="left">Part</th>
+  <th valign="top" align="left">Value</th>
+
+</tr><tr>
+  <td valign="top">Classification:</td>
+  <td valign="top">[% classification FILTER html %]</td>
+
+</tr><tr>
+  <td valign="top">Description:</td>
+  <td valign="top">
+    [% IF description %]
+      [% description FILTER html %]
+    [% ELSE %]
+      <font color="red">description missing</font>
+    [% END %]
+  </td>
+
+</tr>
+</table>
+
+<h2>Confirmation</h2>
+
+<p>Do you really want to delete this classification?<p>
+<form method=post action="editclassifications.cgi">
+  <input type=submit value="Yes, delete">
+  <input type=hidden name="action" value="delete">
+  <input type=hidden name="classification" value="[% classification FILTER html %]">
+</form>
+
+<p>Back to the <a href="./">main [% terms.bugs %] page</a>
+or <a href="editclassifications.cgi"> edit</a> more classifications.
+
+[% PROCESS global/footer.html.tmpl %] 
diff --git a/template/en/default/admin/classifications/delete.html.tmpl b/template/en/default/admin/classifications/delete.html.tmpl
new file mode 100644 (file)
index 0000000..b2ec26c
--- /dev/null
@@ -0,0 +1,31 @@
+[%# 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): Albert Ting <alt@sonic.net>
+  #%]
+
+[% PROCESS global/header.html.tmpl
+  title = "Classification deleted"
+%]
+
+Classification [% classification FILTER html %] deleted.<br>
+
+<p>Back to the <a href="./">main [% terms.bugs %] page</a>
+or <a href="editclassifications.cgi"> edit</a> more classifications.
+
+[% PROCESS global/footer.html.tmpl %] 
diff --git a/template/en/default/admin/classifications/edit.html.tmpl b/template/en/default/admin/classifications/edit.html.tmpl
new file mode 100644 (file)
index 0000000..ebc16e8
--- /dev/null
@@ -0,0 +1,70 @@
+[%# 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): Albert Ting <alt@sonic.net>
+  #%]
+
+[% PROCESS global/header.html.tmpl
+  title = "Edit classification"
+%]
+
+<form method=post action="editclassifications.cgi">
+  <table  border=0 cellpadding=4 cellspacing=0>
+    <tr>
+      <th align="right">Classification:</th>
+      <td><input size=64 maxlength=64 name="classification" value="[% classification FILTER html %]"></TD>
+    </tr>
+    <tr>
+      <th align="right">Description:</th>
+      <td><textarea rows=4 cols=64 name="description">[% description FILTER html %]</textarea></TD>
+    </tr>
+    <tr valign=top>
+      <th align="right"><a href="editproducts.cgi?classification=[% classification FILTER html %]">Edit products</a></th>
+      <td>
+        [% IF products AND products.size > 0 %]
+          <table>
+            [% FOREACH product = products %]
+              <tr>
+                <th align=right valign=top>[% product.name FILTER html %]</th>
+                <td valign=top>
+                  [% IF product.description %]
+                    [% product.description FILTER html %]
+                  [% ELSE %]
+                    <font color="red">description missing</font>
+                  [% END %]
+                </td>
+              </tr>
+            [% END %]
+          </table>
+        [% ELSE %]
+          <font color="red">none</font>
+        [% END %]
+      </td>
+    </tr>
+  </table>
+
+  <input type=hidden name="classificationold" value="[% classification FILTER html %]">
+  <input type=hidden name="descriptionold" value="[% description FILTER html %]">
+  <input type=hidden name="action" value="update">
+  <input type=submit value="Update">
+</form>
+
+<p>Back to the <a href="./">main [% terms.bugs %] page</a>
+or <a href="editclassifications.cgi"> edit</a> more classifications.
+
+[% PROCESS global/footer.html.tmpl %] 
diff --git a/template/en/default/admin/classifications/new.html.tmpl b/template/en/default/admin/classifications/new.html.tmpl
new file mode 100644 (file)
index 0000000..c8046b8
--- /dev/null
@@ -0,0 +1,32 @@
+[%# 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): Albert Ting <alt@sonic.net>
+  #%]
+
+[% PROCESS global/header.html.tmpl
+  title = "Adding new classification"
+%]
+
+OK, done.
+
+<p>Back to the <a href="./">main [% terms.bugs %] page</a>,
+<a href="editproducts.cgi?action=add&amp;classification=[% classification FILTER html %]">add</a> products to this new classification, 
+or <a href="editclassifications.cgi"> edit</a> more classifications.
+
+[% PROCESS global/footer.html.tmpl %] 
diff --git a/template/en/default/admin/classifications/reclassify.html.tmpl b/template/en/default/admin/classifications/reclassify.html.tmpl
new file mode 100644 (file)
index 0000000..d860f67
--- /dev/null
@@ -0,0 +1,84 @@
+[%# 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): Albert Ting <alt@sonic.net>
+  #%]
+
+[% PROCESS global/header.html.tmpl
+  title = "Reclassify products"
+%]
+
+[% main_classification = classification %]
+
+<form method=post action="editclassifications.cgi">
+  <table border=0 cellpadding=4 cellspacing=0>
+    <tr>
+      <td valign="top">Classification:</td>
+      <td valign="top" colspan=3>[% main_classification FILTER html %]</td>
+
+    </tr><tr>
+      <td valign="top">Description:</td>
+      <td valign="top" colspan=3>
+        [% IF description %]
+          [% description FILTER html %]
+        [% ELSE %]
+          <font color="red">description missing</font>
+        [% END %]
+      </td>
+
+    </tr><tr>
+      <td valign="top">Products:</td>
+      <td valign="top">Products</td>
+      <td></td>
+      <td valign="top">[% main_classification FILTER html %] Products</td>
+
+    </tr><tr>
+      <td></td>
+      <td valign="top">
+      <select name="prodlist" id="prodlist" multiple="multiple" size="20">
+        [% FOREACH cl = class_products %]
+          <option value="[% cl.value FILTER html %]">
+             [% cl.name FILTER html %]
+          </option>
+        [% END %]
+      </select></td>
+
+      <td align="center">
+        <input type=submit value="     Add &gt;&gt;  " name="add_products"><br><br>
+        <input type=submit value="&lt;&lt; Remove" name="remove_products">
+      </td>
+
+      <td valign="middle" rowspan=2>
+        <select name="myprodlist" id="myprodlist" multiple="multiple" size="20">
+          [% FOREACH product = selected_products %]
+            <option value="[% product FILTER html %]">
+              [% product FILTER html %]
+            </option>
+          [% END %]
+      </select></td>
+    </tr>
+  </table>
+
+  <input type=hidden name="action" value="reclassify">
+  <input type=hidden name="classification" value="[% main_classification FILTER html %]">
+</form>
+
+<p>Back to the <a href="./">main [% terms.bugs %] page</a>,
+or <a href="editclassifications.cgi"> edit</a> more classifications.
+
+[% PROCESS global/footer.html.tmpl %] 
diff --git a/template/en/default/admin/classifications/select.html.tmpl b/template/en/default/admin/classifications/select.html.tmpl
new file mode 100644 (file)
index 0000000..5908375
--- /dev/null
@@ -0,0 +1,70 @@
+[%# 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): Albert Ting <alt@sonic.net>
+  #%]
+
+[% PROCESS global/header.html.tmpl
+  title = "Select classification"
+%]
+
+[% filt_classification = classification FILTER html %]
+
+<table border=1 cellpadding=4 cellspacing=0>
+  <tr bgcolor="#6666ff">
+    <th align="left">Edit Classification ...</th>
+    <th align="left">Description</th>
+    <th align="left">Products</th>
+    <th align="left">Action</th>
+  </tr>
+
+  [% FOREACH cl = classifications %]
+    <tr>
+      <td valign="top"><a href="editclassifications.cgi?action=edit&amp;classification=[% cl.classification FILTER html %]"><b>[% cl.classification FILTER html %]</b></a></td>
+      <td valign="top"> 
+      [% IF cl.description %]
+        [% cl.description FILTER html %]
+      [% ELSE %]
+        <font color="red">none</font>
+      [% END %]
+      </td>
+      [% IF (cl.id == 1) %]
+        <td valign="top">[% cl.total FILTER html %]</td>
+      [% ELSE %]
+        <td valign="top"><a href="editclassifications.cgi?action=reclassify&amp;classification=[% cl.classification FILTER html %]">reclassify ([% cl.total FILTER html %])</a></td>
+      [% END %]
+
+      [%# don't allow user to delete the default id. %]
+      [% IF (cl.id == 1) %]
+        <td valign="top">&nbsp;</td>
+      [% ELSE %]
+        <td valign="top"><a href="editclassifications.cgi?action=del&amp;classification=[% cl.classification FILTER html %]">delete</a></td>
+      [% END %]
+    </tr>
+  [% END %]
+
+  <tr>
+    <td valign="top" colspan=3>Add a new classification</td>
+    <td valign="top" align="center"><a href="editclassifications.cgi?action=add">Add</a></td>
+  </tr>
+</table>
+
+<p>Back to the <a href="./">main [% terms.bugs %] page</a>
+or <a href="editclassifications.cgi"> edit</a> more classifications.
+
+[% PROCESS global/footer.html.tmpl %] 
diff --git a/template/en/default/admin/classifications/update.html.tmpl b/template/en/default/admin/classifications/update.html.tmpl
new file mode 100644 (file)
index 0000000..3ad7bbc
--- /dev/null
@@ -0,0 +1,37 @@
+[%# 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): Albert Ting <alt@sonic.net>
+  #%]
+
+[% PROCESS global/header.html.tmpl
+  title = "Update classification"
+%]
+
+[% IF updated_description %] 
+  Updated description.<br>
+[% END %]
+
+[% IF updated_classification %] 
+  Updated classification name.<br>
+[% END %]
+
+<p>Back to the <a href="./">main [% terms.bugs %] page</a>
+or <a href="editclassifications.cgi"> edit</a> more classifications.
+
+[% PROCESS global/footer.html.tmpl %] 
index 85816398bc3891b0d1ae66aeb6b839876d6ae0a2..6bd30040d467041a67f990b276d976e7f6e69a7f 100644 (file)
@@ -22,6 +22,7 @@
 [% PROCESS global/variables.none.tmpl %]
 
 [% filt_product = product FILTER html %]
+[% filt_classification = classification FILTER html %]
 [% PROCESS global/header.html.tmpl
   title = "Edit Group Controls for '$filt_product'"
 %]
@@ -29,6 +30,7 @@
 <form method="post" action="editproducts.cgi">
   <input type="hidden" name="action" value="updategroupcontrols">
   <input type="hidden" name="product" value="[% filt_product %]">
+  <input type="hidden" name="classification" value="[% filt_classification %]">
 
   <table id="form" cellspacing="0" cellpadding="4" border="1">
     <tr bgcolor="#6666ff">
index f4c68bb4d008a4f6e373427ae38bd24bf78824d1..b898afee144f2f74a82b03a58f4a5d3fa3e893d7 100644 (file)
   <table cellspacing="1" cellpadding="1" border="0">
     <tr>
       <td align="right">
+        [% IF Param('useclassification') %]
+          [% IF bug.classification_id != "1" %]
+            <b>[[% bug.classification FILTER html %]]</b>
+          [% END %]
+        [% END %]
         <b>[% terms.Bug %]#:</b>
       </td>
       <td>
index 6b48feb006196c7dd46574f0e8695d0eb38b7ec2..8cf86dd5fbf980540fbd5bf4bb61f8455e0a2038 100644 (file)
           ([% bug.alias FILTER html %])
         [% END %]
       </td>
-      [% PROCESS cell attr = { description => "Product",
-                               name => "product" } %]
+      <td>
+        <b> Product: </b>&nbsp;
+        [% IF Param("useclassification") %]
+          [[% bug.classification FILTER html %]]&nbsp;
+        [% END %]
+        [% bug.product FILTER html %]
+      </td>
+
       [% PROCESS cell attr = { description => "Version",
                                name => "version" } %]
       [% PROCESS cell attr = { description => "Platform",
index 8d25e253653a652d8c2929650a296041d850e741..c1921a9083bf6153b2a629ac92b67387fd21a34b 100644 (file)
 ],
 
 'admin/products/groupcontrol/edit.html.tmpl' => [
+  'filt_classification', 
   'filt_product', 
   'group.bugcount', 
   'group.id', 
diff --git a/template/en/default/global/choose-classification.html.tmpl b/template/en/default/global/choose-classification.html.tmpl
new file mode 100644 (file)
index 0000000..b7b8fb6
--- /dev/null
@@ -0,0 +1,65 @@
+<!-- 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 Albert Ting
+  #
+  # Contributor(s): Albert Ting <alt@sonic.net>
+  #%]
+
+[%# INTERFACE:
+  # classdesc: hash. May be empty. The hash keys are the classifications, and the values
+  # are their descriptions.
+  #%]
+
+[% IF target == "enter_bug.cgi" %]
+  [% title = "Select Classification" %]
+  [% h2 = "Please select the classification." %]
+[% END %]
+
+[% DEFAULT title = "Choose the classification" %]
+[% PROCESS global/header.html.tmpl %]
+
+<table>
+        
+[% IF Param('showallproducts') %]
+  <tr>
+    <th align="right" valign="center" height=50>
+      <a href="[% target FILTER url_quote %]?classification=__all
+            [% IF format %]&amp;format=[% format FILTER url_quote %][% END %]">
+      All</a>:
+    </th>
+
+    <td valign="center">&nbsp;Show all products</td>
+  </tr>    
+[% END %]
+
+[% FOREACH p = classdesc.keys.sort %]
+  [% IF classifications.$p.size > 0 %]
+  <tr>
+    <th align="right" valign="top">
+      <a href="[% target FILTER url_quote %]?classification=[% p FILTER url_quote %]
+            [% IF format %]&amp;format=[% format FILTER url_quote %][% END %]">
+      [% p FILTER html %]</a>:
+    </th>
+
+    [% IF classdesc.$p %]
+      <td valign="top">&nbsp;[% classdesc.$p FILTER html %]</td>
+    [% END %]
+  </tr>
+  [% END %]
+[% END %]
+
+</table>
+
+[% PROCESS global/footer.html.tmpl %]
index 7a21ca056a6825988037913234e9d6e3d69fed7e..1080e8e44cb3b78379a4b7903176e96a156a151a 100644 (file)
@@ -32,6 +32,7 @@
                    "bug_status"           => "Status",
                    "changeddate"          => "Last Changed Date",
                    "cc"                   => "CC",
+                   "classification"       => "Classification",
                    "cclist_accessible"    => "CC list accessible?",
                    "component_id"         => "Component ID",
                    "component"            => "Component",
index 7e568372c905d7e05e56d317964c041557b97f7f..8a11b2ad3d50cee76bf1234847f6fda6f8043c9b 100644 (file)
                                                   IF user.groups.tweakparams %]
         [% ' | <a href="editusers.cgi">Users</a>'     IF user.groups.editusers 
                                                   || user.can_bless %]
-        [% ' | <a href="editproducts.cgi">Products</a>' 
+        [% IF Param('useclassification') %]
+          [% IF user.groups.editclassifications %]
+            [% ' | <a href="editclassifications.cgi">Classifications</a>' %]
+          [% END %]
+          [% IF user.groups.editcomponents %]
+            [% ' | <a href="editproducts.cgi">Products</a>' %]
+          [% END %]
+        [% ELSE %]
+          [% ' | <a href="editproducts.cgi">Products</a>' 
                                                IF user.groups.editcomponents %]
+        [% END %]
         [% ' | <a href="editflagtypes.cgi">Flags</a>'
                                                IF user.groups.editcomponents %]
         [% ' | <a href="editgroups.cgi">Groups</a>' 
index a17b1275bc0fb266fa631d3a223008284b507188..7638806cf311a2fc554bdc1ddf245cf5d5c78f13 100644 (file)
     [% title = "Comment Too Long" %]
     Comments cannot be longer than 65,535 characters.
 
+  [% ELSIF error == "auth_classification_not_enabled" %]
+    [% title = "Classification Not Enabled" %]
+    Sorry, classification is not enabled.
+
+  [% ELSIF error == "auth_cant_edit_classifications" %]
+    [% title = "Access Denied" %]
+    Sorry, you aren't a member of the 'editclassifications' group, and so
+    you aren't allowed to add, modify or delete classifications.
+
+  [% ELSIF error == "classification_not_specified" %]
+    [% title = "You Must Supply A Classification Name" %]
+    You must enter a classification name.
+
+  [% ELSIF error == "classification_already_exists" %]
+    [% title = "Classification Already Exists" %]
+    A classification with the name '[% name FILTER html %]' already exists.
+
+  [% ELSIF error == "classification_doesnt_exist" %]
+    [% title = "Classification Does Not Exist" %]
+    The classification '[% name FILTER html %]' does not exist.
+
+  [% ELSIF error == "classification_not_deletable" %]
+    [% title = "Default Classification Can Not Be Deleted" %]
+    You can not delete the default classification
+
+  [% ELSIF error == "classification_has_products" %]
+     Sorry, there are products for this classification. You
+     must reassign those products to another classification before you
+     can delete this one.
+
+  [% ELSIF error == "cant_delete_default_classification" %]
+     Sorry, but you can not delete the default classification,
+     '[% name FILTER html %]'.
+
   [% ELSIF error == "auth_cant_edit_components" %]
     [% title = "Access Denied" %]
     Sorry, you aren't a member of the 'editcomponents' group, and so
     [% title = "Invalid group name" %]
     The group you specified, [% name FILTER html %], is not valid here.
 
+  [% ELSIF error == "invalid_group_name" %]
+    [% title = "Invalid group name" %]
+    The group you specified, [% name FILTER html %], is not valid here.
+
   [% ELSIF error == "invalid_maxrows" %]
     [% title = "Invalid Max Rows" %]
     The maximum number of rows, '[% maxrows FILTER html %]', must be
index f875f35411773b061660c71b872b84cb6f43940c..77308f28b1604a915fd30bcf808a54835f6f97dc 100644 (file)
   #                 Gervase Markham <gerv@gerv.net>
   #%]
 
+<script type="text/javascript" language="JavaScript">
+
+var first_load = true;         [%# is this the first time we load the page? %]
+var last_sel = new Array();    [%# caches last selection %]
+
+[% IF Param('useclassification') %]
+var useclassification = true;
+var prods = new Array();
+[% ELSE %]
+var useclassification = false;
+[% END %]
+var cpts = new Array();
+var vers = new Array();
+[% IF Param('usetargetmilestone') %]
+var tms = new Array();
+[% END %]
+
+[%# Create an array of products, indexed by the classification #%]
+
+[% nclass = 0 %]
+[% FOREACH c = classification %]
+  prods[[% nclass FILTER js %]] = [
+    [%- FOREACH item = c.products %]'[% item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+  [% nclass = nclass+1 %]
+[% END %]
+
+[%# Create three arrays of components, versions and target milestones, indexed
+  # numerically according to the product they refer to. #%]
+
+[% n = 0 %]
+[% FOREACH p = product %]
+  [% IF Param('useclassification') %]
+  prods['[% p.name FILTER js %]'] = [% n %]
+  [% END %]
+  cpts[[% n %]] = [
+    [%- FOREACH item = p.components %]'[% item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+  vers[[% n %]] = [
+    [%- FOREACH item = p.versions -%]'[%  item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+  [% IF Param('usetargetmilestone') %]
+  tms[[% n %]]  = [
+     [%- FOREACH item = p.milestones %]'[% item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+  [% END %]
+  [% n = n+1 %]
+[% END %]
+
+/*
+ * doOnSelectProduct determines which selection should get updated 
+ *
+ * - selectmode = 0  - init
+ *   selectmode = 1  - classification selected
+ *   selectmode = 2  - product selected
+ *
+ * globals:
+ *   queryform - string holding the name of the selection form
+ */
+function doOnSelectProduct(selectmode) {
+    var f = document.forms[queryform];
+    milestone = (typeof(f.target_milestone) == "undefined" ? 
+                                               null : f.target_milestone);
+    if (selectmode == 0) {
+        if (useclassification) {
+            selectClassification(f.classification, f.product, f.component, f.version, milestone);
+        } else {
+            selectProduct(f.product, f.component, f.version, milestone);
+        }
+    } else if (selectmode == 1) {
+        selectClassification(f.classification, f.product, f.component, f.version, milestone);
+    } else {
+        selectProduct(f.product, f.component, f.version, milestone);
+    }
+}
+
+</script>
+
+
 [% PROCESS global/variables.none.tmpl %]
 
 [% query_variants = [
       <input name="short_desc" size="40" accesskey="s"
              value="[% default.short_desc.0 FILTER html %]">
       <script language="JavaScript" type="text/javascript"> <!--
-          document.forms['queryform'].short_desc.focus(); 
+          document.forms[queryform].short_desc.focus(); 
       // -->
       </script>
     </td>
     </td>
   </tr>
 
-[%# *** Product Component Version Target *** %]
-
+[%# *** Classification Product Component Version Target *** %]
   <tr>
     <td colspan="4">
       <table>
         <tr>
+        [% IF Param('useclassification') %]
+          <td valign="top">
+            <table>
+              <tr valign="bottom">
+                <th align="left"><u>C</u>lassification:</th>
+              </tr>
+              <tr valign="top">
+                <td align="left">
+                  <label for="classification">
+                    <select name="classification" multiple="multiple" size="5" id="classification"
+                            onchange="doOnSelectProduct(1);">
+                    [% FOREACH cat = classification %]
+                      <option value="[% cat.name FILTER html %]"
+                        [% " selected" IF lsearch(default.classification, cat.name) != -1 %]>
+                        [% cat.name FILTER html %]
+                      </option>
+                    [% END %]
+                    </select>
+                  </label>
+                </td>
+              </tr>
+            </table>
+          </td>
+        [% END %]
           <td valign="top">
             <table>
               <tr valign="bottom">
                 <td align="left">
                   <label for="product" accesskey="p">
                     <select name="product" multiple="multiple" size="5" id="product"
-                            onchange="doOnSelectProduct();">
+                            onchange="doOnSelectProduct(2);">
                     [% FOREACH p = product %]
                       <option value="[% p.name FILTER html %]"
                         [% " selected" IF lsearch(default.product, p.name) != -1 %]>
index 89938adbe7cf13c946d214d6332dc6543f6b5266..c17d8a2e42d6d0e400cd7c93300aa4a17cec97ba 100644 (file)
 
 
 [% js_data = BLOCK %]
-function doOnSelectProduct() {
-    var f = document.forms['queryform'];
-    milestone = (typeof(f.target_milestone) == "undefined" ? 
-                                               null : f.target_milestone);
-    selectProduct(f.product, f.component, f.version, milestone);
-}
-
-var first_load = true;         [%# is this the first time we load the page? %]
-var last_sel = new Array();    [%# caches last selection %]
-
-var cpts = new Array();
-var vers = new Array();
-[% IF Param('usetargetmilestone') %]
-var tms = new Array();
-[% END %]
-
-[%# Create three arrays of components, versions and target milestones, indexed
-  # numerically according to the product they refer to. #%]
-
-[% n = 0 %]
-[% FOREACH p = product %]
-  cpts[[% n %]] = [
-    [%- FOREACH item = p.components %]'[% item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
-  vers[[% n %]] = [
-    [%- FOREACH item = p.versions -%]'[%  item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
-  [% IF Param('usetargetmilestone') %]
-  tms[[% n %]]  = [
-     [%- FOREACH item = p.milestones %]'[% item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
-  [% END %]
-  [% n = n+1 %]
-[% END %]
-
+var queryform = "queryform"
 [% END %]
 
 [% PROCESS global/header.html.tmpl
   title = "Search for $terms.bugs"
   h1 = ""
-  onload = "doOnSelectProduct(); initHelp();"
+  onload = "doOnSelectProduct(0); initHelp();"
   javascript = js_data
   javascript_urls = [ "js/productform.js" ]
   style = "td.selected_tab {
index 1d3544e289f4c9a7b9a7796e7553497814d14ac1..0d4b53e267f981a8c63825586f69900237c45c3d 100644 (file)
 { id => "short_desc", 
   html => "The $terms.bug summary is a short sentence which succinctly  
            describes <br> what the $terms.bug is about." },   
+{ id => "classification", 
+  html => "$terms.Bugs are categorised into Classifications, Products and Components. classifications is the<br>
+           top-level categorisation." },   
 { id => "product", 
-  html => "$terms.Bugs are categorised into Products and Components. Product is 
+  html => Param('useclassification') ?
+       "$terms.Bugs are categorised into Products and Components. Select a Classification to narrow down this list" :
+       "$terms.Bugs are categorised into Products and Components. Product is 
            the<br>top-level categorisation." },   
 { id => "component", 
   html => "Components are second-level categories; each belongs to a<br> 
index 4e1f0bae301e7577ec907c2d290821bc9fae0c15..276fe75600344230ce621ce1b373e3c25d5ee022 100644 (file)
 
 [% PROCESS global/variables.none.tmpl %]
 
+[% js_data = BLOCK %]
+var queryform = "reportform"
+[% END %]
+
 [% PROCESS global/header.html.tmpl
   title = "Generate Graphical Report"
-  onload = "selectProduct(document.forms['reportform']);chartTypeChanged()"
+  onload = "doOnSelectProduct(0); chartTypeChanged()"
+  javascript = js_data
+  javascript_urls = [ "js/productform.js" ]
 %]
 
 [% PROCESS "search/search-report-select.html.tmpl" %]
index 9afe41c4baf274ec8334d1990c3e51ac98e05b82..36288425c2cb62504db072b3dcce5170a67da487 100644 (file)
@@ -27,7 +27,7 @@
 [% PROCESS "global/field-descs.none.tmpl" %]
 
 [% BLOCK select %]
-  [% rep_fields = ["product", "component", "version", "rep_platform",  
+  [% rep_fields = ["classification", "product", "component", "version", "rep_platform",  
                    "op_sys", "bug_status", "resolution", "bug_severity", 
                    "priority", "target_milestone", "assigned_to",
                    "reporter", "qa_contact", "votes" ] %]
index 8718da5fd3d796b18e9f873573eb925d26947c65..1894fa795b70bc34ad76afe39bfdbb365672b6c9 100644 (file)
 
 [% PROCESS global/variables.none.tmpl %]
 
+[% js_data = BLOCK %]
+var queryform = "reportform"
+[% END %]
+
 [% PROCESS global/header.html.tmpl
   title = "Generate Tabular Report"
-  onload = "selectProduct(document.forms['reportform']);"
+  onload = "doOnSelectProduct(0)"
+  javascript = js_data
+  javascript_urls = [ "js/productform.js" ]
 %]
 
 [% PROCESS "search/search-report-select.html.tmpl" %]
index b503075523a55682f41d8ffa33b319fef517c0c5..30b3e7b9ea3b71055a8b4b664e44ab82c3b4f262 100644 (file)
@@ -75,10 +75,24 @@ for "crash secure SSL flash".
     <td>
       <select name="product" id="product">
         <option value="">All</option>
-        [% FOREACH p = product %]
-          <option value="[% p.name FILTER html %]"
-            [% " selected" IF lsearch(default.product, p.name) != -1 %]>
-            [% p.name FILTER html %]</option>
+        [% IF Param('useclassification') %]
+          [% FOREACH c = classification %]
+            <optgroup label="[% c.name FILTER html %]">
+            [% FOREACH p = c.products %]
+              <option value="[% p FILTER html %]"
+                [% " selected" IF lsearch(default.product, p) != -1 %]>
+                [% p FILTER html %]
+              </option>
+            [% END %]
+            </optgroup>
+          [% END %]
+        [% ELSE %]
+          [% FOREACH p = product %]
+            <option value="[% p.name FILTER html %]"
+              [% " selected" IF lsearch(default.product, p.name) != -1 %]>
+              [% p.name FILTER html %]
+            </option>
+          [% END %]
         [% END %]
       </select>
     </td>