Commit b6e4f982 authored by myk%mozilla.org's avatar myk%mozilla.org
Browse files

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

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 ee4cdca7
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -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
+11 −2
Original line number Diff line number Diff line
@@ -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}) {
+13 −0
Original line number Diff line number Diff line
@@ -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;
+5 −0
Original line number Diff line number Diff line
@@ -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'},
+297 −45
Original line number Diff line number Diff line
@@ -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);
=head2 Instance Properties

        my $field = $result || $fieldname;
        ThrowCodeError("illegal_field", { field => $field });
    }
}
=over

sub check_form_field_defined {
    my ($cgi, $fieldname) = @_;
=item C<id>

    if (!defined $cgi->param($fieldname)) {
        ThrowCodeError("undefined_field", { field => $fieldname });
    }
}
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) = @_;
  
=head2 Validation
    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 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__
Loading