diff options
author | Byron Jones <bjones@mozilla.com> | 2013-07-18 09:07:57 +0200 |
---|---|---|
committer | Byron Jones <bjones@mozilla.com> | 2013-07-18 09:07:57 +0200 |
commit | cc20ecfa1b8c151690e8d12c8ad5c544fa1a4a5a (patch) | |
tree | 3202a2614139d143131fa5b900a928ec64ce6be3 | |
parent | 1551a42f3337482bd6aaa1493d987e05d704f128 (diff) | |
download | bugzilla-cc20ecfa1b8c151690e8d12c8ad5c544fa1a4a5a.tar.gz bugzilla-cc20ecfa1b8c151690e8d12c8ad5c544fa1a4a5a.tar.xz |
Bug 804708: Add a 'Review' extension to customise the review flag for Mozilla's workflow (make requestee/reviewer mandatory, provide review suggestions, etc)
18 files changed, 701 insertions, 7 deletions
diff --git a/extensions/FlagDefaultRequestee/Extension.pm b/extensions/FlagDefaultRequestee/Extension.pm index b444bce49..9c15f741a 100644 --- a/extensions/FlagDefaultRequestee/Extension.pm +++ b/extensions/FlagDefaultRequestee/Extension.pm @@ -10,6 +10,7 @@ package Bugzilla::Extension::FlagDefaultRequestee; use strict; use base qw(Bugzilla::Extension); +use Bugzilla::Error; use Bugzilla::FlagType; use Bugzilla::User; @@ -94,29 +95,32 @@ sub template_before_process { sub flagtype_end_of_create { my ($self, $args) = @_; - _set_default_requestee($args->{id}); + _set_default_requestee($args->{type}); } sub flagtype_end_of_update { my ($self, $args) = @_; - _set_default_requestee($args->{id}); + _set_default_requestee($args->{type}); } sub _set_default_requestee { - my $type_id = shift; - my $input = Bugzilla->input_params; - my $dbh = Bugzilla->dbh; + my $type = shift; + my $input = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; my $requestee_login = $input->{'default_requestee'}; my $requestee_id = undef; if ($requestee_login) { + if ($type->name eq 'review') { + ThrowUserError("flag_default_requestee_review"); + } my $requestee = Bugzilla::User->check($requestee_login); $requestee_id = $requestee->id; } $dbh->do("UPDATE flagtypes SET default_requestee = ? WHERE id = ?", - undef, $requestee_id, $type_id); + undef, $requestee_id, $type->id); } ################## diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..3fbd0458c --- /dev/null +++ b/extensions/FlagDefaultRequestee/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,13 @@ +[%# 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. + #%] + +[% IF error == "flag_default_requestee_review" %] + [% title = "Review flag not supported" %] + You cannot use the 'Default Requestee' field for the review flag. + Instead set the 'Suggested Reviewers' on the Product or Component edit forms. +[% END %] diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm index cb7aa74e9..d4e9ae761 100644 --- a/extensions/Review/Extension.pm +++ b/extensions/Review/Extension.pm @@ -13,6 +13,253 @@ use base qw(Bugzilla::Extension); our $VERSION = '1'; use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::User; +use Bugzilla::Util qw(clean_text); + +# +# monkey-patched methods +# + +BEGIN { + *Bugzilla::Product::reviewers = \&_product_reviewers; + *Bugzilla::Product::reviewers_objs = \&_product_reviewers_objs; + *Bugzilla::Product::reviewer_required = \&_product_reviewer_required; + *Bugzilla::Component::reviewers = \&_component_reviewers; + *Bugzilla::Component::reviewers_objs = \&_component_reviewers_objs; + *Bugzilla::Bug::mentor = \&_bug_mentor; +} + +# +# reviewers +# + +sub _product_reviewers { _reviewers($_[0], 'product') } +sub _product_reviewers_objs { _reviewers_objs($_[0], 'product') } +sub _component_reviewers { _reviewers($_[0], 'component') } +sub _component_reviewers_objs { _reviewers_objs($_[0], 'component') } + +sub _reviewers { + my ($object, $type) = @_; + return join(', ', map { $_->login } @{ _reviewers_objs($object, $type) }); +} + +sub _reviewers_objs { + my ($object, $type) = @_; + if (!$object->{reviewers}) { + my $dbh = Bugzilla->dbh; + my $user_ids = $dbh->selectcol_arrayref( + "SELECT user_id FROM ${type}_reviewers WHERE ${type}_id = ? ORDER BY sortkey", + undef, + $object->id, + ); + # new_from_list always sorts according to the object's definition, + # so we have to reorder the list + my $users = Bugzilla::User->new_from_list($user_ids); + my %user_map = map { $_->id => $_ } @$users; + my @reviewers = map { $user_map{$_} } @$user_ids; + $object->{reviewers} = \@reviewers; + } + return $object->{reviewers}; +} + +sub _bug_mentor { + my ($self) = @_; + # extract the mentor from the status_whiteboard + # when the mentor gets its own field, this will be easier + if (!exists $self->{mentor}) { + my $mentor; + if ($self->status_whiteboard =~ /\[mentor=([^\]]+)\]/) { + my $mentor_string = $1; + if ($mentor_string =~ /\@/) { + # assume it's a full username if it contains an @ + $mentor = Bugzilla::User->new({ name => $mentor_string }); + } else { + # otherwise assume it's a : prefixed nick. only works if a + # single user matches. + my $matches = Bugzilla::User::match("*:$mentor_string*", 2); + if ($matches && scalar(@$matches) == 1) { + $mentor = $matches->[0]; + } + } + } + $self->{mentor} = $mentor; + } + return $self->{mentor}; +} + +# +# reviewer-required +# + +sub _product_reviewer_required { $_[0]->{reviewer_required} } + +sub object_columns { + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Product')) { + push @$columns, 'reviewer_required'; + } +} + +sub object_update_columns { + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::Product')) { + push @$columns, 'reviewer_required'; + } +} + +# +# create/update +# + +sub object_before_create { + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + return unless $class->isa('Bugzilla::Product'); + + $params->{reviewer_required} = Bugzilla->cgi->param('reviewer_required') ? 1 : 0; +} + +sub object_end_of_set_all { + my ($self, $args) = @_; + my ($object, $params) = @$args{qw(object params)}; + return unless $object->isa('Bugzilla::Product'); + + $object->set('reviewer_required', Bugzilla->cgi->param('reviewer_required') ? 1 : 0); +} + +sub object_end_of_create { + my ($self, $args) = @_; + my ($object, $params) = @$args{qw(object params)}; + return unless $object->isa('Bugzilla::Product') || $object->isa('Bugzilla::Component');; + + my ($new, $new_users) = _new_reviewers_from_input(); + _update_reviewers($object, [], $new_users); +} + +sub object_end_of_update { + my ($self, $args) = @_; + my ($object, $old_object, $changes) = @$args{qw(object old_object changes)}; + return unless $object->isa('Bugzilla::Product') || $object->isa('Bugzilla::Component');; + + my ($new, $new_users) = _new_reviewers_from_input(); + my $old = $old_object->reviewers; + if ($old ne $new) { + _update_reviewers($object, $old_object->reviewers_objs, $new_users); + $changes->{reviewers} = [ $old ? $old : undef, $new ? $new : undef ]; + } +} + +sub _new_reviewers_from_input { + if (!Bugzilla->input_params->{reviewers}) { + return (undef, []); + } + Bugzilla::User::match_field({ 'reviewers' => {'type' => 'multi'} }); + my $new = Bugzilla->input_params->{reviewers}; + $new = [ $new ] unless ref($new); + my $new_users = []; + foreach my $login (@$new) { + push @$new_users, Bugzilla::User->check($login); + } + $new = join(', ', @$new); + return ($new, $new_users); +} + +sub _update_reviewers { + 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, + ); + } + } + # 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( + "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 +# wrong, or matches too many, default to empty", which breaks mandatory +# reviewer requirements. instead we just throw an error. +sub post_bug_attachment_flags { + my ($self, $args) = @_; + my $bug = $args->{bug}; + my $cgi = Bugzilla->cgi; + + # extract the set flag-types + my @flagtype_ids = map { /^flag_type-(\d+)$/ ? $1 : () } $cgi->param(); + @flagtype_ids = grep { $cgi->param("flag_type-$_") ne 'X' } @flagtype_ids; + return unless scalar(@flagtype_ids); + + # find valid review flagtypes + my $flag_types = Bugzilla::FlagType::match({ + product_id => $bug->product_id, + component_id => $bug->component_id, + is_active => 1 + }); + foreach my $flag_type (@$flag_types) { + next unless $flag_type->name eq 'review' + && $flag_type->target_type eq 'attachment'; + my $type_id = $flag_type->id; + next unless scalar(grep { $_ == $type_id } @flagtype_ids); + + my $reviewers = clean_text($cgi->param("requestee_type-$type_id") || ''); + if ($reviewers eq '' && $bug->product_obj->reviewer_required) { + ThrowUserError('reviewer_required'); + } + + foreach my $reviewer (split(/[,;]+/, $reviewers)) { + # search on the reviewer + my $users = Bugzilla::User::match($reviewer, 2, 1); + + # no matches + if (scalar(@$users) == 0) { + ThrowUserError('user_match_failed', { name => $reviewer }); + } + + # more than one match, throw error + if (scalar(@$users) > 1) { + ThrowUserError('user_match_too_many', { fields => [ 'review' ] }); + } + } + } +} + +sub flag_end_of_update { + my ($self, $args) = @_; + my ($attachment, $changes) = @$args{qw(object changes)}; + if (exists $changes->{'flag.review'} + && $changes->{'flag.review'}->[1] eq '?' + && $attachment->bug->product_obj->reviewer_required) + { + ThrowUserError('reviewer_required'); + } +} # # installation diff --git a/extensions/Review/template/en/default/hook/admin/components/edit-common-rows.html.tmpl b/extensions/Review/template/en/default/hook/admin/components/edit-common-rows.html.tmpl new file mode 100644 index 000000000..befb183b2 --- /dev/null +++ b/extensions/Review/template/en/default/hook/admin/components/edit-common-rows.html.tmpl @@ -0,0 +1,22 @@ +[%# 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 align="right">Suggested Reviewers:</th> + <td> + [% INCLUDE global/userselect.html.tmpl + id => "reviewers" + name => "reviewers" + value => comp.reviewers + size => 64 + emptyok => 1 + title => "One or more email address (comma delimited)" + placeholder => product.reviewers + %] + </td> +</tr> diff --git a/extensions/Review/template/en/default/hook/admin/products/edit-common-rows.html.tmpl b/extensions/Review/template/en/default/hook/admin/products/edit-common-rows.html.tmpl new file mode 100644 index 000000000..da41f7dde --- /dev/null +++ b/extensions/Review/template/en/default/hook/admin/products/edit-common-rows.html.tmpl @@ -0,0 +1,28 @@ +[%# 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 align="right">Reviewer required:</th> + <td> + <input type="checkbox" name="reviewer_required" value="1" + [% "checked" IF product.reviewer_required %]> + </td> +</tr> +<tr> + <th align="right">Suggested Reviewers:</th> + <td> + [% INCLUDE global/userselect.html.tmpl + id => "reviewers" + name => "reviewers" + value => product.reviewers + size => 64 + emptyok => 1 + title => "One or more email address (comma delimited)" + %] + </td> +</tr> diff --git a/extensions/Review/template/en/default/hook/admin/products/updated-changes.html.tmpl b/extensions/Review/template/en/default/hook/admin/products/updated-changes.html.tmpl new file mode 100644 index 000000000..667848281 --- /dev/null +++ b/extensions/Review/template/en/default/hook/admin/products/updated-changes.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. + #%] + +[% IF changes.reviewers.defined %] + <p> + Updated suggested reviewers from '[% changes.reviewers.0 FILTER html %]' to + '[% product.reviewers FILTER html %]'. + </p> +[% END %] +[% IF changes.reviewer_required.defined %] + <p> + [% changes.reviewer_required.1 ? "Enabled" : "Disabled" %] 'review required'. + </p> +[% END %] diff --git a/extensions/Review/template/en/default/hook/attachment/create-end.html.tmpl b/extensions/Review/template/en/default/hook/attachment/create-end.html.tmpl new file mode 100644 index 000000000..c8e2a68a1 --- /dev/null +++ b/extensions/Review/template/en/default/hook/attachment/create-end.html.tmpl @@ -0,0 +1,20 @@ +[%# 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. + #%] + +[% UNLESS bug %] + [% bug = attachment.bug %] +[% END %] + +[% IF bug.product_obj.reviewer_required %] +<script> + YAHOO.util.Event.onDOMReady(function() { + REVIEW.init_mandatory(); + REVIEW.init_create_attachment(); + }); +</script> +[% END %] diff --git a/extensions/Review/template/en/default/hook/attachment/edit-end.html.tmpl b/extensions/Review/template/en/default/hook/attachment/edit-end.html.tmpl new file mode 100644 index 000000000..bc6230d1c --- /dev/null +++ b/extensions/Review/template/en/default/hook/attachment/edit-end.html.tmpl @@ -0,0 +1,15 @@ +[%# 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. + #%] + +[% IF attachment.bug.product_obj.reviewer_required %] +<script> +YAHOO.util.Event.onDOMReady(function() { + REVIEW.init_mandatory(); +}); +</script> +[% END %] diff --git a/extensions/Review/template/en/default/hook/bug/create/create-end.html.tmpl b/extensions/Review/template/en/default/hook/bug/create/create-end.html.tmpl new file mode 100644 index 000000000..a59cef950 --- /dev/null +++ b/extensions/Review/template/en/default/hook/bug/create/create-end.html.tmpl @@ -0,0 +1,16 @@ +[%# 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. + #%] + +<script> + YAHOO.util.Event.onDOMReady(function() { + [% IF product.reviewer_required %] + REVIEW.init_mandatory(); + [% END %] + REVIEW.init_enter_bug(); + }); +</script> diff --git a/extensions/Review/template/en/default/hook/flag/list-requestee.html.tmpl b/extensions/Review/template/en/default/hook/flag/list-requestee.html.tmpl new file mode 100644 index 000000000..a3f0e8a44 --- /dev/null +++ b/extensions/Review/template/en/default/hook/flag/list-requestee.html.tmpl @@ -0,0 +1,17 @@ +[%# 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. + #%] + +[% RETURN UNLESS type.name == 'review' %] + +<span id="[% fid FILTER none %]_suggestions" class="bz_default_hidden"> + (<a href="#" id="[% fid FILTER none %]_suggestions_link">suggested reviewers</a>) +</span> + +<script> + REVIEW.init_review_flag('[% fid FILTER none %]', '[% flag_name FILTER none %]'); +</script> diff --git a/extensions/Review/template/en/default/hook/global/header-start.html.tmpl b/extensions/Review/template/en/default/hook/global/header-start.html.tmpl new file mode 100644 index 000000000..6063aa920 --- /dev/null +++ b/extensions/Review/template/en/default/hook/global/header-start.html.tmpl @@ -0,0 +1,81 @@ +[%# 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. + #%] + +[% RETURN UNLESS template.name == 'attachment/edit.html.tmpl' + || template.name == 'attachment/create.html.tmpl' + || template.name == 'attachment/diff-header.html.tmpl' + || template.name == 'bug/create/create.html.tmpl' %] + +[% style_urls.push('extensions/Review/web/style/review.css') %] +[% javascript_urls.push('extensions/Review/web/js/review.js') %] + +[% IF bug %] + [%# create attachment %] + [% mentor = bug.mentor %] + [% product_obj = bug.product_obj %] + [% component_obj = bug.component_obj %] +[% ELSIF attachment.bug %] + [%# edit attachment %] + [% mentor = attachment.bug.mentor %] + [% product_obj = attachment.bug.product_obj %] + [% component_obj = attachment.bug.component_obj %] +[% ELSE %] + [%# create bug %] + [% mentor = '' %] + [% product_obj = product %] + [% component_obj = 0 %] +[% END %] + +[% review_js = BLOCK %] + review_suggestions = { + [% IF mentor %] + _mentor: [% PROCESS reviewer u = mentor %], + [% END %] + + [% IF product_obj.reviewers %] + _product: [ + [% FOREACH u = product_obj.reviewers_objs %] + [% PROCESS reviewer %][% "," UNLESS loop.last %] + [% END %] + ], + [% END %] + + [% IF component_obj %] + [%# single component (create/edit attachment) %] + '[% component_obj.name FILTER js %]': [ + [% FOREACH u = component_obj.reviewers_objs %] + [% PROCESS reviewer %][% "," UNLESS loop.last %] + [% END %] + ], + [% ELSE %] + [%# all components (create bug) %] + [% FOREACH c = product_obj.components %] + [% NEXT UNLESS c.reviewers %] + '[% c.name FILTER js %]': [ + [% FOREACH u = c.reviewers_objs %] + [% PROCESS reviewer %][% "," UNLESS loop.last %] + [% END %] + ], + [% END %] + [% END %] + + [%# to keep IE happy, no trailing commas %] + _end: 1 + }; + + [% IF component_obj %] + static_component = '[% component_obj.name FILTER js %]'; + [% ELSE %] + static_component = false; + [% END %] +[% END %] +[% javascript = javascript _ review_js %] + +[% BLOCK reviewer %] + { login: '[% u.login FILTER js%]', identity: '[% u.identity FILTER js %]' } +[% END %] diff --git a/extensions/Review/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Review/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..4265f4b32 --- /dev/null +++ b/extensions/Review/template/en/default/hook/global/user-error-errors.html.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. + #%] + +[% IF error == "reviewer_required" %] + [% title = "Reviewer Required" %] + You must provide a reviewer for review requests. +[% END %] diff --git a/extensions/Review/web/js/review.js b/extensions/Review/web/js/review.js new file mode 100644 index 000000000..3ba00487d --- /dev/null +++ b/extensions/Review/web/js/review.js @@ -0,0 +1,184 @@ +/* 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. */ + +var Dom = YAHOO.util.Dom; +var Event = YAHOO.util.Event; + +var REVIEW = { + widget: false, + target: false, + fields: [], + use_error_for: false, + + init_review_flag: function(fid, flag_name) { + var idx = this.fields.push({ 'fid': fid, 'flag_name': flag_name, 'component': '' }) - 1; + this.flag_change(false, idx); + Event.addListener(fid, 'change', this.flag_change, idx); + }, + + init_mandatory: function() { + var form = this.find_form(); + if (!form) return; + Event.addListener(form, 'submit', this.check_mandatory); + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i]; + // existing reviews that have empty requestee shouldn't force a + // reviewer to be selected + field.old_empty_review = Dom.get(field.fid).value == '?' + && Dom.get(field.flag_name).value == ''; + if (!field.old_empty_review) + Dom.addClass(field.flag_name, 'required'); + } + }, + + init_enter_bug: function() { + Event.addListener('component', 'change', REVIEW.component_change); + BUGZILLA.string['reviewer_required'] = 'A reviewer is required.'; + this.use_error_for = true; + this.init_create_attachment(); + }, + + init_create_attachment: function() { + Event.addListener('data', 'change', REVIEW.attachment_change); + }, + + component_change: function() { + for (var i = 0; i < REVIEW.fields.length; i++) { + REVIEW.flag_change(false, i); + } + }, + + attachment_change: function() { + var filename = Dom.get('data').value.split('/').pop().split('\\').pop(); + var description = Dom.get('description'); + if (description.value == '') { + description.value = filename; + } + Dom.get('ispatch').checked = + REVIEW.endsWith(filename, '.diff') || REVIEW.endsWith(filename, '.patch'); + bz_fireEvent(Dom.get('ispatch'), 'change'); + description.select(); + description.focus(); + }, + + flag_change: function(e, field_idx) { + var field = REVIEW.fields[field_idx]; + var suggestions_span = Dom.get(field.fid + '_suggestions'); + + // for requests only + if (Dom.get(field.fid).value != '?') { + Dom.addClass(suggestions_span, 'bz_default_hidden'); + return; + } + + // find selected component + var component = static_component || Dom.get('component').value; + if (!component) { + Dom.addClass(suggestions_span, 'bz_default_hidden'); + return; + } + + // init menu and events + if (!field.menu) { + field.menu = new YAHOO.widget.Menu(field.fid + '_menu'); + field.menu.render(document.body); + field.menu.subscribe('click', REVIEW.suggestion_click); + Event.addListener(field.fid + '_suggestions_link', 'click', REVIEW.suggestions_click, field_idx) + } + + // build review list + if (field.component != component) { + field.menu.clearContent(); + if (review_suggestions._mentor) { + REVIEW.add_menu_item(field_idx, review_suggestions._mentor, true); + } + if (review_suggestions[component] && review_suggestions[component].length) { + REVIEW.add_menu_items(field_idx, review_suggestions[component]); + } else if (review_suggestions._product) { + REVIEW.add_menu_items(field_idx, review_suggestions._product); + } + field.menu.render(); + field.component = component; + } + + // show (or hide) the menu + if (field.menu.getItem(0)) { + Dom.removeClass(suggestions_span, 'bz_default_hidden'); + } else { + Dom.addClass(suggestions_span, 'bz_default_hidden'); + } + }, + + add_menu_item: function(field_idx, user, is_mentor) { + var menu = REVIEW.fields[field_idx].menu; + var item = menu.addItem( + { text: user.identity, url: '#' + user.login } + ); + if (is_mentor) + item.cfg.setProperty('classname', 'mentor'); + }, + + add_menu_items: function(field_idx, users) { + for (var i = 0; i < users.length; i++) { + if (!review_suggestions._mentor + || users[i].login != review_suggestions._mentor.login) + { + REVIEW.add_menu_item(field_idx, users[i]); + } + } + }, + + suggestions_click: function(e, field_idx) { + var field = REVIEW.fields[field_idx]; + field.menu.cfg.setProperty('xy', Event.getXY(e)); + field.menu.show(); + Event.stopEvent(e); + REVIEW.target = field.flag_name; + }, + + suggestion_click: function(type, args) { + if (args[1]) { + Dom.get(REVIEW.target).value = decodeURIComponent(args[1].cfg.getProperty('url')).substr(1); + } + Event.stopEvent(args[0]); + }, + + check_mandatory: function(e) { + if (Dom.get('data') && !Dom.get('data').value + && Dom.get('attach_text') && !Dom.get('attach_text').value) + { + return; + } + for (var i = 0; i < REVIEW.fields.length; i++) { + var field = REVIEW.fields[i]; + if (!field.old_empty_review + && Dom.get(field.fid).value == '?' + && Dom.get(field.flag_name).value == '') + { + if (REVIEW.use_error_for) { + _errorFor(Dom.get(REVIEW.fields[i].flag_name), 'reviewer'); + } else { + alert('You must provide a reviewer for review requests.'); + } + Event.stopEvent(e); + } + } + }, + + find_form: function() { + for (var i = 0; i < document.forms.length; i++) { + var action = document.forms[i].getAttribute('action'); + if (action == 'attachment.cgi' || action == 'post_bug.cgi') + return document.forms[i]; + } + return false; + }, + + endsWith: function(str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; + } +}; diff --git a/extensions/Review/web/styles/review.css b/extensions/Review/web/styles/review.css new file mode 100644 index 000000000..9f5b63603 --- /dev/null +++ b/extensions/Review/web/styles/review.css @@ -0,0 +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. */ + +.mentor { + font-weight: bold; +} diff --git a/post_bug.cgi b/post_bug.cgi index af8c2cd2e..839751b58 100755 --- a/post_bug.cgi +++ b/post_bug.cgi @@ -201,6 +201,7 @@ if (defined($cgi->upload('data')) || $cgi->param('attach_text')) { if ($attachment) { # Set attachment flags. + Bugzilla::Hook::process('post_bug_attachment_flags', { bug => $bug }); my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi( $bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR); $attachment->set_flags($flags, $new_flags); diff --git a/skins/standard/attachment.css b/skins/standard/attachment.css index 55e62f2b0..01c4311d4 100644 --- a/skins/standard/attachment.css +++ b/skins/standard/attachment.css @@ -30,7 +30,9 @@ table.attachment_entry td { } table#flags th, -table#flags td { +table#flags td, +table#attachment_flags th, +table#attachment_flags td { text-align: left; vertical-align: baseline; font-size: small; diff --git a/template/en/default/flag/list.html.tmpl b/template/en/default/flag/list.html.tmpl index 8de9955ea..ecc919a38 100644 --- a/template/en/default/flag/list.html.tmpl +++ b/template/en/default/flag/list.html.tmpl @@ -172,6 +172,7 @@ classes => ["requestee"] custom_userlist => grant_list %] + [% Hook.process("requestee", "flag/list.html.tmpl") %] </span> [% END %] </td> diff --git a/template/en/default/global/userselect.html.tmpl b/template/en/default/global/userselect.html.tmpl index 1d0395043..d7b4786f9 100644 --- a/template/en/default/global/userselect.html.tmpl +++ b/template/en/default/global/userselect.html.tmpl @@ -30,6 +30,7 @@ # multiple: optional, do multiselect box, value is size (height) of box # custom_userlist: optional, specify a limited list of users to use # field_title: optional, extra information to display as a tooltip + # placeholder: optional, input only; placeholder attribute value #%] [% IF Param("usemenuforusers") %] @@ -92,6 +93,7 @@ [% IF accesskey %] accesskey="[% accesskey FILTER html %]" [% END %] [% IF field_title %] title="[% field_title FILTER html %]" [% END %] [% IF size %] size="[% size FILTER html %]" [% END %] + [% IF placeholder %] placeholder="[% placeholder FILTER html %]" [% END %] [% IF id %] id="[% id FILTER html %]" [% END %] > [% IF feature_enabled('jsonrpc') && Param('ajax_user_autocompletion') && id %] |