diff options
author | bugreport%peshkin.net <> | 2004-08-21 06:49:17 +0200 |
---|---|---|
committer | bugreport%peshkin.net <> | 2004-08-21 06:49:17 +0200 |
commit | 88d26275229b5f52f435130496169766313c87b7 (patch) | |
tree | c71532045500b6e2335b1e5e2512d10b8846551b | |
parent | 2f9f28d0bfb1f321d1548844a41aaf2d51789695 (diff) | |
download | bugzilla-88d26275229b5f52f435130496169766313c87b7.tar.gz bugzilla-88d26275229b5f52f435130496169766313c87b7.tar.xz |
Bug 224208 Add a higher level of categorization (.ie departments, locations, etc.)
patch by Albert Ting
r=joel, glob
a=myk
38 files changed, 1607 insertions, 103 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 8863f5432..d5aa5fd17 100755 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -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", diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index 91785963f..23bb34eae 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -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; diff --git a/buglist.cgi b/buglist.cgi index eaca25612..3c575d2b8 100755 --- a/buglist.cgi +++ b/buglist.cgi @@ -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"); diff --git a/bugzilla.dtd b/bugzilla.dtd index aecf9920d..82ccfff18 100644 --- a/bugzilla.dtd +++ b/bugzilla.dtd @@ -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)> diff --git a/checksetup.pl b/checksetup.pl index 703358cec..807fa9016 100755 --- a/checksetup.pl +++ b/checksetup.pl @@ -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... diff --git a/colchange.cgi b/colchange.cgi index 8d3ee49da..03d5388bf 100755 --- a/colchange.cgi +++ b/colchange.cgi @@ -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"); diff --git a/defparams.pl b/defparams.pl index 8260be978..c0e608a4d 100644 --- a/defparams.pl +++ b/defparams.pl @@ -321,6 +321,23 @@ sub find_languages { }, { + 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 ' . 'product in the database, and use it for querying bugs.', diff --git a/editclassifications.cgi b/editclassifications.cgi new file mode 100755 index 000000000..c1186f792 --- /dev/null +++ b/editclassifications.cgi @@ -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); diff --git a/editproducts.cgi b/editproducts.cgi index bd71bdd6d..74a62166e 100755 --- a/editproducts.cgi +++ b/editproducts.cgi @@ -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, diff --git a/enter_bug.cgi b/enter_bug.cgi index cbe2929aa..a0291fae5 100755 --- a/enter_bug.cgi +++ b/enter_bug.cgi @@ -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, diff --git a/globals.pl b/globals.pl index 9872dff70..829417e44 100644 --- a/globals.pl +++ b/globals.pl @@ -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; +} diff --git a/js/productform.js b/js/productform.js index 0be0d4971..7cf07d732 100644 --- a/js/productform.js +++ b/js/productform.js @@ -21,6 +21,72 @@ /* 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; diff --git a/long_list.cgi b/long_list.cgi index 757d00239..515f4c226 100755 --- a/long_list.cgi +++ b/long_list.cgi @@ -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", @@ -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'; diff --git a/report.cgi b/report.cgi index 66060723d..fac2f0e75 100755 --- a/report.cgi +++ b/report.cgi @@ -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 index 000000000..d6a7c3880 --- /dev/null +++ b/template/en/default/admin/classifications/add.html.tmpl @@ -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 index 000000000..008971562 --- /dev/null +++ b/template/en/default/admin/classifications/del.html.tmpl @@ -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 index 000000000..b2ec26cbb --- /dev/null +++ b/template/en/default/admin/classifications/delete.html.tmpl @@ -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 index 000000000..ebc16e82e --- /dev/null +++ b/template/en/default/admin/classifications/edit.html.tmpl @@ -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 index 000000000..c8046b898 --- /dev/null +++ b/template/en/default/admin/classifications/new.html.tmpl @@ -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&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 index 000000000..d860f67cf --- /dev/null +++ b/template/en/default/admin/classifications/reclassify.html.tmpl @@ -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 >> " name="add_products"><br><br> + <input type=submit value="<< 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 index 000000000..5908375d2 --- /dev/null +++ b/template/en/default/admin/classifications/select.html.tmpl @@ -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&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&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"> </td> + [% ELSE %] + <td valign="top"><a href="editclassifications.cgi?action=del&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 index 000000000..3ad7bbc69 --- /dev/null +++ b/template/en/default/admin/classifications/update.html.tmpl @@ -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 %] diff --git a/template/en/default/admin/products/groupcontrol/edit.html.tmpl b/template/en/default/admin/products/groupcontrol/edit.html.tmpl index 85816398b..6bd30040d 100644 --- a/template/en/default/admin/products/groupcontrol/edit.html.tmpl +++ b/template/en/default/admin/products/groupcontrol/edit.html.tmpl @@ -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"> diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl index f4c68bb4d..b898afee1 100644 --- a/template/en/default/bug/edit.html.tmpl +++ b/template/en/default/bug/edit.html.tmpl @@ -119,6 +119,11 @@ <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> diff --git a/template/en/default/bug/show-multiple.html.tmpl b/template/en/default/bug/show-multiple.html.tmpl index 6b48feb00..8cf86dd5f 100644 --- a/template/en/default/bug/show-multiple.html.tmpl +++ b/template/en/default/bug/show-multiple.html.tmpl @@ -62,8 +62,14 @@ ([% bug.alias FILTER html %]) [% END %] </td> - [% PROCESS cell attr = { description => "Product", - name => "product" } %] + <td> + <b> Product: </b> + [% IF Param("useclassification") %] + [[% bug.classification FILTER html %]] + [% END %] + [% bug.product FILTER html %] + </td> + [% PROCESS cell attr = { description => "Version", name => "version" } %] [% PROCESS cell attr = { description => "Platform", diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl index 8d25e2536..c1921a908 100644 --- a/template/en/default/filterexceptions.pl +++ b/template/en/default/filterexceptions.pl @@ -500,6 +500,7 @@ ], '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 index 000000000..b7b8fb66e --- /dev/null +++ b/template/en/default/global/choose-classification.html.tmpl @@ -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 %]&format=[% format FILTER url_quote %][% END %]"> + All</a>: + </th> + + <td valign="center"> 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 %]&format=[% format FILTER url_quote %][% END %]"> + [% p FILTER html %]</a>: + </th> + + [% IF classdesc.$p %] + <td valign="top"> [% classdesc.$p FILTER html %]</td> + [% END %] + </tr> + [% END %] +[% END %] + +</table> + +[% PROCESS global/footer.html.tmpl %] diff --git a/template/en/default/global/field-descs.none.tmpl b/template/en/default/global/field-descs.none.tmpl index 7a21ca056..1080e8e44 100644 --- a/template/en/default/global/field-descs.none.tmpl +++ b/template/en/default/global/field-descs.none.tmpl @@ -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", diff --git a/template/en/default/global/useful-links.html.tmpl b/template/en/default/global/useful-links.html.tmpl index 7e568372c..8a11b2ad3 100644 --- a/template/en/default/global/useful-links.html.tmpl +++ b/template/en/default/global/useful-links.html.tmpl @@ -77,8 +77,17 @@ 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>' diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl index a17b1275b..7638806cf 100644 --- a/template/en/default/global/user-error.html.tmpl +++ b/template/en/default/global/user-error.html.tmpl @@ -142,6 +142,40 @@ [% 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 @@ -463,6 +497,10 @@ [% 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 diff --git a/template/en/default/search/form.html.tmpl b/template/en/default/search/form.html.tmpl index f875f3541..77308f28b 100644 --- a/template/en/default/search/form.html.tmpl +++ b/template/en/default/search/form.html.tmpl @@ -21,6 +21,81 @@ # 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 = [ @@ -56,7 +131,7 @@ <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> @@ -67,12 +142,35 @@ </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"> @@ -83,7 +181,7 @@ <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 %]> diff --git a/template/en/default/search/search-advanced.html.tmpl b/template/en/default/search/search-advanced.html.tmpl index 89938adbe..c17d8a2e4 100644 --- a/template/en/default/search/search-advanced.html.tmpl +++ b/template/en/default/search/search-advanced.html.tmpl @@ -32,44 +32,13 @@ [% 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 { diff --git a/template/en/default/search/search-help.html.tmpl b/template/en/default/search/search-help.html.tmpl index 1d3544e28..0d4b53e26 100644 --- a/template/en/default/search/search-help.html.tmpl +++ b/template/en/default/search/search-help.html.tmpl @@ -25,8 +25,13 @@ { 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> diff --git a/template/en/default/search/search-report-graph.html.tmpl b/template/en/default/search/search-report-graph.html.tmpl index 4e1f0bae3..276fe7560 100644 --- a/template/en/default/search/search-report-graph.html.tmpl +++ b/template/en/default/search/search-report-graph.html.tmpl @@ -26,9 +26,15 @@ [% 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" %] diff --git a/template/en/default/search/search-report-select.html.tmpl b/template/en/default/search/search-report-select.html.tmpl index 9afe41c4b..36288425c 100644 --- a/template/en/default/search/search-report-select.html.tmpl +++ b/template/en/default/search/search-report-select.html.tmpl @@ -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" ] %] diff --git a/template/en/default/search/search-report-table.html.tmpl b/template/en/default/search/search-report-table.html.tmpl index 8718da5fd..1894fa795 100644 --- a/template/en/default/search/search-report-table.html.tmpl +++ b/template/en/default/search/search-report-table.html.tmpl @@ -26,9 +26,15 @@ [% 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" %] diff --git a/template/en/default/search/search-specific.html.tmpl b/template/en/default/search/search-specific.html.tmpl index b50307552..30b3e7b9e 100644 --- a/template/en/default/search/search-specific.html.tmpl +++ b/template/en/default/search/search-specific.html.tmpl @@ -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> |