From da9ac9431cc959eedef78a5118ac3b4c6fbf7d03 Mon Sep 17 00:00:00 2001 From: "lpsolit%gmail.com" <> Date: Tue, 21 Feb 2006 21:08:18 +0000 Subject: Bug 287325: Ability to add custom plain-text fields to a Bug - Patch by Myk Melez r=mkanat a=justdave --- Bugzilla/Bug.pm | 116 +++++++++++------ Bugzilla/Constants.pm | 13 ++ Bugzilla/DB/Schema.pm | 5 + Bugzilla/Field.pm | 342 +++++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 392 insertions(+), 84 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index cab54d7da..22f62f186 100755 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -69,41 +69,6 @@ use constant MAX_COMMENT_LENGTH => 65535; ##################################################################### -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 - priority bug_severity target_milestone - dependson blocked votes everconfirmed - reporter assigned_to cc - ); - - if (Param('useqacontact')) { - push @fields, "qa_contact"; - } - - if (Param('timetrackinggroup')) { - push @fields, qw(estimated_time remaining_time actual_time deadline); - } - - return @fields; -} - -my %ok_field; -foreach my $key (qw(error groups - longdescs milestoneurl attachments - isopened isunconfirmed - flag_types num_attachment_flag_types - show_attachment_flags use_keywords any_flags_requesteeble - ), - fields()) { - $ok_field{$key}++; -} - # create a new empty bug # sub new { @@ -162,6 +127,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 +145,8 @@ sub initBug { delta_ts, COALESCE(SUM(votes.vote_count), 0), everconfirmed, 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 +183,8 @@ sub initBug { "target_milestone", "qa_contact_id", "status_whiteboard", "creation_ts", "delta_ts", "votes", "everconfirmed", "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}) { @@ -290,8 +262,41 @@ sub remove_from_db { return $self; } + ##################################################################### -# Accessors +# Class Accessors +##################################################################### + +sub fields { + my $class = shift; + + return ( + # Standard Fields + # Keep this ordering in sync with bugzilla.dtd. + 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 + priority bug_severity target_milestone + dependson blocked votes + reporter assigned_to cc), + + # Conditional Fields + Param('useqacontact') ? "qa_contact" : (), + Param('timetrackinggroup') ? qw(estimated_time remaining_time + actual_time deadline) + : (), + + # Custom Fields + Bugzilla->custom_field_names + ); +} + + +##################################################################### +# Instance Accessors ##################################################################### # These subs are in alphabetical order, as much as possible. @@ -1299,13 +1304,46 @@ sub ValidateDependencies { return %deps; } + +##################################################################### +# Autoloaded Accessors +##################################################################### + +# Determines whether an attribute access trapped by the AUTOLOAD function +# is for a valid bug attribute. Bug attributes are properties and methods +# predefined by this module as well as bug fields for which an accessor +# can be defined by AUTOLOAD at runtime when the accessor is first accessed. +# +# XXX Strangely, some predefined attributes are on the list, but others aren't, +# and the original code didn't specify why that is. Presumably the only +# attributes that need to be on this list are those that aren't predefined; +# we should verify that and update the list accordingly. +# +sub _validate_attribute { + my ($attribute) = @_; + + my @valid_attributes = ( + # Miscellaneous properties and methods. + qw(error groups + longdescs milestoneurl attachments + isopened isunconfirmed + flag_types num_attachment_flag_types + show_attachment_flags use_keywords any_flags_requesteeble), + + # Bug fields. + Bugzilla::Bug->fields + ); + + return grep($attribute eq $_, @valid_attributes) ? 1 : 0; +} + sub AUTOLOAD { use vars qw($AUTOLOAD); my $attr = $AUTOLOAD; $attr =~ s/.*:://; return unless $attr=~ /[^A-Z]/; - confess ("invalid bug attribute $attr") unless $ok_field{$attr}; + confess("invalid bug attribute $attr") unless _validate_attribute($attr); no strict 'refs'; *$AUTOLOAD = sub { diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index c00518732..afb621f78 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__ -- cgit v1.2.3-24-g4f1b