diff options
Diffstat (limited to 'extensions/Review')
23 files changed, 1565 insertions, 0 deletions
diff --git a/extensions/Review/Config.pm b/extensions/Review/Config.pm new file mode 100644 index 000000000..f7da458af --- /dev/null +++ b/extensions/Review/Config.pm @@ -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. + +package Bugzilla::Extension::Review; +use strict; + +use constant NAME => 'Review'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; + +__PACKAGE__->NAME; diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm new file mode 100644 index 000000000..c2951d29b --- /dev/null +++ b/extensions/Review/Extension.pm @@ -0,0 +1,589 @@ +# 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. + +package Bugzilla::Extension::Review; +use strict; +use warnings; + +use base qw(Bugzilla::Extension); +our $VERSION = '1'; + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Extension::Review::Util; +use Bugzilla::Install::Filesystem; +use Bugzilla::User; +use Bugzilla::Util qw(clean_text); + +use constant UNAVAILABLE_RE => qr/\b(?:unavailable|pto|away)\b/i; + +# +# 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::mentors = \&_bug_mentors; + *Bugzilla::Bug::is_mentor = \&_bug_is_mentor; + *Bugzilla::User::review_count = \&_user_review_count; +} + +# +# monkey-patched methods +# + +sub _product_reviewers { _reviewers($_[0], 'product', $_[1]) } +sub _product_reviewers_objs { _reviewers_objs($_[0], 'product', $_[1]) } +sub _component_reviewers { _reviewers($_[0], 'component', $_[1]) } +sub _component_reviewers_objs { _reviewers_objs($_[0], 'component', $_[1]) } + +sub _reviewers { + my ($object, $type, $include_disabled) = @_; + return join(', ', map { $_->login } @{ _reviewers_objs($object, $type, $include_disabled) }); +} + +sub _reviewers_objs { + my ($object, $type, $include_disabled) = @_; + 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; + if (!$include_disabled) { + @reviewers = grep { $_->name !~ UNAVAILABLE_RE } @reviewers; + } + $object->{reviewers} = \@reviewers; + } + return $object->{reviewers}; +} + +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; + } + Bugzilla->logout_request() if $logout; + $self->{mentors} = \@mentors; + } + return $self->{mentors}; +} + +sub _bug_is_mentor { + my ($self, $user) = @_; + my $user_id = ($user || Bugzilla->user)->id; + return (grep { $_->id == $user_id} @{ $self->mentors }) ? 1 : 0; +} + +sub _user_review_count { + my ($self) = @_; + if (!exists $self->{review_count}) { + my $dbh = Bugzilla->dbh; + ($self->{review_count}) = $dbh->selectrow_array( + "SELECT COUNT(*) + FROM flags + INNER JOIN flagtypes ON flagtypes.id = flags.type_id + WHERE flags.requestee_id = ? + AND " . $dbh->sql_in('flagtypes.name', [ "'review'", "'feedback'" ]), + undef, + $self->id, + ); + } + return $self->{review_count}; +} + +# +# 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'; + } + elsif ($class->isa('Bugzilla::User')) { + push @$columns, qw(review_request_count feedback_request_count needinfo_request_count); + } +} + +sub object_update_columns { + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::Product')) { + push @$columns, 'reviewer_required'; + } + elsif ($object->isa('Bugzilla::User')) { + push @$columns, qw(review_request_count feedback_request_count needinfo_request_count); + } +} + +# +# 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)}; + + if ($object->isa('Bugzilla::Product') || $object->isa('Bugzilla::Component')) { + my ($new, $new_users) = _new_reviewers_from_input(); + _update_reviewers($object, [], $new_users); + } + elsif (_is_countable_flag($object) && $object->requestee_id && $object->status eq '?') { + _adjust_request_count($object, +1); + } +} + +sub object_end_of_update { + my ($self, $args) = @_; + 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 $old = $old_object->reviewers(1); + if ($old ne $new) { + _update_reviewers($object, $old_object->reviewers_objs(1), $new_users); + $changes->{reviewers} = [ $old ? $old : undef, $new ? $new : undef ]; + } + } + elsif (_is_countable_flag($object)) { + my ($old_status, $new_status) = ($old_object->status, $object->status); + if ($old_status ne '?' && $new_status eq '?') { + # setting flag to ? + _adjust_request_count($object, +1); + } + elsif ($old_status eq '?' && $new_status ne '?') { + # setting flag from ? + _adjust_request_count($old_object, -1); + } + elsif ($old_object->requestee_id && !$object->requestee_id) { + # removing requestee + _adjust_request_count($old_object, -1); + } + elsif (!$old_object->requestee_id && $object->requestee_id) { + # setting requestee + _adjust_request_count($object, +1); + } + elsif ($old_object->requestee_id && $object->requestee_id + && $old_object->requestee_id != $object->requestee_id) + { + # changing requestee + _adjust_request_count($old_object, -1); + _adjust_request_count($object, +1); + } + } +} + +sub object_before_delete { + my ($self, $args) = @_; + my $object = $args->{object}; + + if (_is_countable_flag($object) && $object->requestee_id && $object->status eq '?') { + _adjust_request_count($object, -1); + } +} + +sub _is_countable_flag { + my ($object) = @_; + return unless $object->isa('Bugzilla::Flag'); + my $type_name = $object->type->name; + return $type_name eq 'review' || $type_name eq 'feedback' || $type_name eq 'needinfo'; +} + +sub _adjust_request_count { + my ($flag, $add) = @_; + return unless my $requestee_id = $flag->requestee_id; + my $field = $flag->type->name . '_request_count'; + + # update the current user's object so things are display correctly on the + # post-processing page + my $user = Bugzilla->user; + if ($requestee_id == $user->id) { + $user->{$field} += $add; + } + + # update database directly to avoid creating audit_log entries + $add = $add == -1 ? ' - 1' : ' + 1'; + Bugzilla->dbh->do( + "UPDATE profiles SET $field = $field $add WHERE userid = ?", + undef, + $requestee_id + ); +} + +sub _new_reviewers_from_input { + if (!Bugzilla->input_params->{reviewers}) { + return ('', []); + } + 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 ($object, $new_flags) = @$args{qw(object new_flags)}; + my $bug = $object->isa('Bugzilla::Attachment') ? $object->bug : $object; + return unless $bug->product_obj->reviewer_required; + + foreach my $orig_change (@$new_flags) { + my $change = $orig_change; # work on a copy + $change =~ s/^[^:]+://; + my $reviewer = ''; + if ($change =~ s/\(([^\)]+)\)$//) { + $reviewer = $1; + } + my ($name, $value) = $change =~ /^(.+)(.)$/; + + if ($name eq 'review' && $value eq '?' && $reviewer eq '') { + ThrowUserError('reviewer_required'); + } + } +} + +# +# web service / pages +# + +sub webservice { + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{Review} = "Bugzilla::Extension::Review::WebService"; +} + +sub page_before_template { + my ($self, $args) = @_; + + if ($args->{page_id} eq 'review_suggestions.html') { + $self->review_suggestions_report($args); + } + elsif ($args->{page_id} eq 'review_requests_rebuild.html') { + $self->review_requests_rebuild($args); + } +} + +sub review_suggestions_report { + my ($self, $args) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $products = []; + my @products = sort { lc($a->name) cmp lc($b->name) } + @{ Bugzilla->user->get_accessible_products }; + foreach my $product_obj (@products) { + my $has_reviewers = 0; + my $product = { + name => $product_obj->name, + components => [], + reviewers => $product_obj->reviewers_objs(1), + }; + $has_reviewers = scalar @{ $product->{reviewers} }; + + foreach my $component_obj (@{ $product_obj->components }) { + my $component = { + name => $component_obj->name, + reviewers => $component_obj->reviewers_objs(1), + }; + if (@{ $component->{reviewers} }) { + push @{ $product->{components} }, $component; + $has_reviewers = 1; + } + } + + if ($has_reviewers) { + push @$products, $product; + } + } + $args->{vars}->{products} = $products; +} + +sub review_requests_rebuild { + my ($self, $args) = @_; + + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', { group => 'admin', + action => 'run', + object => 'review_requests_rebuild' }); + if (Bugzilla->cgi->param('rebuild')) { + my $processed_users = 0; + rebuild_review_counters(sub { + my ($count, $total) = @_; + $processed_users = $total; + }); + $args->{vars}->{rebuild} = 1; + $args->{vars}->{total} = $processed_users; + } +} + +# +# installation +# + +sub db_schema_abstract_schema { + my ($self, $args) = @_; + $args->{'schema'}->{'product_reviewers'} = { + FIELDS => [ + id => { + TYPE => 'MEDIUMSERIAL', + NOTNULL => 1, + PRIMARYKEY => 1, + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'CASCADE', + } + }, + display_name => { + TYPE => 'VARCHAR(64)', + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => { + TABLE => 'products', + COLUMN => 'id', + DELETE => 'CASCADE', + } + }, + sortkey => { + TYPE => 'INT2', + NOTNULL => 1, + DEFAULT => 0, + }, + ], + INDEXES => [ + product_reviewers_idx => { + FIELDS => [ 'user_id', 'product_id' ], + TYPE => 'UNIQUE', + }, + ], + }; + $args->{'schema'}->{'component_reviewers'} = { + FIELDS => [ + id => { + TYPE => 'MEDIUMSERIAL', + NOTNULL => 1, + PRIMARYKEY => 1, + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'CASCADE', + } + }, + display_name => { + TYPE => 'VARCHAR(64)', + }, + component_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => { + TABLE => 'components', + COLUMN => 'id', + DELETE => 'CASCADE', + } + }, + sortkey => { + TYPE => 'INT2', + NOTNULL => 1, + DEFAULT => 0, + }, + ], + INDEXES => [ + component_reviewers_idx => { + FIELDS => [ 'user_id', 'component_id' ], + TYPE => 'UNIQUE', + }, + ], + }; + +} + +sub install_update_db { + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column( + 'products', + 'reviewer_required', { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' } + ); + $dbh->bz_add_column( + 'profiles', + 'review_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 } + ); + $dbh->bz_add_column( + 'profiles', + 'feedback_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 } + ); + $dbh->bz_add_column( + 'profiles', + 'needinfo_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 } + ); +} + +sub install_filesystem { + my ($self, $args) = @_; + my $files = $args->{files}; + my $extensions_dir = bz_locations()->{extensionsdir}; + $files->{"$extensions_dir/Review/bin/review_requests_rebuild.pl"} = { + perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE + }; +} + + +__PACKAGE__->NAME; diff --git a/extensions/Review/bin/review_requests_rebuild.pl b/extensions/Review/bin/review_requests_rebuild.pl new file mode 100755 index 000000000..04f8b1042 --- /dev/null +++ b/extensions/Review/bin/review_requests_rebuild.pl @@ -0,0 +1,29 @@ +#!/usr/bin/perl + +# 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. + +use strict; +use warnings; +$| = 1; + +use FindBin qw($Bin); +use lib "$Bin/../../.."; + +use Bugzilla; +BEGIN { Bugzilla->extensions() } + +use Bugzilla::Constants; +use Bugzilla::Install::Util qw(indicate_progress); +use Bugzilla::Extension::Review::Util; + +Bugzilla->usage_mode(USAGE_MODE_CMDLINE); + +rebuild_review_counters(sub{ + my ($count, $total) = @_; + indicate_progress({ current => $count, total => $total, every => 5 }); +}); diff --git a/extensions/Review/lib/Util.pm b/extensions/Review/lib/Util.pm new file mode 100644 index 000000000..7304f9ba6 --- /dev/null +++ b/extensions/Review/lib/Util.pm @@ -0,0 +1,63 @@ +# 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. + +package Bugzilla::Extension::Review::Util; +use strict; +use warnings; + +use base qw(Exporter); +use Bugzilla; + +our @EXPORT = qw( rebuild_review_counters ); + +sub rebuild_review_counters { + my ($callback) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction; + my $rows = $dbh->selectall_arrayref(" + SELECT flags.requestee_id AS user_id, + flagtypes.name AS flagtype, + COUNT(*) as count + FROM flags + INNER JOIN profiles ON profiles.userid = flags.requestee_id + INNER JOIN flagtypes ON flagtypes.id = flags.type_id + WHERE flags.status = '?' + AND flagtypes.name IN ('review', 'feedback', 'needinfo') + GROUP BY flags.requestee_id, flagtypes.name + ", { Slice => {} }); + + my ($count, $total, $current) = (1, scalar(@$rows), { id => 0 }); + foreach my $row (@$rows) { + $callback->($count++, $total) if $callback; + if ($row->{user_id} != $current->{id}) { + _update_profile($dbh, $current) if $current->{id}; + $current = { id => $row->{user_id} }; + } + $current->{$row->{flagtype}} = $row->{count}; + } + _update_profile($dbh, $current) if $current->{id}; + $dbh->bz_commit_transaction; +} + +sub _update_profile { + my ($dbh, $data) = @_; + $dbh->do(" + UPDATE profiles + SET review_request_count = ?, + feedback_request_count = ?, + needinfo_request_count = ? + WHERE userid = ?", + undef, + $data->{review} || 0, + $data->{feedback} || 0, + $data->{needinfo} || 0, + $data->{id} + ); +} + +1; diff --git a/extensions/Review/lib/WebService.pm b/extensions/Review/lib/WebService.pm new file mode 100644 index 000000000..4cb3d48d8 --- /dev/null +++ b/extensions/Review/lib/WebService.pm @@ -0,0 +1,195 @@ +# 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. + +package Bugzilla::Extension::Review::WebService; + +use strict; +use warnings; + +use base qw(Bugzilla::WebService); + +use Bugzilla::Bug; +use Bugzilla::Component; +use Bugzilla::Error; + +sub suggestions { + my ($self, $params) = @_; + my $dbh = Bugzilla->switch_to_shadow_db(); + + my ($bug, $product, $component); + if (exists $params->{bug_id}) { + $bug = Bugzilla::Bug->check($params->{bug_id}); + $product = $bug->product_obj; + $component = $bug->component_obj; + } + elsif (exists $params->{product}) { + $product = Bugzilla::Product->check($params->{product}); + if (exists $params->{component}) { + $component = Bugzilla::Component->check({ + product => $product, name => $params->{component} + }); + } + } + else { + ThrowUserError("reviewer_suggestions_param_required"); + } + + my @reviewers; + if ($bug) { + # we always need to be authentiated to perform user matching + my $user = Bugzilla->user; + if (!$user->id) { + Bugzilla->set_user(Bugzilla::User->check({ name => 'nobody@mozilla.org' })); + push @reviewers, @{ $bug->mentors }; + Bugzilla->set_user($user); + } else { + push @reviewers, @{ $bug->mentors }; + } + } + if ($component) { + push @reviewers, @{ $component->reviewers_objs }; + } + if (!@{ $component->reviewers_objs }) { + push @reviewers, @{ $product->reviewers_objs }; + } + + my @result; + foreach my $reviewer (@reviewers) { + push @result, { + id => $self->type('int', $reviewer->id), + email => $self->type('email', $reviewer->login), + name => $self->type('string', $reviewer->name), + review_count => $self->type('int', $reviewer->review_count), + }; + } + return \@result; +} + +sub rest_resources { + return [ + # bug-id + qr{^/review/suggestions/(\d+)$}, { + GET => { + method => 'suggestions', + params => sub { + return { bug_id => $_[0] }; + }, + }, + }, + # product/component + qr{^/review/suggestions/([^/]+)/(.+)$}, { + GET => { + method => 'suggestions', + params => sub { + return { product => $_[0], component => $_[1] }; + }, + }, + }, + # just product + qr{^/review/suggestions/([^/]+)$}, { + GET => { + method => 'suggestions', + params => sub { + return { product => $_[0] }; + }, + }, + }, + # named parameters + qr{^/review/suggestions$}, { + GET => { + method => 'suggestions', + }, + }, + ]; +}; + +1; + +__END__ +=head1 NAME + +Bugzilla::Extension::Review::WebService - Functions for the Mozilla specific +'review' flag optimisations. + +=head1 METHODS + +See L<Bugzilla::WebService> for a description of how parameters are passed, +and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. + +Although the data input and output is the same for JSONRPC, XMLRPC and REST, +the directions for how to access the data via REST is noted in each method +where applicable. + +=head2 suggestions + +B<EXPERIMENTAL> + +=over + +=item B<Description> + +Returns the list of suggestions for reviewers. + +=item B<REST> + +GET /rest/review/suggestions/C<bug-id> + +GET /rest/review/suggestions/C<product-name> + +GET /rest/review/suggestions/C<product-name>/C<component-name> + +GET /rest/review/suggestions?product=C<product-name> + +GET /rest/review/suggestions?product=C<product-name>&component=C<component-name> + +The returned data format is the same as below. + +=item B<Params> + +Query by Bug: + +=over + +=over + +=item C<bug_id> (integer) - The bug ID. + +=back + +=back + +Query by Product or Component: + +=over + +=over + +=item C<product> (string) - The product name. + +=item C<component> (string) - The component name (optional). If providing a C<component>, a C<product> must also be provided. + +=back + +=back + +=item B<Returns> + +An array of hashes with the following keys/values: + +=over + +=item C<id> (integer) - The user's ID. + +=item C<email> (string) - The user's email address (aka login). + +=item C<name> (string) - The user's display name (may not match the Bugzilla "real name"). + +=item C<review_count> (string) - The number of "review" and "feedback" requests in the user's queue. + +=back + +=back 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..297b08506 --- /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(1) + size => 64 + emptyok => 1 + title => "One or more email address (comma delimited)" + placeholder => product.reviewers(1) + %] + </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..61a275e72 --- /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(1) + 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..55226545d --- /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 %] + +<script> + YAHOO.util.Event.onDOMReady(function() { + [% IF bug.product_obj.reviewer_required %] + REVIEW.init_mandatory(); + [% END %] + REVIEW.init_create_attachment(); + }); +</script> 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-message.html.tmpl b/extensions/Review/template/en/default/hook/global/header-message.html.tmpl new file mode 100644 index 000000000..33e899492 --- /dev/null +++ b/extensions/Review/template/en/default/hook/global/header-message.html.tmpl @@ -0,0 +1,23 @@ +[%# 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 + user.review_request_count + || user.feedback_request_count + || user.needinfo_request_count +%] + +<span id="badge" title="Flags requested of you: + [%- " review (" _ user.review_request_count _ ")" IF user.review_request_count -%] + [%- " feedback (" _ user.feedback_request_count _ ")" IF user.feedback_request_count -%] + [%- " needinfo (" _ user.needinfo_request_count _ ")" IF user.needinfo_request_count -%] +"> + <a href="request.cgi?action=queue&requestee=[% user.login FILTER uri %]&group=type"> + [%- user.review_request_count + user.feedback_request_count + user.needinfo_request_count ~%] + </a> +</span> 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..0c276ad8c --- /dev/null +++ b/extensions/Review/template/en/default/hook/global/header-start.html.tmpl @@ -0,0 +1,90 @@ +[%# 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 user.review_request_count + || user.feedback_request_count + || user.needinfo_request_count +%] + [% style_urls.push('extensions/Review/web/styles/badge.css') %] +[% END %] + +[% 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/styles/review.css') %] +[% javascript_urls.push('extensions/Review/web/js/review.js') %] + +[% IF bug %] + [%# create attachment %] + [% mentors = bug.mentors %] + [% product_obj = bug.product_obj %] + [% component_obj = bug.component_obj %] +[% ELSIF attachment.bug %] + [%# edit attachment %] + [% mentors = attachment.bug.mentors %] + [% product_obj = attachment.bug.product_obj %] + [% component_obj = attachment.bug.component_obj %] +[% ELSE %] + [%# create bug %] + [% mentors = [] %] + [% product_obj = product %] + [% component_obj = 0 %] +[% END %] + +[% review_js = BLOCK %] + review_suggestions = { + _mentors: [ + [% FOREACH u = mentors %] + [% PROCESS reviewer %][% "," UNLESS loop.last %] + [% 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 %]', review_count: [% u.review_count FILTER js %] } +[% END %] diff --git a/extensions/Review/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/Review/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl new file mode 100644 index 000000000..156f0aa93 --- /dev/null +++ b/extensions/Review/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl @@ -0,0 +1,11 @@ +[%# 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 object == 'review_requests_rebuild' %] + rebuild review request counters +[% 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..856ff3c75 --- /dev/null +++ b/extensions/Review/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,18 @@ +[%# 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. + +[% ELSIF error == "reviewer_suggestions_param_required" %] + [% title = "Parameter Required" %] + You must provide either a bug_id, or a product (and optionally a + component). + +[% END %] diff --git a/extensions/Review/template/en/default/hook/reports/menu-end.html.tmpl b/extensions/Review/template/en/default/hook/reports/menu-end.html.tmpl new file mode 100644 index 000000000..d25ba20ee --- /dev/null +++ b/extensions/Review/template/en/default/hook/reports/menu-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. + #%] + +<ul> + <li> + <strong> + <a href="[% urlbase FILTER none %]page.cgi?id=review_suggestions.html">Suggested Reviewers</a> + </strong> - All suggestions for the "review" flag. + </li> +</ul> + diff --git a/extensions/Review/template/en/default/pages/review_requests_rebuild.html.tmpl b/extensions/Review/template/en/default/pages/review_requests_rebuild.html.tmpl new file mode 100644 index 000000000..5ec811126 --- /dev/null +++ b/extensions/Review/template/en/default/pages/review_requests_rebuild.html.tmpl @@ -0,0 +1,23 @@ +[%# 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. + #%] + +[% INCLUDE global/header.html.tmpl + title = "Review Requests Rebuild" +%] + +[% IF rebuild %] + Counters rebuilt for [% total FILTER html %] users. +[% ELSE %] + <form method="post"> + <input type="hidden" name="id" value="review_requests_rebuild.html"> + <input type="hidden" name="rebuild" value="1"> + <input type="submit" value="Rebuild Review Request Counters"> + </form> +[% END %] + +[% INCLUDE global/footer.html.tmpl %] diff --git a/extensions/Review/template/en/default/pages/review_suggestions.html.tmpl b/extensions/Review/template/en/default/pages/review_suggestions.html.tmpl new file mode 100644 index 000000000..5d9132e40 --- /dev/null +++ b/extensions/Review/template/en/default/pages/review_suggestions.html.tmpl @@ -0,0 +1,76 @@ +[%# 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. + #%] + +[% INCLUDE global/header.html.tmpl + title = "Suggested Reviewers Report" + style_urls = [ "extensions/BMO/web/styles/reports.css", + "extensions/Review/web/styles/reports.css" ] +%] + +Products: +<ul> + [% FOREACH product = products %] + <li> + <a href="#[% product.name FILTER uri %]"> + [% product.name FILTER html %] + </a> + </li> + [% END %] +</ul> + +<a href="enter_bug.cgi?product=bugzilla.mozilla.org&component=Administration&format=__default__">Request a change</a> + +<table id="report" class="hover" cellspacing="0"> + +<tr id="report-header"> + <th>Product/Component</th> + <th>Suggested Reviewers</th> +</tr> + +[% FOREACH product = products %] + <tr class="report_subheader"> + <td class="product_name"> + <a name="[% product.name FILTER html %]"> + [% product.name FILTER html %] + </a> + </td> + <td> + </td> + </tr> + [% row_class = "report_row_even" %] + [% FOREACH component = product.components %] + <tr class="[% row_class FILTER none %]"> + <td class="component_name">[% component.name FILTER html %]</td> + <td class="reviewers"> + [% FOREACH reviewer = component.reviewers %] + <span title="[% reviewer.name FILTER html %]"> + [% reviewer.email FILTER html %]</span> + [% ", " UNLESS loop.last %] + [% END %] + </td> + </tr> + [% row_class = row_class == "report_row_even" ? "report_row_odd" : "report_row_even" %] + [% END %] + [% IF product.reviewers.size %] + <tr class="[% row_class FILTER none %]"> + <td class="other_components">All [% product.components.size ? "other" : "" %] components</td> + <td class="reviewers"> + [% FOREACH reviewer = product.reviewers %] + <span title="[% reviewer.name FILTER html %]"> + [% reviewer.email FILTER html %]</span> + [% ", " UNLESS loop.last %] + [% END %] + </td> + </tr> + [% row_class = row_class == "report_row_even" ? "report_row_odd" : "report_row_even" %] + [% END %] +[% END %] + +</table> + +[% INCLUDE global/footer.html.tmpl %] diff --git a/extensions/Review/web/js/review.js b/extensions/Review/web/js/review.js new file mode 100644 index 000000000..08ae29547 --- /dev/null +++ b/extensions/Review/web/js/review.js @@ -0,0 +1,210 @@ +/* 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, + ispatch_override: false, + description_override: 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); + Event.addListener('description', 'change', REVIEW.description_change); + Event.addListener('ispatch', 'change', REVIEW.ispatch_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 == '' || !REVIEW.description_override) { + description.value = filename; + } + if (!REVIEW.ispatch_override) { + Dom.get('ispatch').checked = + REVIEW.endsWith(filename, '.diff') || REVIEW.endsWith(filename, '.patch'); + } + setContentTypeDisabledState(this.form); + description.select(); + description.focus(); + }, + + description_change: function() { + REVIEW.description_override = true; + }, + + ispatch_change: function() { + REVIEW.ispatch_override = true; + }, + + 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(); + for (var i = 0, il = review_suggestions._mentors.length; i < il; i++) { + REVIEW.add_menu_item(field_idx, review_suggestions._mentors[i], 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 items = menu.getItems(); + for (var i = 0, il = items.length; i < il; i++) { + if (items[i].cfg.config.url.value == '#' + user.login) { + return; + } + } + var queue = ''; + if (user.review_count == 0) { + queue = 'empty queue'; + } else { + queue = user.review_count + ' review' + (user.review_count == 1 ? '' : 's') + ' in queue'; + } + var item = menu.addItem( + { text: user.identity + ' (' + queue + ')', 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/badge.css b/extensions/Review/web/styles/badge.css new file mode 100644 index 000000000..8b0791f12 --- /dev/null +++ b/extensions/Review/web/styles/badge.css @@ -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. */ + +#badge { + background-color: #c00; + font-size: small; + font-weight: bold; + padding: 0 5px; + border-radius: 10px; + margin: 0 5px; +} + +#badge a { + color: #fff !important; +} diff --git a/extensions/Review/web/styles/reports.css b/extensions/Review/web/styles/reports.css new file mode 100644 index 000000000..bbbf93559 --- /dev/null +++ b/extensions/Review/web/styles/reports.css @@ -0,0 +1,41 @@ +/* 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. */ + +#report { + margin-top: 1em; +} + +.product_name { + font-weight: bold; + white-space: nowrap; +} + +.product_name a { + color: inherit; +} + +.product_name a:hover { + color: inherit; + text-decoration: none; +} + +.component_name, .other_components { + padding: 0 1em; + white-space: nowrap; +} + +.component_name:before, .other_components:before { + content: "\a0\a0\a0\a0"; +} + +.other_components { + font-style: italic; +} + +.reviewers { + width: 100%; +} 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; +} |