Commit 199d6ed7 authored by myk%mozilla.org's avatar myk%mozilla.org

Bug 287325: an initial implementation of custom fields, including the ability to…

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
parent 8a751a1e
......@@ -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
......
......@@ -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}) {
......
......@@ -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;
......@@ -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'},
......
......@@ -14,6 +14,57 @@
#
# Contributor(s): Dan Mosedale <dmose@mozilla.org>
# Frdric Buclin <LpSolit@gmail.com>
# Myk Melez <myk@mozilla.org>
=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<id>
the unique identifier for the field;
=back
=cut
sub id { return $_[0]->{id} }
=over
=item C<name>
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<description>
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<type>
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<custom>
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<obsolete>
a boolean specifying whether or not the field is obsolete;
=back
=cut
sub obsolete { return $_[0]->{obsolete} }
=pod
=head2 Class Methods
=over
=item C<create($name, $desc)>
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<match($criteria)>
=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<check_form_field_defined($cgi, $fieldname)>
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<get_field_id($fieldname)>
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__
......@@ -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 <myk@mozilla.org> 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.
#
......
#!/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 <myk@mozilla.org>
################################################################################
# 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 <<END;
Usage:
Use this script to add a custom field to your Bugzilla installation
by invoking it with the --name and --desc command-line options:
$command --name=<field_name> --desc="<field description>"
<field_name> 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.
<field description> 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";
......@@ -31,6 +31,7 @@ use lib ".";
require "globals.pl";
use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Config qw(:DEFAULT $datadir);
use Bugzilla::Series;
......
......@@ -21,6 +21,7 @@ use lib ".";
require "globals.pl";
use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Config qw(:DEFAULT $datadir);
use Bugzilla::Product;
......
......@@ -33,6 +33,7 @@
use strict;
use lib ".";
use Bugzilla;
use Bugzilla::Constants;
require "globals.pl";
use Bugzilla::Bug;
......
......@@ -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;
......
......@@ -28,6 +28,7 @@ use lib qw(.);
require "globals.pl";
use Bugzilla;
use Bugzilla::Bug;
my $cgi = Bugzilla->cgi;
......
......@@ -27,6 +27,7 @@ use strict;
use lib qw(.);
require "globals.pl";
use Bugzilla;
use Bugzilla::User;
use Bugzilla::Bug;
......
......@@ -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_*
......
......@@ -497,6 +497,15 @@
</table>
[% END %]
[%# *** Custom Fields *** %]
[% USE Bugzilla %]
<table>
[% FOREACH field = Bugzilla.get_fields({ obsolete => 0, custom => 1 }) %]
[% PROCESS bug/field.html.tmpl value=bug.${field.name} %]
[% END %]
</table>
[%# *** Attachments *** %]
[% PROCESS attachment/list.html.tmpl
......
[%# 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 <myk@mozilla.org>
#%]
<tr>
[% SWITCH field.type %]
[% CASE constants.FIELD_TYPE_FREETEXT %]
<th align="right">
<label for="[% field.name FILTER html %]">
[% field.description FILTER html %]:
</label>
</th>
<td>
<input name="[% field.name FILTER html %]"
value="[% value FILTER html %]"
size="60">
</td>
[% END %]
</tr>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment