diff options
-rw-r--r-- | Bugzilla/Search.pm | 10 | ||||
-rw-r--r-- | Bugzilla/User.pm | 3 | ||||
-rw-r--r-- | Bugzilla/WebService/Bug.pm | 8 | ||||
-rw-r--r-- | extensions/Review/Extension.pm | 229 | ||||
-rw-r--r-- | extensions/Review/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl | 19 | ||||
-rw-r--r-- | extensions/Review/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl | 50 | ||||
-rw-r--r-- | extensions/Review/template/en/default/hook/bug/show-bug_end.xml.tmpl | 12 | ||||
-rw-r--r-- | skins/custom/create_bug.css | 13 | ||||
-rw-r--r-- | template/en/default/bug/create/create.html.tmpl | 12 | ||||
-rw-r--r-- | template/en/default/search/form.html.tmpl | 6 |
10 files changed, 287 insertions, 75 deletions
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index 519ebe69f..356af1870 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -384,6 +384,9 @@ sub SPECIAL_PARSING { # last_visit field that accept both a 1d, 1w, 1m, 1y format and the # %last_changed% pronoun. last_visit_ts => \&_last_visit_datetime, + + # BMO - Add ability to use pronoun for bug mentors field + bug_mentor => \&_commenter_pronoun, }; foreach my $field (Bugzilla->active_custom_fields) { if ($field->type == FIELD_TYPE_DATETIME) { @@ -425,6 +428,11 @@ use constant USER_FIELDS => { field => 'setter_id', join => { table => 'flags' }, }, + # BMO - Ability to search for bugs with specific mentors + 'bug_mentor' => { + field => 'user_id', + join => { table => 'bug_mentors' }, + } }; # Backwards compatibility for times that we changed the names of fields @@ -1701,7 +1709,7 @@ sub _special_parse_email { $type = "anyexact" if $type eq "exact"; my $or_clause = new Bugzilla::Search::Clause('OR'); - foreach my $field (qw(assigned_to reporter cc qa_contact)) { + foreach my $field (qw(assigned_to reporter cc qa_contact bug_mentor)) { if ($params->{"email$field$id"}) { $or_clause->add($field, $type, $email); } diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index f707a8e80..8526c42d7 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -775,6 +775,9 @@ sub is_involved_in_bug { return 1 if $user_id == $bug->qa_contact->id; } + # BMO - Bug mentors are considered involved with the bug + return 1 if $bug->is_mentor($self); + return unless $bug->cc; return any { $user_login eq $_ } @{ $bug->cc }; } diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index a82f55d3b..0a8145d37 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -1376,6 +1376,14 @@ sub _bug_to_hash { $item{'is_creator_accessible'} = $self->type('boolean', $bug->reporter_accessible); } + # BMO - support for special mentors field + if (filter_wants $params, 'mentors') { + $item{'mentors'} + = [ map { $self->type('email', $_->login) } @{ $bug->mentors || [] } ]; + $item{'mentors_detail'} + = [ map { $self->_user_to_hash($_, $params, undef, 'mentors') } @{ $bug->mentors } ]; + } + return \%item; } diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm index 6509218f3..ac7cb7233 100644 --- a/extensions/Review/Extension.pm +++ b/extensions/Review/Extension.pm @@ -18,8 +18,9 @@ use Bugzilla::Error; use Bugzilla::Extension::Review::FlagStateActivity; use Bugzilla::Extension::Review::Util; use Bugzilla::Install::Filesystem; +use Bugzilla::Search; use Bugzilla::User; -use Bugzilla::Util qw(clean_text); +use Bugzilla::Util qw(clean_text diff_arrays); use constant UNAVAILABLE_RE => qr/\b(?:unavailable|pto|away)\b/i; @@ -77,42 +78,19 @@ sub _reviewers_objs { sub _bug_mentors { my ($self) = @_; - # extract the mentors from the status_whiteboard - # when the mentors gets its own field, this will be easier - if (!exists $self->{mentors}) { - my @mentors; - my $whiteboard = $self->status_whiteboard; - my $logout = 0; - while ($whiteboard =~ /\[mentor=([^\]]+)\]/g) { - my $mentor_string = $1; - my $user; - if ($mentor_string =~ /\@/) { - # assume it's a full username if it contains an @ - $user = Bugzilla::User->new({ name => $mentor_string }); - } else { - # otherwise assume it's a : prefixed nick. only works if a - # single user matches. - - # we need to be logged in to do user searching - if (!Bugzilla->user->id) { - Bugzilla->set_user(Bugzilla::User->check({ name => 'nobody@mozilla.org' })); - $logout = 1; - } - - foreach my $query ("*:$mentor_string*", "*$mentor_string*") { - my $matches = Bugzilla::User::match($query, 2); - if ($matches && scalar(@$matches) == 1) { - $user = $matches->[0]; - last; - } - } - } - push @mentors, $user if $user; + my $dbh = Bugzilla->dbh; + if (!$self->{bug_mentors}) { + my $mentor_ids = $dbh->selectcol_arrayref(" + SELECT user_id FROM bug_mentors WHERE bug_id = ?", + undef, + $self->id); + $self->{bug_mentors} = []; + foreach my $mentor_id (@$mentor_ids) { + push(@{ $self->{bug_mentors} }, + Bugzilla::User->new({ id => $mentor_id, cache => 1 })); } - Bugzilla->logout_request() if $logout; - $self->{mentors} = \@mentors; } - return $self->{mentors}; + return [ sort { $a->login cmp $b->login } @{ $self->{bug_mentors} } ]; } sub _bug_is_mentor { @@ -191,8 +169,8 @@ sub object_end_of_create { my ($object, $params) = @$args{qw(object params)}; if ($object->isa('Bugzilla::Product') || $object->isa('Bugzilla::Component')) { - my ($new, $new_users) = _new_reviewers_from_input(); - _update_reviewers($object, [], $new_users); + my ($new, $new_users) = _new_users_from_input('reviewers'); + _update_users($object, [], $new_users); } elsif (_is_countable_flag($object) && $object->requestee_id && $object->status eq '?') { _adjust_request_count($object, +1); @@ -200,6 +178,10 @@ sub object_end_of_create { if (_is_countable_flag($object)) { $self->_log_flag_state_activity($object, $object->status); } + elsif ($object->isa('Bugzilla::Bug')) { + my ($new, $new_users) = _new_users_from_input('bug_mentors'); + _update_users($object, [], $new_users); + } } sub object_end_of_update { @@ -207,10 +189,10 @@ sub object_end_of_update { my ($object, $old_object, $changes) = @$args{qw(object old_object changes)}; if ($object->isa('Bugzilla::Product') || $object->isa('Bugzilla::Component')) { - my ($new, $new_users) = _new_reviewers_from_input(); + my ($new, $new_users) = _new_users_from_input('reviewers'); my $old = $old_object->reviewers(1); if ($old ne $new) { - _update_reviewers($object, $old_object->reviewers_objs(1), $new_users); + _update_users($object, $old_object->reviewers_objs(1), $new_users); $changes->{reviewers} = [ $old ? $old : undef, $new ? $new : undef ]; } } @@ -240,6 +222,19 @@ sub object_end_of_update { _adjust_request_count($object, +1); } } + elsif ($object->isa('Bugzilla::Bug')) { + my ($new, $new_mentors) = _new_users_from_input('bug_mentors'); + my $old = join(", ", map { $_->login } @{ $old_object->mentors }); + if ($old ne $new) { + _update_users($object, $old_object->mentors, $new_mentors); + + my @old_names = map { $_->login } @{ $old_object->mentors }; + my @new_names = map { $_->login } @{ $new_mentors }; + my ($removed, $added) = diff_arrays(\@old_names, \@new_names); + $changes->{bug_mentor} = [ @$removed ? join(", ", @$removed) : undef, + @$added ? join(", ", @$added) : undef ]; + } + } } sub flag_updated { @@ -310,12 +305,13 @@ sub _adjust_request_count { Bugzilla->memcached->clear({ table => 'profiles', id => $requestee_id }); } -sub _new_reviewers_from_input { - if (!Bugzilla->input_params->{reviewers}) { +sub _new_users_from_input { + my $field = shift; + if (!Bugzilla->input_params->{$field}) { return ('', []); } - Bugzilla::User::match_field({ 'reviewers' => {'type' => 'multi'} }); - my $new = Bugzilla->input_params->{reviewers}; + Bugzilla::User::match_field({ $field => {'type' => 'multi'} }); + my $new = Bugzilla->input_params->{$field}; $new = [ $new ] unless ref($new); my $new_users = []; foreach my $login (@$new) { @@ -325,39 +321,64 @@ sub _new_reviewers_from_input { return ($new, $new_users); } -sub _update_reviewers { +sub _update_users { my ($object, $old_users, $new_users) = @_; my $dbh = Bugzilla->dbh; - my $type = $object->isa('Bugzilla::Product') ? 'product' : 'component'; - # remove deleted users - foreach my $old_user (@$old_users) { - if (!grep { $_->id == $old_user->id } @$new_users) { - $dbh->do( - "DELETE FROM ${type}_reviewers WHERE ${type}_id=? AND user_id=?", - undef, - $object->id, $old_user->id, - ); + if ($object->isa('Bugzilla::Bug')) { + # remove deleted users + foreach my $old_user (@$old_users) { + if (!grep { $_->id == $old_user->id } @$new_users) { + $dbh->do( + "DELETE FROM bug_mentors WHERE bug_id = ? AND user_id = ?", + undef, + $object->id, $old_user->id, + ); + } + } + # add new users + foreach my $new_user (@$new_users) { + if (!grep { $_->id == $new_user->id } @$old_users) { + $dbh->do( + "INSERT INTO bug_mentors (bug_id,user_id) VALUES (?, ?)", + undef, + $object->id, $new_user->id, + ); + } } } - # add new users - foreach my $new_user (@$new_users) { - if (!grep { $_->id == $new_user->id } @$old_users) { + else { + my $type = $object->isa('Bugzilla::Product') ? 'product' : 'component'; + + # remove deleted users + foreach my $old_user (@$old_users) { + if (!grep { $_->id == $old_user->id } @$new_users) { + $dbh->do( + "DELETE FROM ${type}_reviewers WHERE ${type}_id=? AND user_id=?", + undef, + $object->id, $old_user->id, + ); + } + } + # add new users + foreach my $new_user (@$new_users) { + if (!grep { $_->id == $new_user->id } @$old_users) { + $dbh->do( + "INSERT INTO ${type}_reviewers(${type}_id,user_id) VALUES (?,?)", + undef, + $object->id, $new_user->id, + ); + } + } + # and update the sortkey for all users + for (my $i = 0; $i < scalar(@$new_users); $i++) { $dbh->do( - "INSERT INTO ${type}_reviewers(${type}_id,user_id) VALUES (?,?)", + "UPDATE ${type}_reviewers SET sortkey=? WHERE ${type}_id=? AND user_id=?", undef, - $object->id, $new_user->id, + ($i + 1) * 10, $object->id, $new_users->[$i]->id, ); } } - # and update the sortkey for all users - for (my $i = 0; $i < scalar(@$new_users); $i++) { - $dbh->do( - "UPDATE ${type}_reviewers SET sortkey=? WHERE ${type}_id=? AND user_id=?", - undef, - ($i + 1) * 10, $object->id, $new_users->[$i]->id, - ); - } } # bugzilla's handling of requestee matching when creating bugs is "if it's @@ -439,6 +460,51 @@ sub flag_end_of_update { } # +# search +# + +sub buglist_columns { + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + my $columns = $args->{columns}; + $columns->{bug_mentor} = { title => 'Mentor' }; + if (Bugzilla->user->id) { + $columns->{bug_mentor}->{name} + = $dbh->sql_group_concat('map_mentors_names.login_name'); + } + else { + $columns->{bug_mentor}->{name} + = $dbh->sql_group_concat('map_mentors_names.realname'); + + } +} + +sub buglist_column_joins { + my ($self, $args) = @_; + my $column_joins = $args->{column_joins}; + $column_joins->{bug_mentor} = { + as => 'map_mentors', + table => 'bug_mentors', + then_to => { + as => 'map_mentors_names', + table => 'profiles', + from => 'map_mentors.user_id', + to => 'userid', + }, + }, +} + +sub search_operator_field_override { + my ($self, $args) = @_; + my $operators = $args->{operators}; + $operators->{bug_mentor} = { + _non_changed => sub { + Bugzilla::Search::_user_nonchanged(@_) + } + }; +} + +# # web service / pages # @@ -700,6 +766,36 @@ sub db_schema_abstract_schema { bug_mentors_bug_id_idx => [ 'bug_id' ], ], }; + + $args->{'schema'}->{'bug_mentors'} = { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'bugs', + COLUMN => 'bug_id', + DELETE => 'CASCADE', + }, + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'CASCADE', + } + }, + ], + INDEXES => [ + bug_mentors_idx => { + FIELDS => [ 'bug_id', 'user_id' ], + TYPE => 'UNIQUE', + }, + bug_mentors_bug_id_idx => [ 'bug_id' ], + ], + }; } sub install_update_db { @@ -720,6 +816,7 @@ sub install_update_db { 'profiles', 'needinfo_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 } ); + my $field = Bugzilla::Field->new({ name => 'bug_mentor' }); if (!$field) { Bugzilla::Field->create({ diff --git a/extensions/Review/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl b/extensions/Review/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl new file mode 100644 index 000000000..6d890d975 --- /dev/null +++ b/extensions/Review/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl @@ -0,0 +1,19 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +<tr> + <th class="field_label">Mentors:</th> + <td colspan="3" class="field_value"> + [% INCLUDE global/userselect.html.tmpl + id = "bug_mentors" + name = "bug_mentors" + size = 30 + multiple = 5 + %] + </td> +</tr> diff --git a/extensions/Review/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/Review/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl new file mode 100644 index 000000000..352dfa6c7 --- /dev/null +++ b/extensions/Review/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl @@ -0,0 +1,50 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% mentor_logins = [] %] +[% FOREACH mentor = bug.mentors %] + [% mentor_logins.push(mentor.login) %] +[% END %] +<tr> + <th class="field_label">Mentors:</th> + <td> + [% IF bug.check_can_change_field("bug_mentors", 0, 1) %] + <div id="bz_bug_mentors_edit_container" class="bz_default_hidden"> + <span> + [% FOREACH mentor = bug.mentors %] + [% INCLUDE global/user.html.tmpl who = mentor %]<br> + [% END %] + (<a href="#" id="bz_bug_mentors_edit_action">edit</a>) + </span> + </div> + <div id="bz_bug_mentors_input"> + [% INCLUDE global/userselect.html.tmpl + id = "bug_mentors" + name = "bug_mentors" + value = mentor_logins.join(", ") + classes = ["bz_userfield"] + size = 30 + multiple = 5 + %] + <br> + </div> + <script type="text/javascript"> + hideEditableField('bz_bug_mentors_edit_container', + 'bz_bug_mentors_input', + 'bz_bug_mentors_edit_action', + 'bug_mentors', + '[% mentor_logins.join(", ") FILTER js %]' ); + </script> + [% ELSE %] + [% FOREACH mentor = bug.mentors %] + [% INCLUDE global/user.html.tmpl who = mentor %]<br> + [% END %] + [% END %] + </td> +</tr> + diff --git a/extensions/Review/template/en/default/hook/bug/show-bug_end.xml.tmpl b/extensions/Review/template/en/default/hook/bug/show-bug_end.xml.tmpl new file mode 100644 index 000000000..9ad650b2f --- /dev/null +++ b/extensions/Review/template/en/default/hook/bug/show-bug_end.xml.tmpl @@ -0,0 +1,12 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% FOREACH mentor = bug.mentors %] + <mentor name="[% mentor.name FILTER xml %]"> + [% mentor.login FILTER email FILTER xml %]</mentor> +[% END %] diff --git a/skins/custom/create_bug.css b/skins/custom/create_bug.css index b3088048d..1ca28a101 100644 --- a/skins/custom/create_bug.css +++ b/skins/custom/create_bug.css @@ -1,3 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. */ + .tracking_flags .field_label a { font-weight: normal !important; @@ -32,3 +39,9 @@ #bug_create_warning_text { margin-left: 42px; } + +#keyword_container, +#bug_mentors_autocomplete, +#container_see_also { + width: 25em +} diff --git a/template/en/default/bug/create/create.html.tmpl b/template/en/default/bug/create/create.html.tmpl index d41dc9d35..2e3ba66c6 100644 --- a/template/en/default/bug/create/create.html.tmpl +++ b/template/en/default/bug/create/create.html.tmpl @@ -633,10 +633,11 @@ TUI_hide_default('attachment_text_field'); [% IF Param('use_see_also') %] <tr> [% INCLUDE bug/field.html.tmpl - bug = default - field = bug_fields.see_also - editable = 1 - value = see_also + bug = default + field = bug_fields.see_also + editable = 1 + value = see_also + value_span = 3 %] </tr> [% END %] @@ -656,9 +657,8 @@ TUI_hide_default('attachment_text_field'); value_span = 3 %] </tr> [% END %] -</tbody> - [% Hook.process('after_custom_fields') %] +</tbody> [% display_flags = 0 %] [% any_flags_requesteeble = 0 %] diff --git a/template/en/default/search/form.html.tmpl b/template/en/default/search/form.html.tmpl index 5a97dd4d3..1b9b8310f 100644 --- a/template/en/default/search/form.html.tmpl +++ b/template/en/default/search/form.html.tmpl @@ -330,6 +330,8 @@ TUI_hide_default('information_query'); label=> "a ${field_descs.cc} list member" } %] [% PROCESS role_types field = { count => n, name => "emaillongdesc", label=> " a ${field_descs.commenter}" } %] + [% PROCESS role_types field = { count => n, name => "emailbug_mentor", + label => " a ${field_descs.bug_mentor}" } %] <select name="emailtype[% n %]"> [% FOREACH qv = [ { name => "substring", description => "contains" }, @@ -357,8 +359,8 @@ TUI_hide_default('information_query'); [% END %] </div> [% END %] - [% Hook.process('email_numbering_end') %] - </div> + [% Hook.process('email_numbering_end') %] + </div> [%# *** Bug Changes *** %] <div class="bz_section_title" id="history_filter"> <div id="history_query_controller" class="arrow">▼</div> |