Commit da9ac943 authored by lpsolit%gmail.com's avatar lpsolit%gmail.com

Bug 287325: Ability to add custom plain-text fields to a Bug - Patch by Myk…

Bug 287325: Ability to add custom plain-text fields to a Bug - Patch by Myk Melez <myk@mozilla.org> r=mkanat a=justdave
parent 7780ace7
......@@ -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
......
......@@ -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 {
......
......@@ -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
......@@ -4263,6 +4263,13 @@ if (scalar(@$controlchar_bugs))
print " done.\n" if $found;
}
# 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;
......
......@@ -694,7 +694,7 @@ if ($action eq Param('move-button-text')) {
$msg .= "From: Bugzilla <" . $from . ">\n";
$msg .= "Subject: Moving bug(s) " . join(', ', @idlist) . "\n\n";
my @fieldlist = (Bugzilla::Bug::fields(), 'group', 'long_desc',
my @fieldlist = (Bugzilla::Bug->fields, 'group', 'long_desc',
'attachment', 'attachmentdata');
my %displayfields;
foreach (@fieldlist) {
......@@ -867,6 +867,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;
......
......@@ -108,7 +108,7 @@ $vars->{'bug_list'} = \@bug_list;
# If no explicit list is defined, we show all fields. We then exclude any
# on the exclusion list. This is so you can say e.g. "Everything except
# attachments" without listing almost all the fields.
my @fieldlist = (Bugzilla::Bug::fields(), 'group', 'long_desc',
my @fieldlist = (Bugzilla::Bug->fields, 'group', 'long_desc',
'attachment', 'attachmentdata');
my %displayfields;
......
......@@ -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