diff options
Diffstat (limited to 'Bugzilla/Bug.pm')
-rw-r--r-- | Bugzilla/Bug.pm | 438 |
1 files changed, 327 insertions, 111 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 7b86ab2a1..1c3b0267c 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -80,7 +80,8 @@ use constant AUDIT_UPDATES => 0; # This is a sub because it needs to call other subroutines. sub DB_COLUMNS { my $dbh = Bugzilla->dbh; - my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT} + my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT + && $_->type != FIELD_TYPE_EXTENSION} Bugzilla->active_custom_fields; my @custom_names = map {$_->name} @custom; @@ -93,6 +94,7 @@ sub DB_COLUMNS { bug_status cclist_accessible component_id + creation_ts delta_ts estimated_time everconfirmed @@ -111,12 +113,11 @@ sub DB_COLUMNS { version ), 'reporter AS reporter_id', - $dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts', $dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline', @custom_names); - + Bugzilla::Hook::process("bug_columns", { columns => \@columns }); - + return @columns; } @@ -166,6 +167,9 @@ sub VALIDATORS { elsif ($field->type == FIELD_TYPE_DATETIME) { $validator = \&_check_datetime_field; } + elsif ($field->type == FIELD_TYPE_DATE) { + $validator = \&_check_date_field; + } elsif ($field->type == FIELD_TYPE_FREETEXT) { $validator = \&_check_freetext_field; } @@ -211,7 +215,8 @@ sub VALIDATOR_DEPENDENCIES { }; sub UPDATE_COLUMNS { - my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT} + my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT + && $_->type != FIELD_TYPE_EXTENSION} Bugzilla->active_custom_fields; my @custom_names = map {$_->name} @custom; my @columns = qw( @@ -248,7 +253,8 @@ use constant NUMERIC_COLUMNS => qw( ); sub DATE_COLUMNS { - my @fields = @{ Bugzilla->fields({ type => FIELD_TYPE_DATETIME }) }; + my @fields = (@{ Bugzilla->fields({ type => FIELD_TYPE_DATETIME }) }, + @{ Bugzilla->fields({ type => FIELD_TYPE_DATE }) }); return map { $_->name } @fields; } @@ -280,10 +286,6 @@ use constant FIELD_MAP => { summary => 'short_desc', url => 'bug_file_loc', whiteboard => 'status_whiteboard', - - # These are special values for the WebService Bug.search method. - limit => 'LIMIT', - offset => 'OFFSET', }; use constant REQUIRED_FIELD_MAP => { @@ -311,21 +313,11 @@ use constant EXTRA_REQUIRED_FIELDS => qw(creation_ts target_milestone cc qa_cont ##################################################################### -# This and "new" catch every single way of creating a bug, so that we -# can call _create_cf_accessors. -sub _do_list_select { - my $invocant = shift; - $invocant->_create_cf_accessors(); - return $invocant->SUPER::_do_list_select(@_); -} - sub new { my $invocant = shift; my $class = ref($invocant) || $invocant; my $param = shift; - $class->_create_cf_accessors(); - # Remove leading "#" mark if we've just been passed an id. if (!ref $param && $param =~ /^#(\d+)$/) { $param = $1; @@ -333,10 +325,14 @@ sub new { # If we get something that looks like a word (not a number), # make it the "name" param. - if (!defined $param || (!ref($param) && $param !~ /^\d+$/)) { + if (!defined $param + || (!ref($param) && (!$param || $param =~ /\D/)) + || (ref($param) && (!$param->{id} || $param->{id} =~ /\D/))) + { # But only if aliases are enabled. if (Bugzilla->params->{'usebugaliases'} && $param) { - $param = { name => $param }; + $param = { name => ref($param) ? $param->{id} : $param, + cache => ref($param) ? $param->{cache} : 0 }; } else { # Aliases are off, and we got something that's not a number. @@ -370,20 +366,35 @@ sub new { return $self; } -sub check { +sub initialize { + $_[0]->_create_cf_accessors(); +} + +sub cache_key { my $class = shift; - my ($id, $field) = @_; + my $key = $class->SUPER::cache_key(@_) + || return; + return $key . ',' . Bugzilla->user->id; +} - ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id; +sub check { + my $class = shift; + my ($param, $field) = @_; # Bugzilla::Bug throws lots of special errors, so we don't call # SUPER::check, we just call our new and do our own checks. - my $self = $class->new(trim($id)); - # For error messages, use the id that was returned by new(), because - # it's cleaned up. - $id = $self->id; + my $id = ref($param) + ? ($param->{id} = trim($param->{id})) + : ($param = trim($param)); + ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id; + + my $self = $class->new($param); if ($self->{error}) { + # For error messages, use the id that was returned by new(), because + # it's cleaned up. + $id = $self->id; + if ($self->{error} eq 'NotFound') { ThrowUserError("bug_id_does_not_exist", { bug_id => $id }); } @@ -493,6 +504,13 @@ sub preload { # If we don't do this, can_see_bug will do one call per bug in # the dependency lists, during get_bug_link in Bugzilla::Template. $user->visible_bugs(\@all_dep_ids); + + # We preload comments here in order to allow us to compare the time it + # takes to load comments from the database with the template rendering + # time. + foreach my $bug (@$bugs) { + $bug->comments(); + } } sub possible_duplicates { @@ -676,21 +694,20 @@ sub create { # Set up dependencies (blocked/dependson) my $sth_deps = $dbh->prepare( 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)'); - my $sth_bug_time = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); foreach my $depends_on_id (@$depends_on) { $sth_deps->execute($bug->bug_id, $depends_on_id); # Log the reverse action on the other bug. LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id, $bug->{reporter_id}, $timestamp); - $sth_bug_time->execute($timestamp, $depends_on_id); + _update_delta_ts($depends_on_id, $timestamp); } foreach my $blocked_id (@$blocked) { $sth_deps->execute($blocked_id, $bug->bug_id); # Log the reverse action on the other bug. LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id, $bug->{reporter_id}, $timestamp); - $sth_bug_time->execute($timestamp, $blocked_id); + _update_delta_ts($blocked_id, $timestamp); } # Insert the values into the multiselect value tables @@ -721,7 +738,10 @@ sub create { # Because MySQL doesn't support transactions on the fulltext table, # we do this after we've committed the transaction. That way we're # sure we're inserting a good Bug ID. - $bug->_sync_fulltext('new bug'); + $bug->_sync_fulltext( new_bug => 1 ); + + # BMO - some work should happen outside of the transaction block + Bugzilla::Hook::process('bug_after_create', { bug => $bug, timestamp => $timestamp }); return $bug; } @@ -775,8 +795,9 @@ sub run_create_validators { sub update { my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; # XXX This is just a temporary hack until all updating happens # inside this function. my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); @@ -785,6 +806,10 @@ sub update { my ($changes, $old_bug) = $self->SUPER::update(@_); + Bugzilla::Hook::process('bug_start_of_update', + { timestamp => $delta_ts, bug => $self, + old_bug => $old_bug, changes => $changes }); + # Certain items in $changes have to be fixed so that they hold # a name instead of an ID. foreach my $field (qw(product_id component_id)) { @@ -863,10 +888,9 @@ sub update { # Add an activity entry for the other bug. LogActivityEntry($removed_id, $other, $self->id, '', - Bugzilla->user->id, $delta_ts); + $user->id, $delta_ts); # Update delta_ts on the other bug so that we trigger mid-airs. - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $delta_ts, $removed_id); + _update_delta_ts($removed_id, $delta_ts); } foreach my $added_id (@$added) { $dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)", @@ -874,10 +898,9 @@ sub update { # Add an activity entry for the other bug. LogActivityEntry($added_id, $other, '', $self->id, - Bugzilla->user->id, $delta_ts); + $user->id, $delta_ts); # Update delta_ts on the other bug so that we trigger mid-airs. - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $delta_ts, $added_id); + _update_delta_ts($added_id, $delta_ts); } if (scalar(@$removed) || scalar(@$added)) { @@ -922,7 +945,7 @@ sub update { $comment = Bugzilla::Comment->insert_create_data($comment); if ($comment->work_time) { LogActivityEntry($self->id, "work_time", "", $comment->work_time, - Bugzilla->user->id, $delta_ts); + $user->id, $delta_ts); } } @@ -933,7 +956,7 @@ sub update { my ($from, $to) = $comment->is_private ? (0, 1) : (1, 0); LogActivityEntry($self->id, "longdescs.isprivate", $from, $to, - Bugzilla->user->id, $delta_ts, $comment->id); + $user->id, $delta_ts, $comment->id); } # Insert the values into the multiselect value tables @@ -971,6 +994,10 @@ sub update { $_->update foreach @{ $self->{_update_ref_bugs} || [] }; delete $self->{_update_ref_bugs}; + # BMO - allow extensions to alter what is logged into bugs_activity + Bugzilla::Hook::process('bug_update_before_logging', + { bug => $self, timestamp => $delta_ts, changes => $changes, old_bug => $old_bug }); + # Log bugs_activity items # XXX Eventually, when bugs_activity is able to track the dupe_id, # this code should go below the duplicates-table-updating code below. @@ -978,8 +1005,8 @@ sub update { my $change = $changes->{$field}; my $from = defined $change->[0] ? $change->[0] : ''; my $to = defined $change->[1] ? $change->[1] : ''; - LogActivityEntry($self->id, $field, $from, $to, Bugzilla->user->id, - $delta_ts); + LogActivityEntry($self->id, $field, $from, $to, + $user->id, $delta_ts); } # Check if we have to update the duplicates table and the other bug. @@ -993,7 +1020,7 @@ sub update { $update_dup->update(); } } - + $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef]; } @@ -1005,20 +1032,39 @@ sub update { if (scalar(keys %$changes) || $self->{added_comments} || $self->{comment_isprivate}) { - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, ($delta_ts, $self->id)); + _update_delta_ts($self->id, $delta_ts); $self->{delta_ts} = $delta_ts; } + # Update bug ignore data if user wants to ignore mail for this bug + if (exists $self->{'bug_ignored'}) { + my $bug_ignored_changed; + if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) { + $dbh->do('INSERT INTO email_bug_ignore + (user_id, bug_id) VALUES (?, ?)', + undef, $user->id, $self->id); + $bug_ignored_changed = 1; + + } + elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) { + $dbh->do('DELETE FROM email_bug_ignore + WHERE user_id = ? AND bug_id = ?', + undef, $user->id, $self->id); + $bug_ignored_changed = 1; + } + delete $user->{bugs_ignored} if $bug_ignored_changed; + } + $dbh->bz_commit_transaction(); # The only problem with this here is that update() is often called # in the middle of a transaction, and if that transaction is rolled # back, this change will *not* be rolled back. As we expect rollbacks # to be extremely rare, that is OK for us. - $self->_sync_fulltext() - if $self->{added_comments} || $changes->{short_desc} - || $self->{comment_isprivate}; + $self->_sync_fulltext( + update_short_desc => $changes->{short_desc}, + update_comments => $self->{added_comments} || $self->{comment_isprivate} + ); # Remove obsolete internal variables. delete $self->{'_old_assigned_to'}; @@ -1026,7 +1072,11 @@ sub update { # Also flush the visible_bugs cache for this bug as the user's # relationship with this bug may have changed. - delete Bugzilla->user->{_visible_bugs_cache}->{$self->id}; + delete $user->{_visible_bugs_cache}->{$self->id}; + + # BMO - some work should happen outside of the transaction block + Bugzilla::Hook::process('bug_after_update', + { bug => $self, timestamp => $delta_ts, changes => $changes, old_bug => $old_bug }); return $changes; } @@ -1052,27 +1102,56 @@ sub _extract_multi_selects { # Should be called any time you update short_desc or change a comment. sub _sync_fulltext { - my ($self, $new_bug) = @_; + my ($self, %options) = @_; my $dbh = Bugzilla->dbh; - if ($new_bug) { - $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc) - SELECT bug_id, short_desc FROM bugs WHERE bug_id = ?', - undef, $self->id); + + my($all_comments, $public_comments); + if ($options{new_bug} || $options{update_comments}) { + my $comments = $dbh->selectall_arrayref( + 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?', + undef, $self->id); + $all_comments = join("\n", map { $_->[0] } @$comments); + my @no_private = grep { !$_->[1] } @$comments; + $public_comments = join("\n", map { $_->[0] } @no_private); } - else { - $dbh->do('UPDATE bugs_fulltext SET short_desc = ? WHERE bug_id = ?', - undef, $self->short_desc, $self->id); + + if ($options{new_bug}) { + $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc, comments, + comments_noprivate) + VALUES (?, ?, ?, ?)', + undef, + $self->id, $self->short_desc, $all_comments, $public_comments); + } else { + my(@names, @values); + if ($options{update_short_desc}) { + push @names, 'short_desc'; + push @values, $self->short_desc; + } + if ($options{update_comments}) { + push @names, ('comments', 'comments_noprivate'); + push @values, ($all_comments, $public_comments); + } + if (@names) { + $dbh->do('UPDATE bugs_fulltext SET ' . + join(', ', map { "$_ = ?" } @names) . + ' WHERE bug_id = ?', + undef, + @values, $self->id); + } } - my $comments = $dbh->selectall_arrayref( - 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?', - undef, $self->id); - my $all = join("\n", map { $_->[0] } @$comments); - my @no_private = grep { !$_->[1] } @$comments; - my $nopriv_string = join("\n", map { $_->[0] } @no_private); - $dbh->do('UPDATE bugs_fulltext SET comments = ?, comments_noprivate = ? - WHERE bug_id = ?', undef, $all, $nopriv_string, $self->id); } +# Update a bug's delta_ts without requiring the full object to be loaded. +sub _update_delta_ts { + my ($bug_id, $timestamp) = @_; + Bugzilla->dbh->do( + "UPDATE bugs SET delta_ts = ? WHERE bug_id = ?", + undef, + $timestamp, $bug_id + ); + Bugzilla::Hook::process('bug_end_of_update_delta_ts', + { bug_id => $bug_id, timestamp => $timestamp }); +} # This is the correct way to delete bugs from the DB. # No bug should be deleted from anywhere else except from here. @@ -1161,15 +1240,15 @@ sub send_changes { changer => $user, ); - _send_bugmail({ id => $self->id, type => 'bug', forced => \%forced }, - $vars); + my $recipient_count = _send_bugmail( + { id => $self->id, type => 'bug', forced => \%forced }, $vars); # If the bug was marked as a duplicate, we need to notify users on the # other bug of any changes to that bug. my $new_dup_id = $changes->{'dup_id'} ? $changes->{'dup_id'}->[1] : undef; if ($new_dup_id) { - _send_bugmail({ forced => { changer => $user }, type => "dupe", - id => $new_dup_id }, $vars); + $recipient_count += _send_bugmail( + { forced => { changer => $user }, type => "dupe", id => $new_dup_id }, $vars); } # If there were changes in dependencies, we need to notify those @@ -1188,7 +1267,7 @@ sub send_changes { foreach my $id (@{ $self->blocked }) { $params->{id} = $id; - _send_bugmail($params, $vars); + $recipient_count += _send_bugmail($params, $vars); } } } @@ -1206,15 +1285,17 @@ sub send_changes { delete $changed_deps{''}; foreach my $id (sort { $a <=> $b } (keys %changed_deps)) { - _send_bugmail({ forced => { changer => $user }, type => "dep", - id => $id }, $vars); + $recipient_count += _send_bugmail( + { forced => { changer => $user }, type => "dep", id => $id }, $vars); } # Sending emails for the referenced bugs. foreach my $ref_bug_id (uniq @{ $self->{see_also_changes} || [] }) { - _send_bugmail({ forced => { changer => $user }, - id => $ref_bug_id }, $vars); + $recipient_count += _send_bugmail( + { forced => { changer => $user }, id => $ref_bug_id }, $vars); } + + return $recipient_count; } sub _send_bugmail { @@ -1222,7 +1303,7 @@ sub _send_bugmail { require Bugzilla::BugMail; - my $results = + my $results = Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { @@ -1233,6 +1314,8 @@ sub _send_bugmail { || ThrowTemplateError($template->error()); $vars->{'header_done'} = 1; } + + return scalar @{ $results->{sent} }; } ##################################################################### @@ -1655,7 +1738,9 @@ sub _check_groups { foreach my $name (@$group_names) { my $group = Bugzilla::Group->check_no_disclose({ %args, name => $name }); - if (!$product->group_is_settable($group)) { + # BMO : allow bugs to be always placed into some groups + if (!$product->group_always_settable($group) + && !$product->group_is_settable($group)) { ThrowUserError('group_restriction_not_allowed', { %args, name => $name }); } $add_groups{$group->id} = $group; @@ -1664,7 +1749,7 @@ sub _check_groups { # Now enforce mandatory groups. $add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory }; - + my @add_groups = values %add_groups; return \@add_groups; } @@ -1986,8 +2071,12 @@ sub _check_field_is_mandatory { } } +sub _check_date_field { + my ($invocant, $date) = @_; + return $invocant->_check_datetime_field($date, undef, {date_only => 1}); +} sub _check_datetime_field { - my ($invocant, $date_time) = @_; + my ($invocant, $date_time, $field, $params) = @_; # Empty datetimes are empty strings or strings only containing # 0's, whitespace, and punctuation. @@ -2001,6 +2090,10 @@ sub _check_datetime_field { ThrowUserError('illegal_date', { date => $date, format => 'YYYY-MM-DD' }); } + if ($time && $params->{date_only}) { + ThrowUserError('illegal_date', { date => $date_time, + format => 'YYYY-MM-DD' }); + } if ($time && !validate_time($time)) { ThrowUserError('illegal_time', { 'time' => $time, format => 'HH:MM:SS' }); @@ -2251,7 +2344,8 @@ sub set_all { $self->_add_remove($params, 'see_also'); # And set custom fields. - my @custom_fields = Bugzilla->active_custom_fields; + my @custom_fields = grep { $_->type != FIELD_TYPE_EXTENSION } + Bugzilla->active_custom_fields; foreach my $field (@custom_fields) { my $fname = $field->name; if (exists $params->{$fname}) { @@ -2266,7 +2360,7 @@ sub set_all { # we have to check that the current assignee, qa, and CCs are still # valid if we've switched products, under strict_isolation. We can only # do that here, because if they *did* change the assignee, qa, or CC, - # then we don't want to check the original ones, only the new ones. + # then we don't want to check the original ones, only the new ones. $self->_check_strict_isolation() if $product_changed; } @@ -2296,6 +2390,7 @@ sub reset_assigned_to { my $comp = $self->component_obj; $self->set_assigned_to($comp->default_assignee); } +sub set_bug_ignored { $_[0]->set('bug_ignored', $_[1]); } sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); } sub set_comment_is_private { my ($self, $comment_id, $isprivate) = @_; @@ -2794,19 +2889,25 @@ sub add_group { return if $self->in_group($group); - # Make sure that bugs in this product can actually be restricted - # to this group by the current user. - $self->product_obj->group_is_settable($group) - || ThrowUserError('group_restriction_not_allowed', $args); + # BMO : allow bugs to be always placed into some groups by the bug's + # reporter + if ($self->{reporter_id} != Bugzilla->user->id + || !$self->product_obj->group_always_settable($group)) + { + # Make sure that bugs in this product can actually be restricted + # to this group by the current user. + $self->product_obj->group_is_settable($group) + || ThrowUserError('group_restriction_not_allowed', $args); - # OtherControl people can add groups only during a product change, - # and only when the group is not NA for them. - if (!Bugzilla->user->in_group($group->name)) { - my $controls = $self->product_obj->group_controls->{$group->id}; - if (!$self->{_old_product_name} - || $controls->{othercontrol} == CONTROLMAPNA) - { - ThrowUserError('group_restriction_not_allowed', $args); + # OtherControl people can add groups only during a product change, + # and only when the group is not NA for them. + if (!Bugzilla->user->in_group($group->name)) { + my $controls = $self->product_obj->group_controls->{$group->id}; + if (!$self->{_old_product_name} + || $controls->{othercontrol} == CONTROLMAPNA) + { + ThrowUserError('group_restriction_not_allowed', $args); + } } } @@ -3161,8 +3262,8 @@ sub assigned_to { my ($self) = @_; return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'}; $self->{'assigned_to'} = 0 if $self->{'error'}; - $self->{'assigned_to_obj'} ||= new Bugzilla::User($self->{'assigned_to'}); - return $self->{'assigned_to_obj'}; + return $self->{'assigned_to_obj'} + = new Bugzilla::User({ id => $self->{'assigned_to'}, cache => 1 }); } sub blocked { @@ -3239,7 +3340,8 @@ sub component_obj { my ($self) = @_; return $self->{component_obj} if defined $self->{component_obj}; return {} if $self->{error}; - $self->{component_obj} = new Bugzilla::Component($self->{component_id}); + $self->{component_obj} = + new Bugzilla::Component({ id => $self->{component_id}, cache => 1 }); return $self->{component_obj}; } @@ -3278,6 +3380,26 @@ sub depends_on_obj { return $self->{depends_on_obj}; } +sub duplicates { + my $self = shift; + return $self->{duplicates} if exists $self->{duplicates}; + return [] if $self->{error}; + $self->{duplicates} = Bugzilla::Bug->new_from_list($self->duplicate_ids); + return $self->{duplicates}; +} + +sub duplicate_ids { + my $self = shift; + return $self->{duplicate_ids} if exists $self->{duplicate_ids}; + return [] if $self->{error}; + + my $dbh = Bugzilla->dbh; + $self->{duplicate_ids} = + $dbh->selectcol_arrayref('SELECT dupe FROM duplicates WHERE dupe_of = ?', + undef, $self->id); + return $self->{duplicate_ids}; +} + sub flag_types { my ($self) = @_; return $self->{'flag_types'} if exists $self->{'flag_types'}; @@ -3286,7 +3408,8 @@ sub flag_types { my $vars = { target_type => 'bug', product_id => $self->{product_id}, component_id => $self->{component_id}, - bug_id => $self->bug_id }; + bug_id => $self->bug_id, + active_or_has_flags => $self->bug_id }; $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars); return $self->{'flag_types'}; @@ -3337,6 +3460,8 @@ sub comments { } Bugzilla::Comment->preload($self->{'comments'}); } + return unless defined wantarray; + my @comments = @{ $self->{'comments'} }; my $order = $params->{order} @@ -3387,7 +3512,8 @@ sub product { sub product_obj { my $self = shift; return {} if $self->{error}; - $self->{product_obj} ||= new Bugzilla::Product($self->{product_id}); + $self->{product_obj} ||= + new Bugzilla::Product({ id => $self->{product_id}, cache => 1 }); return $self->{product_obj}; } @@ -3397,7 +3523,8 @@ sub qa_contact { return undef if $self->{'error'}; if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) { - $self->{'qa_contact_obj'} = new Bugzilla::User($self->{'qa_contact'}); + $self->{'qa_contact_obj'} + = new Bugzilla::User({ id => $self->{'qa_contact'}, cache => 1 }); } else { # XXX - This is somewhat inconsistent with the assignee/reporter # methods, which will return an empty User if they get a 0. @@ -3411,8 +3538,8 @@ sub reporter { my ($self) = @_; return $self->{'reporter'} if exists $self->{'reporter'}; $self->{'reporter_id'} = 0 if $self->{'error'}; - $self->{'reporter'} = new Bugzilla::User($self->{'reporter_id'}); - return $self->{'reporter'}; + return $self->{'reporter'} + = new Bugzilla::User({ id => $self->{'reporter_id'}, cache => 1 }); } sub see_also { @@ -3553,6 +3680,49 @@ sub groups { } } + # BMO: if required, hack in groups exposed by -visible membership + # (eg mozilla-corporation-confidential-visible), so reporters can add the + # bug to a group on show_bug. + # if the bug is already in the group, the user will not be able to remove + # it unless they are a true group member. + my $user = Bugzilla->user; + if ($self->{'reporter_id'} == $user->id) { + foreach my $group (@{ $user->groups }) { + # map from -visible group to the real one + my $group_name = $group->name; + next unless $group_name =~ s/-visible$//; + next if $user->in_group($group_name); + $group = Bugzilla::Group->new({ name => $group_name, cache => 1 }); + + # only show the group if it's visible to normal members + my ($member_control) = $dbh->selectrow_array( + "SELECT membercontrol + FROM groups + LEFT JOIN group_control_map + ON group_control_map.group_id = groups.id + AND group_control_map.product_id = ? + WHERE groups.id = ?", + undef, + $self->{product_id}, $group->id + ); + + if ( + $member_control + && $member_control == CONTROLMAPSHOWN + && !grep { $_->{bit} == $group->id } @groups) + { + push(@groups, { + bit => $group->id, + name => $group->name, + ison => 0, + ingroup => 1, + mandatory => 0, + description => $group->description, + }); + } + } + } + $self->{'groups'} = \@groups; return $self->{'groups'}; @@ -3671,6 +3841,9 @@ sub editable_bug_fields { # Ensure field exists before attempting to remove it. splice(@fields, $location, 1) if ($location > -1); } + + Bugzilla::Hook::process('bug_editable_bug_fields', { fields => \@fields }); + # Sorted because the old @::log_columns variable, which this replaces, # was sorted. return sort(@fields); @@ -3750,7 +3923,7 @@ sub GetBugActivity { $datepart $attachpart $suppwhere - ORDER BY bugs_activity.bug_when"; + ORDER BY bugs_activity.bug_when, bugs_activity.id"; my $list = $dbh->selectall_arrayref($query, undef, @args); @@ -3805,16 +3978,29 @@ sub GetBugActivity { $changes = []; } + # If this is the same field as the previoius item, then concatenate + # the data into the same change. + if ($operation->{'who'} && $who eq $operation->{'who'} + && $when eq $operation->{'when'} + && $fieldname eq $operation->{'fieldname'} + && ($comment_id || 0) == ($operation->{'comment_id'} || 0) + && ($attachid || 0) == ($operation->{'attachid'} || 0)) + { + my $old_change = pop @$changes; + $removed = _join_activity_entries($fieldname, $old_change->{'removed'}, $removed); + $added = _join_activity_entries($fieldname, $old_change->{'added'}, $added); + } + $operation->{'who'} = $who; $operation->{'when'} = $when; + $operation->{'fieldname'} = $change{'fieldname'} = $fieldname; + $operation->{'attachid'} = $change{'attachid'} = $attachid; - $change{'fieldname'} = $fieldname; - $change{'attachid'} = $attachid; $change{'removed'} = $removed; $change{'added'} = $added; - + if ($comment_id) { - $change{'comment'} = Bugzilla::Comment->new($comment_id); + $operation->{comment_id} = $change{'comment'} = Bugzilla::Comment->new($comment_id); } push (@$changes, \%change); @@ -3829,6 +4015,35 @@ sub GetBugActivity { return(\@operations, $incomplete_data); } +sub _join_activity_entries { + my ($field, $current_change, $new_change) = @_; + # We need to insert characters as these were removed by old + # LogActivityEntry code. + + return $new_change if $current_change eq ''; + + # Buglists and see_also need the comma restored + if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') { + if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { + return $current_change . $new_change; + } else { + return $current_change . ', ' . $new_change; + } + } + + # Assume bug_file_loc contain a single url, don't insert a delimiter + if ($field eq 'bug_file_loc') { + return $current_change . $new_change; + } + + # All other fields get a space + if (substr($new_change, 0, 1) eq ' ') { + return $current_change . $new_change; + } else { + return $current_change . ' ' . $new_change; + } +} + # Update the bugs_activity table to reflect changes made in bugs. sub LogActivityEntry { my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id) = @_; @@ -3843,7 +4058,6 @@ sub LogActivityEntry { my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH); $removestr = substr($removed, 0, $commaposition); $removed = substr($removed, $commaposition); - $removed =~ s/^[,\s]+//; # remove any comma or space } else { $removed = ""; # no more entries } @@ -3851,7 +4065,6 @@ sub LogActivityEntry { my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH); $addstr = substr($added, 0, $commaposition); $added = substr($added, $commaposition); - $added =~ s/^[,\s]+//; # remove any comma or space } else { $added = ""; # no more entries } @@ -3938,8 +4151,8 @@ sub check_can_change_field { return 1; } - # Allow anyone to change comments. - if ($field =~ /^longdesc/) { + # Allow anyone to change comments, or set flags + if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') { return 1; } @@ -4145,6 +4358,7 @@ sub _create_cf_accessors { my $fields = Bugzilla->fields({ custom => 1 }); foreach my $field (@$fields) { + next if $field->type == FIELD_TYPE_EXTENSION; my $accessor = $class->_accessor_for($field); my $name = "${class}::" . $field->name; { @@ -4154,6 +4368,8 @@ sub _create_cf_accessors { } } + Bugzilla::Hook::process('bug_create_cf_accessors'); + Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1; } |