Commit 931bd3b6 authored by mkanat%bugzilla.org's avatar mkanat%bugzilla.org

Bug 455632: Add Bugzilla::Field::Choice->create and have editvalues.cgi use it

Patch By Max Kanat-Alexander <mkanat@bugzilla.org> r=bbaetz, a=mkanat
parent 7bfd680d
...@@ -54,7 +54,6 @@ use base qw(Bugzilla::Object Exporter); ...@@ -54,7 +54,6 @@ use base qw(Bugzilla::Object Exporter);
RemoveVotes CheckIfVotedConfirmed RemoveVotes CheckIfVotedConfirmed
LogActivityEntry LogActivityEntry
editable_bug_fields editable_bug_fields
SPECIAL_STATUS_WORKFLOW_ACTIONS
); );
##################################################################### #####################################################################
...@@ -234,13 +233,6 @@ use constant MAX_LINE_LENGTH => 254; ...@@ -234,13 +233,6 @@ use constant MAX_LINE_LENGTH => 254;
# Used in _check_comment(). Gives the max length allowed for a comment. # Used in _check_comment(). Gives the max length allowed for a comment.
use constant MAX_COMMENT_LENGTH => 65535; use constant MAX_COMMENT_LENGTH => 65535;
use constant SPECIAL_STATUS_WORKFLOW_ACTIONS => qw(
none
duplicate
change_resolution
clearresolution
);
##################################################################### #####################################################################
sub new { sub new {
......
...@@ -151,6 +151,7 @@ use File::Basename; ...@@ -151,6 +151,7 @@ use File::Basename;
MAX_PRODUCT_SIZE MAX_PRODUCT_SIZE
MAX_MILESTONE_SIZE MAX_MILESTONE_SIZE
MAX_COMPONENT_SIZE MAX_COMPONENT_SIZE
MAX_FIELD_VALUE_SIZE
MAX_FREETEXT_LENGTH MAX_FREETEXT_LENGTH
); );
...@@ -427,6 +428,9 @@ use constant MAX_MILESTONE_SIZE => 20; ...@@ -427,6 +428,9 @@ use constant MAX_MILESTONE_SIZE => 20;
# The longest component name allowed. # The longest component name allowed.
use constant MAX_COMPONENT_SIZE => 64; use constant MAX_COMPONENT_SIZE => 64;
# The maximum length for values of <select> fields.
use constant MAX_FIELD_VALUE_SIZE => 64;
# Maximum length allowed for free text fields. # Maximum length allowed for free text fields.
use constant MAX_FREETEXT_LENGTH => 255; use constant MAX_FREETEXT_LENGTH => 255;
......
...@@ -75,7 +75,6 @@ use base qw(Exporter Bugzilla::Object); ...@@ -75,7 +75,6 @@ use base qw(Exporter Bugzilla::Object);
use Bugzilla::Constants; use Bugzilla::Constants;
use Bugzilla::Error; use Bugzilla::Error;
use Bugzilla::Field::Choice;
use Bugzilla::Util; use Bugzilla::Util;
############################### ###############################
...@@ -376,7 +375,8 @@ sub legal_values { ...@@ -376,7 +375,8 @@ sub legal_values {
my $self = shift; my $self = shift;
if (!defined $self->{'legal_values'}) { if (!defined $self->{'legal_values'}) {
my @values = Bugzilla::Field::Choice->get_all({ field => $self }); require Bugzilla::Field::Choice;
my @values = Bugzilla::Field::Choice->type($self)->get_all();
$self->{'legal_values'} = \@values; $self->{'legal_values'} = \@values;
} }
return $self->{'legal_values'}; return $self->{'legal_values'};
......
...@@ -26,6 +26,8 @@ use base qw(Bugzilla::Object); ...@@ -26,6 +26,8 @@ use base qw(Bugzilla::Object);
use Bugzilla::Constants; use Bugzilla::Constants;
use Bugzilla::Error; use Bugzilla::Error;
use Bugzilla::Field;
use Bugzilla::Util qw(trim detaint_natural);
use Scalar::Util qw(blessed); use Scalar::Util qw(blessed);
...@@ -42,104 +44,141 @@ use constant DB_COLUMNS => qw( ...@@ -42,104 +44,141 @@ use constant DB_COLUMNS => qw(
use constant NAME_FIELD => 'value'; use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'sortkey, value'; use constant LIST_ORDER => 'sortkey, value';
########################################## use constant REQUIRED_CREATE_FIELDS => qw(value);
# Constructors and Database Manipulation #
##########################################
# When calling class methods, we aren't based on just one table, use constant VALIDATORS => {
# so we need some slightly hacky way to do DB_TABLE. We do it by overriding value => \&_check_value,
# class methods and making them specify $_new_table. This is possible sortkey => \&_check_sortkey,
# because we're always called from inside Bugzilla::Field, so it always };
# has a $self to pass us which contains info about which field we're
# attached to. use constant CLASS_MAP => {
# bug_status => 'Bugzilla::Status',
# This isn't thread safe, but Bugzilla isn't a threaded application. };
our $_new_table;
our $_current_field; #################
sub DB_TABLE { # Class Factory #
my $invocant = shift; #################
if (blessed $invocant) {
return $invocant->field->name; # Bugzilla::Field::Choice is actually an abstract base class. Every field
# type has its own dynamically-generated class for its values. This allows
# certain fields to have special types, like how bug_status's values
# are Bugzilla::Status objects.
sub type {
my ($class, $field) = @_;
my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field);
my $field_name = $field_obj->name;
if ($class->CLASS_MAP->{$field_name}) {
return $class->CLASS_MAP->{$field_name};
} }
return $_new_table;
# For generic classes, we use a lowercase class name, so as
# not to interfere with any real subclasses we might make some day.
my $package = "Bugzilla::Field::Choice::$field_name";
# We check defined so that the package and the stored field are only
# created once globally (at least per request). We prefix it with
# field_ (instead of suffixing it) so that we don't somehow conflict
# with the names of custom fields.
if (!defined Bugzilla->request_cache->{"field_$package"}) {
eval <<EOC;
package $package;
use base qw(Bugzilla::Field::Choice);
use constant DB_TABLE => '$field_name';
EOC
Bugzilla->request_cache->{"field_$package"} = $field_obj;
}
return $package;
} }
################
# Constructors #
################
# We just make new() enforce this, which should give developers
# the understanding that you can't use Bugzilla::Field::Choice
# without calling type().
sub new { sub new {
my $class = shift; my $class = shift;
my ($params) = @_; if ($class eq 'Bugzilla::Field::Choice') {
_check_field_arg($params); ThrowCodeError('field_choice_must_use_type');
my $self = $class->SUPER::new($params); }
_fix_return($self); $class->SUPER::new(@_);
return $self;
} }
sub new_from_list { #########################
my $class = shift; # Database Manipulation #
my ($ids, $params) = @_; #########################
_check_field_arg($params);
my $list = $class->SUPER::new_from_list(@_);
_fix_return($list);
return $list;
}
sub match { # Our subclasses can take more arguments than we normally accept.
# So, we override create() to remove arguments that aren't valid
# columns. (Normally Bugzilla::Object dies if you pass arguments
# that aren't valid columns.)
sub create {
my $class = shift; my $class = shift;
my ($params) = @_; my ($params) = @_;
_check_field_arg($params); foreach my $key (keys %$params) {
my $results = $class->SUPER::match(@_); if (!grep {$_ eq $key} $class->DB_COLUMNS) {
_fix_return($results); delete $params->{$key};
return $results; }
}
return $class->SUPER::create(@_);
} }
sub get_all { #############
my $class = shift; # Accessors #
_check_field_arg(@_); #############
my @list = $class->SUPER::get_all(@_);
_fix_return(\@list);
return @list;
}
sub _check_field_arg { sub sortkey { return $_[0]->{'sortkey'}; }
my ($params) = @_; sub field {
my ($class, undef, undef, $func) = caller(1); my $invocant = shift;
if (!defined $params->{field}) { my $class = ref $invocant || $invocant;
ThrowCodeError('param_required', my $cache = Bugzilla->request_cache;
{ function => "${class}::$func", # This is just to make life easier for subclasses. Our auto-generated
param => 'field' }); # subclasses from type() already have this set.
} $cache->{"field_$class"} ||=
elsif (!blessed $params->{field}) { new Bugzilla::Field({ name => $class->DB_TABLE });
ThrowCodeError('bad_arg', { function => "${class}::$func", return $cache->{"field_$class"};
argument => 'field' });
}
$_new_table = $params->{field}->name;
$_current_field = $params->{field};
delete $params->{field};
} }
sub _fix_return { ##############
my $retval = shift; # Validators #
if (ref $retval eq 'ARRAY') { ##############
foreach my $obj (@$retval) {
$obj->{field} = $_current_field; sub _check_value {
} my ($invocant, $value) = @_;
}
elsif (defined $retval) { my $field = $invocant->field;
$retval->{field} = $_current_field;
$value = trim($value);
ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq "";
ThrowUserError('fieldvalue_name_too_long', { value => $value })
if length($value) > MAX_FIELD_VALUE_SIZE;
my $exists = $invocant->type($field)->new({ name => $value });
if ($exists) {
ThrowUserError('fieldvalue_already_exists',
{ field => $field, value => $value });
} }
# We do this just to avoid any possible bugs where $_new_table or return $value;
# $_current_field are set from a previous call. It also might save
# a little memory under mod_perl by releasing $_current_field explicitly.
undef $_new_table;
undef $_current_field;
} }
############# sub _check_sortkey {
# Accessors # my ($invocant, $value) = @_;
############# $value = trim($value);
return 0 if !$value;
# Store for the error message in case detaint_natural clears it.
my $orig_value = $value;
detaint_natural($value)
|| ThrowUserError('fieldvalue_sortkey_invalid',
{ sortkey => $orig_value,
field => $invocant->field });
return $value;
}
sub sortkey { return $_[0]->{'sortkey'}; }
sub field { return $_[0]->{'field'}; }
1; 1;
...@@ -153,27 +192,47 @@ Bugzilla::Field::Choice - A legal value for a <select>-type field. ...@@ -153,27 +192,47 @@ Bugzilla::Field::Choice - A legal value for a <select>-type field.
my $field = new Bugzilla::Field({name => 'bug_status'}); my $field = new Bugzilla::Field({name => 'bug_status'});
my $choice = new Bugzilla::Field::Choice({ field => $field, id => 1 }); my $choice = new Bugzilla::Field::Choice->type($field)->new(1);
my $choice = new Bugzilla::Field::Choice({ field => $field, name => 'NEW' });
my $choices = Bugzilla::Field::Choice->new_from_list([1,2,3], my $choices = Bugzilla::Field::Choice->type($field)->new_from_list([1,2,3]);
{ field => $field}); my $choices = Bugzilla::Field::Choice->type($field)->get_all();
my $choices = Bugzilla::Field::Choice->get_all({ field => $field }); my $choices = Bugzilla::Field::Choice->type($field->match({ sortkey => 10 });
my $choices = Bugzilla::Field::Choice->match({ sortkey => 10,
field => $field });
=head1 DESCRIPTION =head1 DESCRIPTION
This is an implementation of L<Bugzilla::Object>, but with a twist. This is an implementation of L<Bugzilla::Object>, but with a twist.
All the class methods require that you specify an additional C<field> You can't call any class methods (such as C<new>, C<create>, etc.)
argument, which is a L<Bugzilla::Field> object that represents the directly on C<Bugzilla::Field::Choice> itself. Instead, you have to
field whose value this is. call C<Bugzilla::Field::Choice-E<gt>type($field)> to get the class
you're going to instantiate, and then you call the methods on that.
We do that because each field has its own database table for its values, so
each value type needs its own class.
You can look at the L</SYNOPSIS> to see where this extra C<field> See the L</SYNOPSIS> for examples of how this works.
argument goes in each function.
=head1 METHODS =head1 METHODS
=head2 Class Factory
In object-oriented design, a "class factory" is a method that picks
and returns the right class for you, based on an argument that you pass.
=over
=item C<type>
Takes a single argument, which is either the name of a field from the
C<fielddefs> table, or a L<Bugzilla::Field> object representing a field.
Returns an appropriate subclass of C<Bugzilla::Field::Choice> that you
can now call class methods on (like C<new>, C<create>, C<match>, etc.)
B<NOTE>: YOU CANNOT CALL CLASS METHODS ON C<Bugzilla::Field::Choice>. You
must call C<type> to get a class you can call methods on.
=back
=head2 Accessors =head2 Accessors
These are in addition to the standard L<Bugzilla::Object> accessors. These are in addition to the standard L<Bugzilla::Object> accessors.
......
...@@ -22,13 +22,28 @@ use strict; ...@@ -22,13 +22,28 @@ use strict;
package Bugzilla::Status; package Bugzilla::Status;
use base qw(Bugzilla::Object Exporter); use Bugzilla::Error;
@Bugzilla::Status::EXPORT = qw(BUG_STATE_OPEN is_open_state closed_bug_statuses);
use base qw(Bugzilla::Field::Choice Exporter);
@Bugzilla::Status::EXPORT = qw(
BUG_STATE_OPEN
SPECIAL_STATUS_WORKFLOW_ACTIONS
is_open_state
closed_bug_statuses
);
################################ ################################
##### Initialization ##### ##### Initialization #####
################################ ################################
use constant SPECIAL_STATUS_WORKFLOW_ACTIONS => qw(
none
duplicate
change_resolution
clearresolution
);
use constant DB_TABLE => 'bug_status'; use constant DB_TABLE => 'bug_status';
use constant DB_COLUMNS => qw( use constant DB_COLUMNS => qw(
...@@ -39,8 +54,24 @@ use constant DB_COLUMNS => qw( ...@@ -39,8 +54,24 @@ use constant DB_COLUMNS => qw(
is_open is_open
); );
use constant NAME_FIELD => 'value'; sub VALIDATORS {
use constant LIST_ORDER => 'sortkey, value'; my $invocant = shift;
my $validators = $invocant->SUPER::VALIDATORS;
$validators->{is_open} = \&Bugzilla::Object::check_boolean;
$validators->{value} = \&_check_value;
return $validators;
}
#########################
# Database Manipulation #
#########################
sub create {
my $class = shift;
my $self = $class->SUPER::create(@_);
add_missing_bug_status_transitions();
return $self;
}
############################### ###############################
##### Accessors #### ##### Accessors ####
...@@ -51,6 +82,22 @@ sub sortkey { return $_[0]->{'sortkey'}; } ...@@ -51,6 +82,22 @@ sub sortkey { return $_[0]->{'sortkey'}; }
sub is_active { return $_[0]->{'isactive'}; } sub is_active { return $_[0]->{'isactive'}; }
sub is_open { return $_[0]->{'is_open'}; } sub is_open { return $_[0]->{'is_open'}; }
##############
# Validators #
##############
sub _check_value {
my $invocant = shift;
my $value = $invocant->SUPER::_check_value(@_);
if (grep { lc($value) eq lc($_) } SPECIAL_STATUS_WORKFLOW_ACTIONS) {
ThrowUserError('fieldvalue_reserved_word',
{ field => $invocant->field, value => $value });
}
return $value;
}
############################### ###############################
##### Methods #### ##### Methods ####
############################### ###############################
......
...@@ -29,7 +29,7 @@ use Bugzilla::Config qw(:admin); ...@@ -29,7 +29,7 @@ use Bugzilla::Config qw(:admin);
use Bugzilla::Token; use Bugzilla::Token;
use Bugzilla::Field; use Bugzilla::Field;
use Bugzilla::Bug; use Bugzilla::Bug;
use Bugzilla::Status; use Bugzilla::Status qw(SPECIAL_STATUS_WORKFLOW_ACTIONS);
# List of different tables that contain the changeable field values # List of different tables that contain the changeable field values
# (the old "enums.") Keep them in alphabetical order by their # (the old "enums.") Keep them in alphabetical order by their
...@@ -172,12 +172,8 @@ trick_taint($field); ...@@ -172,12 +172,8 @@ trick_taint($field);
sub display_field_values { sub display_field_values {
my $template = Bugzilla->template; my $template = Bugzilla->template;
my $field = $vars->{'field'}->name; my $field = $vars->{'field'}->name;
my $fieldvalues =
Bugzilla->dbh->selectall_arrayref("SELECT value AS name, sortkey"
. " FROM $field ORDER BY sortkey, value",
{Slice =>{}});
$vars->{'values'} = $fieldvalues; $vars->{'values'} = $vars->{'field'}->legal_values;
$vars->{'default'} = Bugzilla->params->{$defaults{$field}} if defined $defaults{$field}; $vars->{'default'} = Bugzilla->params->{$defaults{$field}} if defined $defaults{$field};
$vars->{'static'} = $static{$field} if exists $static{$field}; $vars->{'static'} = $static{$field} if exists $static{$field};
...@@ -212,53 +208,14 @@ if ($action eq 'add') { ...@@ -212,53 +208,14 @@ if ($action eq 'add') {
if ($action eq 'new') { if ($action eq 'new') {
check_token_data($token, 'add_field_value'); check_token_data($token, 'add_field_value');
# Cleanups and validity checks my $created_value = Bugzilla::Field::Choice->type($field_obj)->create(
$value || ThrowUserError('fieldvalue_undefined'); { value => $value, sortkey => $sortkey,
is_open => scalar $cgi->param('is_open') });
if (length($value) > 60) {
ThrowUserError('fieldvalue_name_too_long',
{'value' => $value});
}
# Need to store in case detaint_natural() clears the sortkey
my $stored_sortkey = $sortkey;
if (!detaint_natural($sortkey)) {
ThrowUserError('fieldvalue_sortkey_invalid',
{'name' => $field,
'sortkey' => $stored_sortkey});
}
if (ValueExists($field, $value)) {
ThrowUserError('fieldvalue_already_exists',
{'field' => $field_obj,
'value' => $value});
}
if ($field eq 'bug_status'
&& (grep { lc($value) eq $_ } SPECIAL_STATUS_WORKFLOW_ACTIONS))
{
$vars->{'value'} = $value;
ThrowUserError('fieldvalue_reserved_word', $vars);
}
# Value is only used in a SELECT placeholder and through the HTML filter.
trick_taint($value);
# Add the new field value.
$dbh->do("INSERT INTO $field (value, sortkey) VALUES (?, ?)",
undef, ($value, $sortkey));
if ($field eq 'bug_status') {
unless ($cgi->param('is_open')) {
# The bug status is a closed state, but they are open by default.
$dbh->do('UPDATE bug_status SET is_open = 0 WHERE value = ?', undef, $value);
}
# Allow the transition from this new bug status to the one used
# by the 'duplicate_or_move_bug_status' parameter.
Bugzilla::Status::add_missing_bug_status_transitions();
}
delete_token($token); delete_token($token);
$vars->{'message'} = 'field_value_created'; $vars->{'message'} = 'field_value_created';
$vars->{'value'} = $value; $vars->{'value'} = $created_value->name;
display_field_values(); display_field_values();
} }
......
...@@ -145,6 +145,12 @@ ...@@ -145,6 +145,12 @@
in the database for '[% username FILTER html %]', but your in the database for '[% username FILTER html %]', but your
account source says that '[% extern_user FILTER html %]' has that ID. account source says that '[% extern_user FILTER html %]' has that ID.
[% ELSIF error == "field_choice_must_use_type" %]
When you call a class method on <code>Bugzilla::Field::Choice</code>,
you must call <code>Bugzilla::Field::Choice-&gt;type('some_field')</code>
to generate the right class (you can't call class methods directly
on Bugzilla::Field::Choice).
[% ELSIF error == "field_type_mismatch" %] [% ELSIF error == "field_type_mismatch" %]
Cannot seem to handle <code>[% field FILTER html %]</code> Cannot seem to handle <code>[% field FILTER html %]</code>
and <code>[% type FILTER html %]</code> together. and <code>[% type FILTER html %]</code> together.
......
...@@ -432,7 +432,7 @@ ...@@ -432,7 +432,7 @@
[% ELSIF error == "fieldvalue_already_exists" %] [% ELSIF error == "fieldvalue_already_exists" %]
[% title = "Field Value Already Exists" %] [% title = "Field Value Already Exists" %]
The value '[% value FILTER html %]' already exists for the The value '[% value FILTER html %]' already exists for the
'[%- field.description FILTER html %]' field. [%+ field.description FILTER html %] field.
[% ELSIF error == "fieldvalue_doesnt_exist" %] [% ELSIF error == "fieldvalue_doesnt_exist" %]
[% title = "Specified Field Value Does Not Exist" %] [% title = "Specified Field Value Does Not Exist" %]
...@@ -450,8 +450,9 @@ ...@@ -450,8 +450,9 @@
[% ELSIF error == "fieldvalue_name_too_long" %] [% ELSIF error == "fieldvalue_name_too_long" %]
[% title = "Field Value Is Too Long" %] [% title = "Field Value Is Too Long" %]
The value of a field is limited to 60 characters. The value of a field is limited to [% constants.FIELD_VALUE_MAX_SIZE %]
'[% value FILTER html %]' is too long ([% value.length %] characters). characters. '[% value FILTER html %]' is too long ([% value.length %]
characters).
[% ELSIF error == "fieldvalue_not_editable" %] [% ELSIF error == "fieldvalue_not_editable" %]
[% title = "Field Value Not Editable" %] [% title = "Field Value Not Editable" %]
...@@ -475,8 +476,9 @@ ...@@ -475,8 +476,9 @@
[% ELSIF error == "fieldvalue_sortkey_invalid" %] [% ELSIF error == "fieldvalue_sortkey_invalid" %]
[% title = "Invalid Field Value Sortkey" %] [% title = "Invalid Field Value Sortkey" %]
The sortkey '[% sortkey FILTER html %]' for the '[% name FILTER html %]' The sortkey '[% sortkey FILTER html %]' for the
field is not a valid (positive) number. [%+ field.description FILTER html %] field is not a valid
(positive) number.
[% ELSIF error == "fieldvalue_still_has_bugs" %] [% ELSIF error == "fieldvalue_still_has_bugs" %]
[% title = "You Cannot Delete This Field Value" %] [% title = "You Cannot Delete This Field Value" %]
...@@ -1178,13 +1180,13 @@ ...@@ -1178,13 +1180,13 @@
is less than the minimum allowable value of '[% min_num FILTER html %]'. is less than the minimum allowable value of '[% min_num FILTER html %]'.
[% ELSIF error == "object_name_not_specified" %] [% ELSIF error == "object_name_not_specified" %]
[% type = BLOCK %][% PROCESS object_name %][% END %] [% type = BLOCK %][% INCLUDE object_name class = class %][% END %]
[% title = BLOCK %][% type FILTER ucfirst FILTER html %] Not [% title = BLOCK %][% type FILTER ucfirst FILTER html %] Not
Specified[% END %] Specified[% END %]
You must select/enter a [% type FILTER html %]. You must select/enter a [% type FILTER html %].
[% ELSIF error == "object_does_not_exist" %] [% ELSIF error == "object_does_not_exist" %]
[% type = BLOCK %][% PROCESS object_name %][% END %] [% type = BLOCK %][% INCLUDE object_name class = class %][% END %]
[% title = BLOCK %]Invalid [% type FILTER ucfirst FILTER html %][% END %] [% title = BLOCK %]Invalid [% type FILTER ucfirst FILTER html %][% END %]
There is no [% type FILTER html %] named '[% name FILTER html %]' There is no [% type FILTER html %] named '[% name FILTER html %]'
[% IF product.defined %] [% IF product.defined %]
...@@ -1672,5 +1674,10 @@ ...@@ -1672,5 +1674,10 @@
milestone milestone
[% ELSIF class == "Bugzilla::Status" %] [% ELSIF class == "Bugzilla::Status" %]
status status
[% ELSIF class == "Bugzilla::Field" %]
field
[% ELSIF ( matches = class.match('^Bugzilla::Field::Choice::(.+)') ) %]
[% SET field_name = matches.0 %]
[% field_descs.$field_name %]
[% END %] [% END %]
[% END %] [% END %]
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