summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorByron Jones <bjones@mozilla.com>2013-07-18 09:07:57 +0200
committerByron Jones <bjones@mozilla.com>2013-07-18 09:07:57 +0200
commitcc20ecfa1b8c151690e8d12c8ad5c544fa1a4a5a (patch)
tree3202a2614139d143131fa5b900a928ec64ce6be3
parent1551a42f3337482bd6aaa1493d987e05d704f128 (diff)
downloadbugzilla-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)
-rw-r--r--extensions/FlagDefaultRequestee/Extension.pm16
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/global/user-error-errors.html.tmpl13
-rw-r--r--extensions/Review/Extension.pm247
-rw-r--r--extensions/Review/template/en/default/hook/admin/components/edit-common-rows.html.tmpl22
-rw-r--r--extensions/Review/template/en/default/hook/admin/products/edit-common-rows.html.tmpl28
-rw-r--r--extensions/Review/template/en/default/hook/admin/products/updated-changes.html.tmpl19
-rw-r--r--extensions/Review/template/en/default/hook/attachment/create-end.html.tmpl20
-rw-r--r--extensions/Review/template/en/default/hook/attachment/edit-end.html.tmpl15
-rw-r--r--extensions/Review/template/en/default/hook/bug/create/create-end.html.tmpl16
-rw-r--r--extensions/Review/template/en/default/hook/flag/list-requestee.html.tmpl17
-rw-r--r--extensions/Review/template/en/default/hook/global/header-start.html.tmpl81
-rw-r--r--extensions/Review/template/en/default/hook/global/user-error-errors.html.tmpl12
-rw-r--r--extensions/Review/web/js/review.js184
-rw-r--r--extensions/Review/web/styles/review.css10
-rwxr-xr-xpost_bug.cgi1
-rw-r--r--skins/standard/attachment.css4
-rw-r--r--template/en/default/flag/list.html.tmpl1
-rw-r--r--template/en/default/global/userselect.html.tmpl2
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">
+ &nbsp;&nbsp;(<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 %]