From 199d6ed76f022232c3799036c75661604a6d70d4 Mon Sep 17 00:00:00 2001 From: "myk%mozilla.org" <> Date: Fri, 6 Jan 2006 22:22:55 +0000 Subject: Bug 287325: an initial implementation of custom fields, including the ability to add text custom fields via the command-line script customfield.pl, search them via the boolean charts, display and edit them on the show bug page, and see changes to them in bug activity; r=mkanat, glob --- Bugzilla.pm | 12 ++ Bugzilla/Bug.pm | 13 +- Bugzilla/Constants.pm | 13 ++ Bugzilla/DB/Schema.pm | 5 + Bugzilla/Field.pm | 342 +++++++++++++++++++++++++++----- checksetup.pl | 9 +- customfield.pl | 88 ++++++++ editcomponents.cgi | 1 + editmilestones.cgi | 1 + editproducts.cgi | 1 + process_bug.cgi | 12 ++ show_activity.cgi | 1 + showdependencytree.cgi | 1 + summarize_time.cgi | 1 + template/en/default/bug/edit.html.tmpl | 9 + template/en/default/bug/field.html.tmpl | 36 ++++ 16 files changed, 497 insertions(+), 48 deletions(-) create mode 100755 customfield.pl create mode 100644 template/en/default/bug/field.html.tmpl diff --git a/Bugzilla.pm b/Bugzilla.pm index 86d6e6e70..5267f3dbd 100644 --- a/Bugzilla.pm +++ b/Bugzilla.pm @@ -35,6 +35,7 @@ use Bugzilla::Template; use Bugzilla::User; use Bugzilla::Error; use Bugzilla::Util; +use Bugzilla::Field; use File::Basename; @@ -276,6 +277,17 @@ sub switch_to_main_db { return $class->dbh; } +sub get_fields { + my $class = shift; + my $criteria = shift; + return Bugzilla::Field::match($criteria); +} + +sub custom_field_names { + # Get a list of custom fields and convert it into a list of their names. + return map($_->{name}, Bugzilla::Field::match({ custom=>1, obsolete=>0 })); +} + # Private methods # Per process cleanup diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 61798f7cb..cfa5f49f6 100755 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -90,6 +90,8 @@ sub fields { push @fields, qw(estimated_time remaining_time actual_time deadline); } + push(@fields, Bugzilla->custom_field_names); + return @fields; } @@ -162,6 +164,11 @@ sub initBug { $self->{'who'} = new Bugzilla::User($user_id); + my $custom_fields = ""; + if (length(Bugzilla->custom_field_names) > 0) { + $custom_fields = ", " . join(", ", Bugzilla->custom_field_names); + } + my $query = " SELECT bugs.bug_id, alias, products.classification_id, classifications.name, @@ -175,7 +182,8 @@ sub initBug { delta_ts, COALESCE(SUM(votes.vote_count), 0), reporter_accessible, cclist_accessible, estimated_time, remaining_time, " . - $dbh->sql_date_format('deadline', '%Y-%m-%d') . " + $dbh->sql_date_format('deadline', '%Y-%m-%d') . + $custom_fields . " FROM bugs LEFT JOIN votes ON bugs.bug_id = votes.bug_id @@ -212,7 +220,8 @@ sub initBug { "target_milestone", "qa_contact_id", "status_whiteboard", "creation_ts", "delta_ts", "votes", "reporter_accessible", "cclist_accessible", - "estimated_time", "remaining_time", "deadline") + "estimated_time", "remaining_time", "deadline", + Bugzilla->custom_field_names) { $fields{$field} = shift @row; if (defined $fields{$field}) { diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 09717486e..6ff7d8fa9 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -91,6 +91,9 @@ use base qw(Exporter); ADMIN_GROUP_NAME SENDMAIL_EXE + + FIELD_TYPE_UNKNOWN + FIELD_TYPE_FREETEXT ); @Bugzilla::Constants::EXPORT_OK = qw(contenttypes); @@ -243,4 +246,14 @@ use constant ADMIN_GROUP_NAME => 'admin'; # Path to sendmail.exe (Windows only) use constant SENDMAIL_EXE => '/usr/lib/sendmail.exe'; +# Field types. Match values in fielddefs.type column. These are purposely +# not named after database column types, since Bugzilla fields comprise not +# only storage but also logic. For example, we might add a "user" field type +# whose values are stored in an integer column in the database but for which +# we do more than we would do for a standard integer type (f.e. we might +# display a user picker). + +use constant FIELD_TYPE_UNKNOWN => 0; +use constant FIELD_TYPE_FREETEXT => 1; + 1; diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm index 63b19578d..3caeba707 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -36,6 +36,7 @@ package Bugzilla::DB::Schema; use strict; use Bugzilla::Error; use Bugzilla::Util; +use Bugzilla::Constants; use Safe; # Historical, needed for SCHEMA_VERSION = '1.00' @@ -453,6 +454,10 @@ use constant ABSTRACT_SCHEMA => { fieldid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, name => {TYPE => 'varchar(64)', NOTNULL => 1}, + type => {TYPE => 'INT2', NOTNULL => 1, + DEFAULT => FIELD_TYPE_UNKNOWN}, + custom => {TYPE => 'BOOLEAN', NOTNULL => 1, + DEFAULT => 'FALSE'}, description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, mailhead => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm index 09c4731ac..8585ff760 100644 --- a/Bugzilla/Field.pm +++ b/Bugzilla/Field.pm @@ -14,6 +14,57 @@ # # Contributor(s): Dan Mosedale # Frédéric Buclin +# Myk Melez + +=head1 NAME + +Bugzilla::Field - a particular piece of information about bugs + and useful routines for form field manipulation + +=head1 SYNOPSIS + + use Bugzilla; + use Data::Dumper; + + # Display information about all fields. + print Dumper(Bugzilla->get_fields()); + + # Display information about non-obsolete custom fields. + print Dumper(Bugzilla->get_fields({ obsolete => 1, custom => 1 })); + + # Display a list of the names of non-obsolete custom fields. + print Bugzilla->custom_field_names; + + use Bugzilla::Field; + + # Display information about non-obsolete custom fields. + # Bugzilla->get_fields() is a wrapper around Bugzilla::Field::match(), + # so both methods take the same arguments. + print Dumper(Bugzilla::Field::match({ obsolete => 1, custom => 1 })); + + # Create a custom field. + my $field = Bugzilla::Field::create("hilarity", "Hilarity"); + print "$field->{description} is a custom field\n"; + + # Instantiate a Field object for an existing field. + my $field = new Bugzilla::Field('qacontact_accessible'); + if ($field->{obsolete}) { + print "$field->{description} is obsolete\n"; + } + + # Validation Routines + check_form_field($cgi, $fieldname, \@legal_values); + check_form_field_defined($cgi, $fieldname); + $fieldid = get_field_id($fieldname); + +=head1 DESCRIPTION + +Field.pm defines field objects, which represent the particular pieces +of information that Bugzilla stores about bugs. + +This package also provides functions for dealing with CGI form fields. + +=cut package Bugzilla::Field; @@ -24,73 +75,214 @@ use base qw(Exporter); get_field_id); use Bugzilla::Util; +use Bugzilla::Constants; use Bugzilla::Error; +use constant DB_COLUMNS => ( + 'fieldid AS id', + 'name', + 'description', + 'type', + 'custom', + 'obsolete' +); + +our $columns = join(", ", DB_COLUMNS); + +sub new { + my $invocant = shift; + my $name = shift; + my $self = shift || Bugzilla->dbh->selectrow_hashref( + "SELECT $columns FROM fielddefs WHERE name = ?", + undef, + $name + ); + bless($self, $invocant); + return $self; +} -sub check_form_field { - my ($cgi, $fieldname, $legalsRef) = @_; - my $dbh = Bugzilla->dbh; +=pod - if (!defined $cgi->param($fieldname) - || trim($cgi->param($fieldname)) eq "" - || (defined($legalsRef) - && lsearch($legalsRef, $cgi->param($fieldname)) < 0)) - { - trick_taint($fieldname); - my ($result) = $dbh->selectrow_array("SELECT description FROM fielddefs - WHERE name = ?", undef, $fieldname); - - my $field = $result || $fieldname; - ThrowCodeError("illegal_field", { field => $field }); - } -} +=head2 Instance Properties -sub check_form_field_defined { - my ($cgi, $fieldname) = @_; +=over - if (!defined $cgi->param($fieldname)) { - ThrowCodeError("undefined_field", { field => $fieldname }); - } -} +=item C + +the unique identifier for the field; + +=back + +=cut + +sub id { return $_[0]->{id} } + +=over + +=item C + +the name of the field in the database; begins with "cf_" if field +is a custom field, but test the value of the boolean "custom" property +to determine if a given field is a custom field; + +=back + +=cut + +sub name { return $_[0]->{name} } + +=over + +=item C + +a short string describing the field; displayed to Bugzilla users +in several places within Bugzilla's UI, f.e. as the form field label +on the "show bug" page; + +=back + +=cut + +sub description { return $_[0]->{description} } + +=over + +=item C + +an integer specifying the kind of field this is; values correspond to +the FIELD_TYPE_* constants in Constants.pm + +=back + +=cut + +sub type { return $_[0]->{type} } + +=over + +=item C + +a boolean specifying whether or not the field is a custom field; +if true, field name should start "cf_", but use this property to determine +which fields are custom fields; + +=back + +=cut + +sub custom { return $_[0]->{custom} } + +=over + +=item C + +a boolean specifying whether or not the field is obsolete; + +=back + +=cut + +sub obsolete { return $_[0]->{obsolete} } + + +=pod + +=head2 Class Methods + +=over + +=item C + +Description: creates a new custom field. + +Params: C<$name> - string - the name of the field; + C<$desc> - string - the field label to display in the UI. + +Returns: a field object. + +=back + +=cut + +sub create { + my ($name, $desc, $custom) = @_; + + # Convert the $custom argument into a DB-compatible value. + $custom = $custom ? 1 : 0; -sub get_field_id { - my ($name) = @_; my $dbh = Bugzilla->dbh; - trick_taint($name); - my $id = $dbh->selectrow_array('SELECT fieldid FROM fielddefs - WHERE name = ?', undef, $name); + # Some day we'll allow invocants to specify the sort key. + my ($sortkey) = + $dbh->selectrow_array("SELECT MAX(sortkey) + 1 FROM fielddefs"); - ThrowCodeError('invalid_field_name', {field => $name}) unless $id; - return $id + # Some day we'll require invocants to specify the field type. + my $type = FIELD_TYPE_FREETEXT; + + # Create the database column that stores the data for this field. + $dbh->bz_add_column("bugs", $name, { TYPE => 'varchar(255)' }); + + # Add the field to the list of fields at this Bugzilla installation. + my $sth = $dbh->prepare( + "INSERT INTO fielddefs (name, description, sortkey, type, + custom, mailhead) + VALUES (?, ?, ?, ?, ?, 1)" + ); + $sth->execute($name, $desc, $sortkey, $type, $custom); + + return new Bugzilla::Field($name); } -1; -__END__ +=pod -=head1 NAME +=over -Bugzilla::Field - Useful routines for fields manipulation +=item C -=head1 SYNOPSIS +Description: returns a list of fields that match the specified criteria. - use Bugzilla::Field; +Params: C<$criteria> - hash reference - the criteria to match against. + Hash keys represent field properties; hash values represent + their values. All criteria are optional. Valid criteria are + "custom" and "obsolete", and both take boolean values. - # Validation Routines - check_form_field($cgi, $fieldname, \@legal_values); - check_form_field_defined($cgi, $fieldname); - $fieldid = get_field_id($fieldname); + Note: Bugzilla->get_fields() and Bugzilla->custom_field_names + wrap this method for most callers. -=head1 DESCRIPTION +Returns: a list of field objects. -This package provides functions for dealing with CGI form fields. +=back -=head1 FUNCTIONS +=cut -This package provides several types of routines: +sub match { + my ($criteria) = @_; + + my @terms; + if (defined $criteria->{name}) { + push(@terms, "name=" . Bugzilla->dbh->quote($criteria->{name})); + } + if (defined $criteria->{custom}) { + push(@terms, "custom=" . ($criteria->{custom} ? "1" : "0")); + } + if (defined $criteria->{obsolete}) { + push(@terms, "obsolete=" . ($criteria->{obsolete} ? "1" : "0")); + } + my $where = (scalar(@terms) > 0) ? "WHERE " . join(" AND ", @terms) : ""; + + my $records = Bugzilla->dbh->selectall_arrayref( + "SELECT $columns FROM fielddefs $where ORDER BY sortkey", + { Slice => {}} + ); + # Generate a array of field objects from the array of field records. + my @fields = map( new Bugzilla::Field(undef, $_), @$records ); + return @fields; +} + +=pod -=head2 Validation +=head2 Data Validation =over @@ -108,6 +300,32 @@ Params: $cgi - a CGI object Returns: nothing +=back + +=cut + +sub check_form_field { + my ($cgi, $fieldname, $legalsRef) = @_; + my $dbh = Bugzilla->dbh; + + if (!defined $cgi->param($fieldname) + || trim($cgi->param($fieldname)) eq "" + || (defined($legalsRef) + && lsearch($legalsRef, $cgi->param($fieldname)) < 0)) + { + trick_taint($fieldname); + my ($result) = $dbh->selectrow_array("SELECT description FROM fielddefs + WHERE name = ?", undef, $fieldname); + + my $field = $result || $fieldname; + ThrowCodeError("illegal_field", { field => $field }); + } +} + +=pod + +=over + =item C Description: Makes sure the field $fieldname is defined and its value @@ -118,14 +336,48 @@ Params: $cgi - a CGI object Returns: nothing +=back + +=cut + +sub check_form_field_defined { + my ($cgi, $fieldname) = @_; + + if (!defined $cgi->param($fieldname)) { + ThrowCodeError("undefined_field", { field => $fieldname }); + } +} + +=pod + +=over + =item C Description: Returns the ID of the specified field name and throws an error if this field does not exist. -Params: $fieldname - a field name +Params: $name - a field name Returns: the corresponding field ID or an error if the field name does not exist. =back + +=cut + +sub get_field_id { + my ($name) = @_; + my $dbh = Bugzilla->dbh; + + trick_taint($name); + my $id = $dbh->selectrow_array('SELECT fieldid FROM fielddefs + WHERE name = ?', undef, $name); + + ThrowCodeError('invalid_field_name', {field => $name}) unless $id; + return $id +} + +1; + +__END__ diff --git a/checksetup.pl b/checksetup.pl index e8528aee1..fdb678e21 100755 --- a/checksetup.pl +++ b/checksetup.pl @@ -1305,7 +1305,7 @@ unless ($switch{'no_templates'}) { # These are the files which need to be marked executable my @executable_files = ('whineatnews.pl', 'collectstats.pl', 'checksetup.pl', 'importxml.pl', 'runtests.pl', 'testserver.pl', - 'whine.pl'); + 'whine.pl', 'customfield.pl'); # tell me if a file is executable. All CGI files and those in @executable_files # are executable @@ -4240,6 +4240,13 @@ $dbh->bz_alter_column('logincookies', 'cookie', {TYPE => 'varchar(16)', PRIMARYKEY => 1, NOTNULL => 1}); +# 2005-08-10 Myk Melez bug 287325 +# Record each field's type and whether or not it's a custom field in fielddefs. +$dbh->bz_add_column('fielddefs', 'type', + { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 }); +$dbh->bz_add_column('fielddefs', 'custom', + { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }); + # If you had to change the --TABLE-- definition in any way, then add your # differential change code *** A B O V E *** this comment. # diff --git a/customfield.pl b/customfield.pl new file mode 100755 index 000000000..106c60582 --- /dev/null +++ b/customfield.pl @@ -0,0 +1,88 @@ +#!/usr/bin/perl -wT +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Myk Melez + +################################################################################ +# Script Initialization +################################################################################ + +use strict; + +use lib "."; +require "globals.pl"; + +use Bugzilla::Field; +use Getopt::Long; + +my ($name, $desc); +my $result = GetOptions("name=s" => \$name, + "description|desc=s" => \$desc); + +if (!$name or !$desc) { + my $command = + $^O =~ /MSWin32/i ? "perl -T customfield.pl" : "./customfield.pl"; + print < --desc="" + + is the name of the custom field in the database. + The string "cf_" will be prepended to this name to distinguish + the field from standard fields. This name must conform to the + naming rules for the database server you use. + + is a short string describing the field. It will + be displayed to Bugzilla users in several parts of Bugzilla's UI, + for example as the label for the field on the "show bug" page. + +Warning: + + Custom fields can make Bugzilla less usable. See this URL + for alternatives to custom fields: + + http://www.gerv.net/hacking/custom-fields.html + + You should try to implement applicable alternatives before using + this script to add a custom field. +END + + exit; +} + +# Prepend cf_ to the custom field name to distinguish it from standard fields. +$name =~ /^cf_/ + or $name = "cf_" . $name; + +# Exit gracefully if there is already a field with the given name. +if (scalar(Bugzilla::Field::match({ name=>$name })) > 0) { + print "There is already a field named $name. Please choose " . + "a different name.\n"; + exit; +} + + +# Create the field. +print "Creating custom field $name ...\n"; +my $field = Bugzilla::Field::create($name, $desc, 1); +print "Custom field $name created.\n"; diff --git a/editcomponents.cgi b/editcomponents.cgi index c65fd3167..3cbd71a9c 100755 --- a/editcomponents.cgi +++ b/editcomponents.cgi @@ -31,6 +31,7 @@ use lib "."; require "globals.pl"; +use Bugzilla; use Bugzilla::Constants; use Bugzilla::Config qw(:DEFAULT $datadir); use Bugzilla::Series; diff --git a/editmilestones.cgi b/editmilestones.cgi index c87828526..bb80164ab 100755 --- a/editmilestones.cgi +++ b/editmilestones.cgi @@ -21,6 +21,7 @@ use lib "."; require "globals.pl"; +use Bugzilla; use Bugzilla::Constants; use Bugzilla::Config qw(:DEFAULT $datadir); use Bugzilla::Product; diff --git a/editproducts.cgi b/editproducts.cgi index 2b7c5dc5d..d9ebcedd9 100755 --- a/editproducts.cgi +++ b/editproducts.cgi @@ -33,6 +33,7 @@ use strict; use lib "."; +use Bugzilla; use Bugzilla::Constants; require "globals.pl"; use Bugzilla::Bug; diff --git a/process_bug.cgi b/process_bug.cgi index 77496f2a3..fcd6408d3 100755 --- a/process_bug.cgi +++ b/process_bug.cgi @@ -855,6 +855,18 @@ foreach my $field ("rep_platform", "priority", "bug_severity", } } +# Add custom fields data to the query that will update the database. +foreach my $field (Bugzilla->custom_field_names) { + if (defined $cgi->param($field) + && (!$cgi->param('dontchange') + || $cgi->param($field) ne $cgi->param('dontchange'))) + { + DoComma(); + $::query .= "$field = " . SqlQuote(trim($cgi->param($field))); + } +} + + my $prod_id; my $prod_changed; my @newprod_ids; diff --git a/show_activity.cgi b/show_activity.cgi index 9b7273a4c..e44bc7054 100755 --- a/show_activity.cgi +++ b/show_activity.cgi @@ -28,6 +28,7 @@ use lib qw(.); require "globals.pl"; +use Bugzilla; use Bugzilla::Bug; my $cgi = Bugzilla->cgi; diff --git a/showdependencytree.cgi b/showdependencytree.cgi index d9d71b0ab..03abb2729 100755 --- a/showdependencytree.cgi +++ b/showdependencytree.cgi @@ -27,6 +27,7 @@ use strict; use lib qw(.); require "globals.pl"; +use Bugzilla; use Bugzilla::User; use Bugzilla::Bug; diff --git a/summarize_time.cgi b/summarize_time.cgi index 44c4f8d50..0827077e8 100755 --- a/summarize_time.cgi +++ b/summarize_time.cgi @@ -23,6 +23,7 @@ use lib qw(.); use Date::Parse; # strptime use Date::Format; # strftime +use Bugzilla; use Bugzilla::Bug; # EmitDependList use Bugzilla::Util; # trim use Bugzilla::Constants; # LOGIN_* diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl index 2252528ac..9768dd3b4 100644 --- a/template/en/default/bug/edit.html.tmpl +++ b/template/en/default/bug/edit.html.tmpl @@ -497,6 +497,15 @@ [% END %] +[%# *** Custom Fields *** %] + +[% USE Bugzilla %] + + [% FOREACH field = Bugzilla.get_fields({ obsolete => 0, custom => 1 }) %] + [% PROCESS bug/field.html.tmpl value=bug.${field.name} %] + [% END %] +
+ [%# *** Attachments *** %] [% PROCESS attachment/list.html.tmpl diff --git a/template/en/default/bug/field.html.tmpl b/template/en/default/bug/field.html.tmpl new file mode 100644 index 000000000..cbde9cf27 --- /dev/null +++ b/template/en/default/bug/field.html.tmpl @@ -0,0 +1,36 @@ +[%# 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): Myk Melez + #%] + + + [% SWITCH field.type %] + [% CASE constants.FIELD_TYPE_FREETEXT %] + + + + + + + [% END %] + -- cgit v1.2.3-24-g4f1b