diff options
author | Byron Jones <glob@mozilla.com> | 2014-07-08 10:40:14 +0200 |
---|---|---|
committer | Byron Jones <glob@mozilla.com> | 2014-07-08 10:40:14 +0200 |
commit | 2f3b5dd7df3e131af6aef3cd5ccf7e8523c1780e (patch) | |
tree | e71ea56398621e038df3c27cc5b87accf7a04968 /extensions | |
parent | d74129306d8d5a903af6fe3957046feb36affdd1 (diff) | |
download | bugzilla-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.pm | 321 | ||||
-rw-r--r-- | extensions/BugmailFilter/lib/Constants.pm | 113 | ||||
-rw-r--r-- | extensions/BugmailFilter/lib/FakeField.pm | 57 | ||||
-rw-r--r-- | extensions/BugmailFilter/lib/Filter.pm | 169 | ||||
-rw-r--r-- | extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl | 256 | ||||
-rw-r--r-- | extensions/BugmailFilter/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl | 14 | ||||
-rw-r--r-- | extensions/BugmailFilter/template/en/default/hook/global/user-error-errors.html.tmpl | 13 | ||||
-rw-r--r-- | extensions/BugmailFilter/web/js/bugmail-filter.js | 37 | ||||
-rw-r--r-- | extensions/BugmailFilter/web/style/bugmail-filter.css | 44 | ||||
-rw-r--r-- | extensions/Review/Extension.pm | 5 | ||||
-rw-r--r-- | extensions/TrackingFlags/Extension.pm | 11 | ||||
-rw-r--r-- | extensions/TrackingFlags/lib/Admin.pm | 4 |
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, |