summaryrefslogtreecommitdiffstats
path: root/extensions
diff options
context:
space:
mode:
authorByron Jones <glob@mozilla.com>2014-07-08 10:40:14 +0200
committerByron Jones <glob@mozilla.com>2014-07-08 10:40:14 +0200
commit2f3b5dd7df3e131af6aef3cd5ccf7e8523c1780e (patch)
treee71ea56398621e038df3c27cc5b87accf7a04968 /extensions
parentd74129306d8d5a903af6fe3957046feb36affdd1 (diff)
downloadbugzilla-2f3b5dd7df3e131af6aef3cd5ccf7e8523c1780e.tar.gz
bugzilla-2f3b5dd7df3e131af6aef3cd5ccf7e8523c1780e.tar.xz
Bug 990980: create an extension for server-side filtering of bugmail
Diffstat (limited to 'extensions')
-rw-r--r--extensions/BugmailFilter/Extension.pm321
-rw-r--r--extensions/BugmailFilter/lib/Constants.pm113
-rw-r--r--extensions/BugmailFilter/lib/FakeField.pm57
-rw-r--r--extensions/BugmailFilter/lib/Filter.pm169
-rw-r--r--extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl256
-rw-r--r--extensions/BugmailFilter/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl14
-rw-r--r--extensions/BugmailFilter/template/en/default/hook/global/user-error-errors.html.tmpl13
-rw-r--r--extensions/BugmailFilter/web/js/bugmail-filter.js37
-rw-r--r--extensions/BugmailFilter/web/style/bugmail-filter.css44
-rw-r--r--extensions/Review/Extension.pm5
-rw-r--r--extensions/TrackingFlags/Extension.pm11
-rw-r--r--extensions/TrackingFlags/lib/Admin.pm4
12 files changed, 1041 insertions, 3 deletions
diff --git a/extensions/BugmailFilter/Extension.pm b/extensions/BugmailFilter/Extension.pm
index e4a1be7ff..89564031c 100644
--- a/extensions/BugmailFilter/Extension.pm
+++ b/extensions/BugmailFilter/Extension.pm
@@ -12,6 +12,327 @@ use warnings;
use base qw(Bugzilla::Extension);
our $VERSION = '1';
+use Bugzilla::BugMail;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Extension::BugmailFilter::Constants;
+use Bugzilla::Extension::BugmailFilter::FakeField;
+use Bugzilla::Extension::BugmailFilter::Filter;
+use Bugzilla::Field;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Encode;
+use Sys::Syslog qw(:DEFAULT);
+
+#
+# preferences
+#
+
+sub user_preferences {
+ my ($self, $args) = @_;
+ return unless $args->{current_tab} eq 'bugmail_filter';
+
+ if ($args->{save_changes}) {
+ my $input = Bugzilla->input_params;
+
+ if ($input->{add_filter}) {
+
+ # add a new filter
+
+ my $params = {
+ user_id => Bugzilla->user->id,
+ };
+ $params->{field_name} = $input->{field} || IS_NULL;
+ $params->{relationship} = $input->{relationship} || IS_NULL;
+ if (my $product_name = $input->{product}) {
+ my $product = Bugzilla::Product->check({
+ name => $product_name, cache => 1
+ });
+ $params->{product_id} = $product->id;
+
+ if (my $component_name = $input->{component}) {
+ $params->{component_id} = Bugzilla::Component->check({
+ name => $component_name, product => $product,
+ cache => 1
+ })->id;
+ }
+ else {
+ $params->{component_id} = IS_NULL;
+ }
+ }
+ else {
+ $params->{product_id} = IS_NULL;
+ $params->{component_id} = IS_NULL;
+ }
+
+ if (@{ Bugzilla::Extension::BugmailFilter::Filter->match($params) }) {
+ ThrowUserError('bugmail_filter_exists');
+ }
+ $params->{action} = $input->{action} eq 'Exclude' ? 1 : 0;
+ foreach my $name (keys %$params) {
+ $params->{$name} = undef
+ if $params->{$name} eq IS_NULL;
+ }
+ Bugzilla::Extension::BugmailFilter::Filter->create($params);
+ }
+
+ elsif ($input->{remove_filter}) {
+
+ # remove filter(s)
+
+ my $ids = ref($input->{remove}) ? $input->{remove} : [ $input->{remove} ];
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction;
+ foreach my $id (@$ids) {
+ if (my $filter = Bugzilla::Extension::BugmailFilter::Filter->new($id)) {
+ $filter->remove_from_db();
+ }
+ }
+ $dbh->bz_commit_transaction;
+ }
+ }
+
+ my $vars = $args->{vars};
+
+ my @fields = @{ Bugzilla->fields({ obsolete => 0 }) };
+
+ # remove time trackinger fields
+ if (!Bugzilla->user->is_timetracker) {
+ foreach my $tt_field (TIMETRACKING_FIELDS) {
+ @fields = grep { $_->name ne $tt_field } @fields;
+ }
+ }
+
+ # remove fields which don't make any sense to filter on
+ foreach my $ignore_field (IGNORE_FIELDS) {
+ @fields = grep { $_->name ne $ignore_field } @fields;
+ }
+
+ # remove all tracking flag fields. these change too frequently to be of
+ # value, so they only add noise to the list.
+ foreach my $name (@{ Bugzilla->tracking_flag_names }) {
+ @fields = grep { $_->name ne $name } @fields;
+ }
+
+ # add tracking flag types instead
+ foreach my $field (
+ @{ Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields() }
+ ) {
+ push @fields, $field;
+ }
+
+ # adjust the description for selected fields. as we shouldn't touch the
+ # real Field objects, we remove the object and insert a FakeField object
+ foreach my $override_field (keys %{ FIELD_DESCRIPTION_OVERRIDE() }) {
+ @fields = grep { $_->name ne $override_field } @fields;
+ push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new({
+ name => $override_field,
+ description => FIELD_DESCRIPTION_OVERRIDE->{$override_field},
+ });
+ }
+
+ # some fields are present in the changed-fields x-header but are not real
+ # bugzilla fields
+ foreach my $field (
+ @{ Bugzilla::Extension::BugmailFilter::FakeField->fake_fields() }
+ ) {
+ push @fields, $field;
+ }
+
+ @fields = sort { lc($a->description) cmp lc($b->description) } @fields;
+ $vars->{fields} = \@fields;
+
+ $vars->{relationships} = FILTER_RELATIONSHIPS();
+
+ $vars->{filters} = [
+ sort {
+ $a->product_name cmp $b->product_name
+ || $a->component_name cmp $b->component_name
+ || $a->field_name cmp $b->field_name
+ }
+ @{ Bugzilla::Extension::BugmailFilter::Filter->match({
+ user_id => Bugzilla->user->id,
+ }) }
+ ];
+
+ ${ $args->{handled} } = 1;
+}
+
+#
+# hooks
+#
+
+sub user_wants_mail {
+ my ($self, $args) = @_;
+
+ my ($user, $wants_mail, $diffs, $comments)
+ = @$args{qw( user wants_mail fieldDiffs comments )};
+
+ return unless $$wants_mail;
+
+ my $cache = Bugzilla->request_cache->{bugmail_filters} //= {};
+ my $filters = $cache->{$user->id} //=
+ Bugzilla::Extension::BugmailFilter::Filter->match({
+ user_id => $user->id
+ });
+ return unless @$filters;
+
+ my $fields = [ map { $_->{field_name} } @$diffs ];
+
+ # insert fake fields for new attachments and comments
+ if (@$comments) {
+ if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) {
+ push @$fields, 'attachment.created';
+ }
+ if (grep { $_->type != CMT_ATTACHMENT_CREATED } @$comments) {
+ push @$fields, 'comment.created';
+ }
+ }
+
+ # replace tracking flag fields with fake tracking flag types
+ require Bugzilla::Extension::TrackingFlags::Flag;
+ my %count;
+ my @tracking_flags;
+ foreach my $field (@$fields, @{ Bugzilla->tracking_flag_names }) {
+ $count{$field}++;
+ }
+ foreach my $field (keys %count) {
+ push @tracking_flags, $field
+ if $count{$field} > 1;
+ }
+ my %tracking_types =
+ map { $_->flag_type => 1 }
+ @{ Bugzilla::Extension::TrackingFlags::Flag->match({
+ name => \@tracking_flags
+ })};
+ foreach my $type (keys %tracking_types) {
+ push @$fields, 'tracking.' . $type;
+ }
+ foreach my $field (@{ Bugzilla->tracking_flag_names }) {
+ $fields = [ grep { $_ ne $field } @$fields ];
+ }
+
+ if (_should_drop($fields, $filters, $args)) {
+ $$wants_mail = 0;
+ openlog('apache', 'cons,pid', 'local4');
+ syslog('notice', encode_utf8(sprintf(
+ '[bugmail] %s (filtered) bug-%s %s',
+ $args->{user}->login,
+ $args->{bug}->id,
+ $args->{bug}->short_desc,
+ )));
+ closelog();
+ }
+}
+
+sub _should_drop {
+ my ($fields, $filters, $args) = @_;
+
+ # calculate relationships
+
+ my ($user, $bug, $relationship) = @$args{qw( user bug relationship )};
+ my ($user_id, $login) = ($user->id, $user->login);
+ my $bit_direct = Bugzilla::BugMail::BIT_DIRECT;
+ my $bit_watching = Bugzilla::BugMail::BIT_WATCHING;
+ my $bit_compwatch = 15; # from Bugzilla::Extension::ComponentWatching
+
+ # the index of $rel_map corresponds to the values in FILTER_RELATIONSHIPS
+ my @rel_map;
+ $rel_map[1] = $bug->assigned_to->id == $user_id;
+ $rel_map[2] = !$rel_map[1];
+ $rel_map[3] = $bug->reporter->id == $user_id;
+ $rel_map[4] = !$rel_map[3];
+ if ($bug->qa_contact) {
+ $rel_map[5] = $bug->qa_contact->id == $user_id;
+ $rel_map[6] = !$rel_map[6];
+ }
+ $rel_map[7] = grep { $_ eq $login } @{ $bug->cc };
+ $rel_map[8] = !$rel_map[8];
+ $rel_map[9] = (
+ $relationship & $bit_watching
+ or $relationship & $bit_compwatch
+ );
+ $rel_map[10] = !$rel_map[9];
+ $rel_map[11] = $bug->is_mentor($user);
+ $rel_map[12] = !$rel_map[11];
+ foreach my $bool (@rel_map) {
+ $bool = $bool ? 1 : 0;
+ }
+
+ # exclusions
+ # drop email where we are excluding all changed fields
+
+ my %exclude = map { $_ => 0 } @$fields;
+ my $params = {
+ product_id => $bug->product_id,
+ component_id => $bug->component_id,
+ rel_map => \@rel_map,
+ };
+
+ foreach my $field_name (@$fields) {
+ $params->{field_name} = $field_name;
+ foreach my $filter (grep { $_->is_exclude } @$filters) {
+ if ($filter->matches($params)) {
+ $exclude{$field_name} = 1;
+ last;
+ }
+ }
+ }
+
+ # no need to process includes if nothing was excluded
+ if (!grep { $exclude{$_} } @$fields) {
+ return 0;
+ }
+
+ # inclusions
+ # flip the bit for fields that should be included
+
+ foreach my $field_name (@$fields) {
+ $params->{field_name} = $field_name;
+ foreach my $filter (grep { $_->is_include } @$filters) {
+ if ($filter->matches($params)) {
+ $exclude{$field_name} = 0;
+ last;
+ }
+ }
+ }
+
+ # drop if all fields are still excluded
+ return !(grep { !$exclude{$_} } keys %exclude);
+}
+
+# catch when fields are renamed, and update the field_name entires
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+
+ return unless $object->isa('Bugzilla::Field')
+ || $object->isa('Bugzilla::Extension::TrackingFlags::Flag');
+
+ return unless exists $args->{changes}->{name};
+
+ my $old_name = $args->{changes}->{name}->[0];
+ my $new_name = $args->{changes}->{name}->[1];
+
+ Bugzilla->dbh->do(
+ "UPDATE bugmail_filters SET field_name=? WHERE field_name=?",
+ undef,
+ $new_name, $old_name);
+}
+
+sub reorg_move_component {
+ my ($self, $args) = @_;
+ my $new_product = $args->{new_product};
+ my $component = $args->{component};
+
+ Bugzilla->dbh->do(
+ "UPDATE bugmail_filters SET product_id=? WHERE component_id=?",
+ undef,
+ $new_product->id, $component->id,
+ );
+}
+
#
# schema / install
#
diff --git a/extensions/BugmailFilter/lib/Constants.pm b/extensions/BugmailFilter/lib/Constants.pm
new file mode 100644
index 000000000..98b5793af
--- /dev/null
+++ b/extensions/BugmailFilter/lib/Constants.pm
@@ -0,0 +1,113 @@
+# 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::BugmailFilter::Constants;
+use strict;
+
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ FAKE_FIELD_NAMES
+ IGNORE_FIELDS
+ FIELD_DESCRIPTION_OVERRIDE
+ FILTER_RELATIONSHIPS
+);
+
+use Bugzilla::Constants;
+
+# these are field names which are inserted into X-Bugzilla-Changed-Field-Names
+# header but are not real fields
+
+use constant FAKE_FIELD_NAMES => [
+ {
+ name => 'comment.created',
+ description => 'Comment created',
+ },
+ {
+ name => 'attachment.created',
+ description => 'Attachment created',
+ },
+];
+
+# these fields don't make any sense to filter on
+
+use constant IGNORE_FIELDS => qw(
+ attach_data.thedata
+ attachments.submitter
+ comment_tag
+ days_elapsed
+ delta_ts
+ everconfirmed
+ longdesc
+ longdescs.count
+ owner_idle_time
+ reporter
+ reporter_accessible
+ tag
+);
+
+# override the description of some fields
+
+use constant FIELD_DESCRIPTION_OVERRIDE => {
+ bug_id => 'Bug Created',
+};
+
+# relationship / int mappings
+# _should_drop() also needs updating when this const is changed
+
+use constant FILTER_RELATIONSHIPS => [
+ {
+ name => 'Assignee',
+ value => 1,
+ },
+ {
+ name => 'Not Assignee',
+ value => 2,
+ },
+ {
+ name => 'Reporter',
+ value => 3,
+ },
+ {
+ name => 'Not Reporter',
+ value => 4,
+ },
+ {
+ name => 'QA Contact',
+ value => 5,
+ },
+ {
+ name => 'Not QA Contact',
+ value => 6,
+ },
+ {
+ name => "CC'ed",
+ value => 7,
+ },
+ {
+ name => "Not CC'ed",
+ value => 8,
+ },
+ {
+ name => 'Watching',
+ value => 9,
+ },
+ {
+ name => 'Not Watching',
+ value => 10,
+ },
+ {
+ name => 'Mentoring',
+ value => 11,
+ },
+ {
+ name => 'Not Mentoring',
+ value => 12,
+ },
+];
+
+1;
diff --git a/extensions/BugmailFilter/lib/FakeField.pm b/extensions/BugmailFilter/lib/FakeField.pm
new file mode 100644
index 000000000..88e4ac1ca
--- /dev/null
+++ b/extensions/BugmailFilter/lib/FakeField.pm
@@ -0,0 +1,57 @@
+# 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::BugmailFilter::FakeField;
+
+use strict;
+use warnings;
+
+use Bugzilla::Extension::BugmailFilter::Constants;
+
+# object
+
+sub new {
+ my ($class, $params) = @_;
+ return bless($params, $class);
+}
+
+sub name { $_[0]->{name} }
+sub description { $_[0]->{description} }
+
+# static methods
+
+sub fake_fields {
+ my $cache = Bugzilla->request_cache->{bugmail_filter};
+ if (!$cache->{fake_fields}) {
+ my @fields;
+ foreach my $rh (@{ FAKE_FIELD_NAMES() }) {
+ push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new($rh);
+ }
+ $cache->{fake_fields} = \@fields;
+ }
+ return $cache->{fake_fields};
+}
+
+sub tracking_flag_fields {
+ my $cache = Bugzilla->request_cache->{bugmail_filter};
+ if (!$cache->{tracking_flag_fields}) {
+ require Bugzilla::Extension::TrackingFlags::Constants;
+ my @fields;
+ my $tracking_types = Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES();
+ foreach my $tracking_type (@$tracking_types) {
+ push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new({
+ name => 'tracking.' . $tracking_type->{name},
+ description => $tracking_type->{description},
+ sortkey => $tracking_type->{sortkey},
+ });
+ }
+ $cache->{tracking_flag_fields} = \@fields;
+ }
+ return $cache->{tracking_flag_fields};
+}
+
+1;
diff --git a/extensions/BugmailFilter/lib/Filter.pm b/extensions/BugmailFilter/lib/Filter.pm
new file mode 100644
index 000000000..9a0d0f89a
--- /dev/null
+++ b/extensions/BugmailFilter/lib/Filter.pm
@@ -0,0 +1,169 @@
+# 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::BugmailFilter::Filter;
+
+use base qw(Bugzilla::Object);
+
+use strict;
+use warnings;
+
+use Bugzilla::Component;
+use Bugzilla::Error;
+use Bugzilla::Extension::BugmailFilter::Constants;
+use Bugzilla::Extension::BugmailFilter::FakeField;
+use Bugzilla::Field;
+use Bugzilla::Product;
+use Bugzilla::User;
+
+use constant DB_TABLE => 'bugmail_filters';
+
+use constant DB_COLUMNS => qw(
+ id
+ user_id
+ product_id
+ component_id
+ field_name
+ relationship
+ action
+);
+
+use constant LIST_ORDER => 'id';
+
+use constant UPDATE_COLUMNS => ();
+
+use constant VALIDATORS => {
+ user_id => \&_check_user,
+ field_name => \&_check_field_name,
+ action => \&Bugzilla::Object::check_boolean,
+};
+use constant VALIDATOR_DEPENDENCIES => {
+ component_id => [ 'product_id' ],
+};
+
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+use constant USE_MEMCACHED => 0;
+
+# getters
+
+sub user {
+ my ($self) = @_;
+ return Bugzilla::User->new({ id => $self->{user_id}, cache => 1 });
+}
+
+sub product {
+ my ($self) = @_;
+ return $self->{product_id}
+ ? Bugzilla::Product->new({ id => $self->{product_id}, cache => 1 })
+ : undef;
+}
+
+sub product_name {
+ my ($self) = @_;
+ return $self->{product_name} //= $self->{product_id} ? $self->product->name : '';
+}
+
+sub component {
+ my ($self) = @_;
+ return $self->{component_id}
+ ? Bugzilla::Component->new({ id => $self->{component_id}, cache => 1 })
+ : undef;
+}
+
+sub component_name {
+ my ($self) = @_;
+ return $self->{component_name} //= $self->{component_id} ? $self->component->name : '';
+}
+
+sub field_name {
+ return $_[0]->{field_name} //= '';
+}
+
+sub field {
+ my ($self) = @_;
+ return unless $self->{field_name};
+ if (!$self->{field}) {
+ foreach my $field (
+ @{ Bugzilla::Extension::BugmailFilter::FakeField->fake_fields() },
+ @{ Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields() },
+ ) {
+ if ($field->{name} eq $self->{field_name}) {
+ return $self->{field} = $field;
+ }
+ }
+ $self->{field} = Bugzilla::Field->new({ name => $self->{field_name}, cache => 1 });
+ }
+ return $self->{field};
+}
+
+sub relationship {
+ return $_[0]->{relationship};
+}
+
+sub relationship_name {
+ my ($self) = @_;
+ foreach my $rel (@{ FILTER_RELATIONSHIPS() }) {
+ return $rel->{name}
+ if $rel->{value} == $self->{relationship};
+ }
+ return '?';
+}
+
+sub is_exclude {
+ return $_[0]->{action} == 1;
+}
+
+sub is_include {
+ return $_[0]->{action} == 0;
+}
+
+# validators
+
+sub _check_user {
+ my ($class, $user) = @_;
+ $user || ThrowCodeError('param_required', { param => 'user' });
+}
+
+sub _check_field_name {
+ my ($class, $field_name) = @_;
+ return undef unless $field_name;
+ foreach my $rh (@{ FAKE_FIELD_NAMES() }) {
+ return $field_name if $rh->{name} eq $field_name;
+ }
+ return $field_name
+ if $field_name =~ /^tracking\./;
+ Bugzilla::Field->check({ name => $field_name, cache => 1});
+ return $field_name;
+}
+
+# methods
+
+sub matches {
+ my ($self, $args) = @_;
+
+ if ($self->{field_name} && $self->{field_name} ne $args->{field_name}) {
+ return 0;
+ }
+
+ if ($self->{product_id} && $self->{product_id} != $args->{product_id}) {
+ return 0;
+ }
+
+ if ($self->{component_id} && $self->{component_id} != $args->{component_id}) {
+ return 0;
+ }
+
+ if ($self->{relationship} && !$args->{rel_map}->[$self->{relationship}]) {
+ return 0;
+ }
+
+ return 1;
+}
+
+1;
diff --git a/extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl b/extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl
new file mode 100644
index 000000000..d2df77d22
--- /dev/null
+++ b/extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl
@@ -0,0 +1,256 @@
+[%# 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.
+ #%]
+
+<link href="[% "extensions/BugmailFilter/web/style/bugmail-filter.css" FILTER mtime %]"
+ rel="stylesheet" type="text/css">
+<script type="text/javascript"
+ src="[% "extensions/BugmailFilter/web/js/bugmail-filter.js" FILTER mtime %]"></script>
+
+[% SET selectable_products = user.get_selectable_products %]
+[% SET dont_show_button = 1 %]
+
+<script>
+var useclassification = false;
+var first_load = true;
+var last_sel = [];
+var cpts = new Array();
+[% n = 1 %]
+[% FOREACH prod = selectable_products %]
+ cpts['[% n %]'] = [
+ [%- FOREACH comp = prod.components %]'[% comp.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% n = n + 1 %]
+[% END %]
+</script>
+<script type="text/javascript" src="[% 'js/productform.js' FILTER mtime FILTER html %]">
+</script>
+
+<hr>
+<b>Bugmail Filtering</b>
+
+<p>
+ You can instruct [% terms.Bugzilla %] to filter bugmail based on the field
+ that was changed.
+</p>
+
+<table id="add_filter_table">
+<tr>
+ <th>Field:</th>
+ <td>
+ <select name="field" id="field">
+ <option value="">__Any__</option>
+ [% FOREACH field = fields %]
+ <option value="[% field.name FILTER html %]">
+ [% field_descs.${field.name} || field.description FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+ <td class="blurb">
+ the field that was changed
+ </td>
+</tr>
+<tr>
+ <th>Product:</th>
+ <td>
+ <select name="product" id="product" onChange="onFilterProductChange()">
+ <option value="">__Any__</option>
+ [% FOREACH product IN selectable_products %]
+ <option>[% product.name FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+ <td class="blurb">
+ the [% terms.bug %]'s current product
+ </td>
+</tr>
+<tr>
+ <th>Component:</th>
+ <td>
+ <select name="component" id="component">
+ <option value="">__Any__</option>
+ [% FOREACH product IN selectable_products %]
+ [% FOREACH component IN product.components %]
+ <option>[% component.name FILTER html %]</option>
+ [% END %]
+ [% END %]
+ </select>
+ </td>
+ <td class="blurb">
+ the [% terms.bug %]'s current component
+ </td>
+</tr>
+<tr>
+ <th>Relationship:</th>
+ <td>
+ <select name="relationship" id="relationship">
+ <option value="">__Any__</option>
+ [% FOREACH rel IN relationships %]
+ <option value="[% rel.value FILTER html %]">
+ [% rel.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+ <td class="blurb">
+ your relationship with the [% terms.bug %]
+ </td>
+</tr>
+<tr>
+ <th>Action:</th>
+ <td>
+ <select name="action" id="action">
+ <option></option>
+ <option>Exclude</option>
+ <option>Include</option>
+ </select>
+ </td>
+ <td class="blurb">
+ action to take when all conditions match
+ </td>
+</tr>
+<tr>
+ <td></td>
+ <td><input type="submit" id="add_filter" name="add_filter" value="Add"></td>
+</tr>
+</table>
+
+<hr>
+<p>
+ You are currently filtering on:
+</p>
+
+[% IF filters.size %]
+
+ <table id="filters_table">
+ <tr>
+ <td></td>
+ <th>Product</th>
+ <th>Component</th>
+ <th>Field</th>
+ <th>Relationship</th>
+ <th>Action</th>
+ </tr>
+ [% FOREACH filter = filters %]
+ <tr class="[% "row_odd" UNLESS loop.count % 2 %]">
+ <td>
+ <input type="checkbox" name="remove" value="[% filter.id FILTER none %]"
+ onChange="onRemoveChange()">
+ </td>
+ <td>[% filter.product ? filter.product.name : 'Any' FILTER html %]</td>
+ <td>[% filter.component ? filter.component.name : 'Any' FILTER html %]</td>
+ <td>[% filter.field ? filter.field.description : 'Any' FILTER html %]</td>
+ <td>[% filter.relationship ? filter.relationship_name : 'Any' FILTER html %]</td>
+ <td>[% filter.action ? 'Exclude' : 'Include' %]</td>
+ </tr>
+ [% END %]
+ <tr>
+ <td></td>
+ <td><input id="remove" name="remove_filter" type="submit" value="Remove Selected"></td>
+ </tr>
+ </table>
+
+[% ELSE %]
+
+ <p>
+ <i>You do not have any filters configured.</i>
+ </p>
+
+[% END %]
+
+<hr>
+<p>
+ This feature provides fine-grained control over what changes to [% terms.bugs
+ %] will result in an email notification. These filters are applied
+ <b>after</b> the rules configured on the
+ <a href="userprefs.cgi?tab=email">Email Preferences</a> tab.
+</p>
+<p>
+ If multiple filters are applicable to the same [% terms.bug %] change,
+ <b>include</b> filters override <b>exclude</b> filters.
+</p>
+<p>
+ Examples:
+</p>
+<p>
+ To never receive changes made to the "QA Whiteboard" field for [% terms.bugs %]
+ where you are not the assignee:<br>
+ <table class="example_filter_table">
+ <tr>
+ <th>Field:</th>
+ <td>QA Whiteboard</td>
+ </tr>
+ <tr>
+ <th>Product:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Component:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Relationship:</th>
+ <td>Not Assignee</td>
+ </tr>
+ <tr>
+ <th>Action:</th>
+ <td>Exclude</td>
+ </tr>
+ </table>
+</p>
+<p>
+ To receive just comments made to Firefox [% terms.bugs %], and no other
+ changes, you require two filters. First an <b>exclude</b> filter to drop all
+ changes made to [% terms.bugs %] in that product:<br>
+ <table class="example_filter_table">
+ <tr>
+ <th>Field:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Product:</th>
+ <td>Firefox</td>
+ </tr>
+ <tr>
+ <th>Component:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Relationship:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Action:</th>
+ <td>Exclude</td>
+ </tr>
+ </table>
+ <br>
+ Then an <b>include</b> filter to indicate that you want to receive
+ comments:<br>
+ <table class="example_filter_table">
+ <tr>
+ <th>Field:</th>
+ <td>Comment Created</td>
+ </tr>
+ <tr>
+ <th>Product:</th>
+ <td>Firefox</td>
+ </tr>
+ <tr>
+ <th>Component:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Relationship:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Action:</th>
+ <td>Include</td>
+ </tr>
+ </table>
+</p>
diff --git a/extensions/BugmailFilter/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/BugmailFilter/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
new file mode 100644
index 000000000..95ffdee99
--- /dev/null
+++ b/extensions/BugmailFilter/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
@@ -0,0 +1,14 @@
+[%# 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.
+ #%]
+
+[% tabs = tabs.import([{
+ name => "bugmail_filter",
+ label => "Bugmail Filtering",
+ link => "userprefs.cgi?tab=bugmail_filter",
+ saveable => 1
+ }]) %]
diff --git a/extensions/BugmailFilter/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/BugmailFilter/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..380c07ee5
--- /dev/null
+++ b/extensions/BugmailFilter/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 == "bugmail_filter_exists" %]
+ [% title = "Filter Already Exists" %]
+ A filter already exists with the selected criteria.
+
+[% END %]
diff --git a/extensions/BugmailFilter/web/js/bugmail-filter.js b/extensions/BugmailFilter/web/js/bugmail-filter.js
new file mode 100644
index 000000000..e298a60f1
--- /dev/null
+++ b/extensions/BugmailFilter/web/js/bugmail-filter.js
@@ -0,0 +1,37 @@
+/* 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;
+
+function onFilterProductChange() {
+ selectProduct(Dom.get('product'), Dom.get('component'), null, null, '__Any__');
+ Dom.get('component').disabled = Dom.get('product').value == '';
+}
+
+function onFilterActionChange() {
+ var value = Dom.get('action').value;
+ Dom.get('add_filter').disabled = value == '';
+}
+
+function onRemoveChange() {
+ var cbs = Dom.get('filters_table').getElementsByTagName('input');
+ for (var i = 0, l = cbs.length; i < l; i++) {
+ if (cbs[i].checked) {
+ Dom.get('remove').disabled = false;
+ return;
+ }
+ }
+ Dom.get('remove').disabled = true;
+}
+
+Event.onDOMReady(function() {
+ Event.on('action', 'change', onFilterActionChange);
+ onFilterProductChange();
+ onFilterActionChange();
+ onRemoveChange();
+});
diff --git a/extensions/BugmailFilter/web/style/bugmail-filter.css b/extensions/BugmailFilter/web/style/bugmail-filter.css
new file mode 100644
index 000000000..193cf4469
--- /dev/null
+++ b/extensions/BugmailFilter/web/style/bugmail-filter.css
@@ -0,0 +1,44 @@
+/* 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. */
+
+#add_filter_table th {
+ text-align: right;
+}
+
+.example_filter_table {
+ margin-left: 2em;
+}
+
+.example_filter_table th {
+ text-align: right;
+ font-weight: normal;
+}
+
+.example_filter_table td {
+ padding-left: 1em;
+}
+
+#add_filter_table .blurb {
+ font-style: italic;
+ padding-left: 2em;
+}
+
+#filters_table {
+ margin-bottom: 1em;
+ border-spacing: 0;
+}
+
+#filters_table th, #filters_table td {
+ text-align: left;
+ padding-right: 1em;
+ padding: 2px;
+}
+
+#filters_table .row_odd {
+ background-color: #eeeeee;
+ color: #000000;
+}
diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm
index 4b7060817..61822d076 100644
--- a/extensions/Review/Extension.pm
+++ b/extensions/Review/Extension.pm
@@ -89,8 +89,11 @@ sub _bug_mentors {
push(@{ $self->{bug_mentors} },
Bugzilla::User->new({ id => $mentor_id, cache => 1 }));
}
+ $self->{bug_mentors} = [
+ sort { $a->login cmp $b->login } @{ $self->{bug_mentors} }
+ ];
}
- return [ sort { $a->login cmp $b->login } @{ $self->{bug_mentors} } ];
+ return $self->{bug_mentors};
}
sub _bug_is_mentor {
diff --git a/extensions/TrackingFlags/Extension.pm b/extensions/TrackingFlags/Extension.pm
index 3ba9e768b..9b3d46ad2 100644
--- a/extensions/TrackingFlags/Extension.pm
+++ b/extensions/TrackingFlags/Extension.pm
@@ -27,6 +27,17 @@ use Bugzilla::Product;
our $VERSION = '1';
+BEGIN {
+ *Bugzilla::tracking_flag_names = \&_tracking_flag_names;
+}
+
+sub _tracking_flag_names {
+ # return just a list of names, hitting the database directly to avoid the
+ # overhead of object creation
+ return Bugzilla->request_cache->{tracking_flag_names} ||=
+ Bugzilla->dbh->selectcol_arrayref("SELECT name FROM tracking_flags");
+}
+
sub page_before_template {
my ($self, $args) = @_;
my $page = $args->{'page_id'};
diff --git a/extensions/TrackingFlags/lib/Admin.pm b/extensions/TrackingFlags/lib/Admin.pm
index 389acde2c..0b19fcf80 100644
--- a/extensions/TrackingFlags/lib/Admin.pm
+++ b/extensions/TrackingFlags/lib/Admin.pm
@@ -343,9 +343,9 @@ sub _update_db_values {
my $value_obj = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id})
|| ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag value', id => $flag->{id} });
my $old_value = $value_obj->value;
+ $value_obj->set_all($object_set);
+ $value_obj->update();
if ($object_set->{value} ne $old_value) {
- $value_obj->set_all($object_set);
- $value_obj->update();
Bugzilla::Extension::TrackingFlags::Flag::Bug->update_all_values({
value_obj => $value_obj,
old_value => $old_value,