diff options
Diffstat (limited to 'extensions/Ember')
-rw-r--r-- | extensions/Ember/Config.pm | 19 | ||||
-rw-r--r-- | extensions/Ember/Extension.pm | 22 | ||||
-rw-r--r-- | extensions/Ember/lib/FakeBug.pm | 78 | ||||
-rw-r--r-- | extensions/Ember/lib/WebService.pm | 790 | ||||
-rw-r--r-- | extensions/Ember/template/en/default/hook/global/user-error-errors.html.tmpl | 4 |
5 files changed, 913 insertions, 0 deletions
diff --git a/extensions/Ember/Config.pm b/extensions/Ember/Config.pm new file mode 100644 index 000000000..e3405146d --- /dev/null +++ b/extensions/Ember/Config.pm @@ -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. + +package Bugzilla::Extension::Ember; + +use 5.10.1; +use strict; + +use constant NAME => 'Ember'; + +use constant REQUIRED_MODULES => []; + +use constant OPTIONAL_MODULES => []; + +__PACKAGE__->NAME; diff --git a/extensions/Ember/Extension.pm b/extensions/Ember/Extension.pm new file mode 100644 index 000000000..1c8b8b4e9 --- /dev/null +++ b/extensions/Ember/Extension.pm @@ -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. + +package Bugzilla::Extension::Ember; + +use 5.10.1; +use strict; +use parent qw(Bugzilla::Extension); + +our $VERSION = '0.01'; + +sub webservice { + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{Ember} = "Bugzilla::Extension::Ember::WebService"; +} + +__PACKAGE__->NAME; diff --git a/extensions/Ember/lib/FakeBug.pm b/extensions/Ember/lib/FakeBug.pm new file mode 100644 index 000000000..46fef4ea7 --- /dev/null +++ b/extensions/Ember/lib/FakeBug.pm @@ -0,0 +1,78 @@ +# 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::Ember::FakeBug; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::Bug; + +our $AUTOLOAD; + +sub new { + my $class = shift; + my $self = shift; + bless $self, $class; + return $self; +} + +sub AUTOLOAD { + my $self = shift; + my $name = $AUTOLOAD; + $name =~ s/.*://; + return exists $self->{$name} ? $self->{$name} : undef; +} + +sub check_can_change_field { + return Bugzilla::Bug::check_can_change_field(@_); +} + +sub id { return undef; } +sub product_obj { return $_[0]->{product_obj}; } +sub reporter { return Bugzilla->user; } + +sub choices { + my $self = shift; + return $self->{'choices'} if exists $self->{'choices'}; + return {} if $self->{'error'}; + my $user = Bugzilla->user; + + my @products = @{ $user->get_enterable_products }; + # The current product is part of the popup, even if new bugs are no longer + # allowed for that product + if (!grep($_->name eq $self->product_obj->name, @products)) { + unshift(@products, $self->product_obj); + } + + my @statuses = @{ Bugzilla::Status->can_change_to }; + + # UNCONFIRMED is only a valid status if it is enabled in this product. + if (!$self->product_obj->allows_unconfirmed) { + @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; + } + + my %choices = ( + bug_status => \@statuses, + product => \@products, + component => $self->product_obj->components, + version => $self->product_obj->versions, + target_milestone => $self->product_obj->milestones, + ); + + my $resolution_field = new Bugzilla::Field({ name => 'resolution' }); + # Don't include the empty resolution in drop-downs. + my @resolutions = grep($_->name, @{ $resolution_field->legal_values }); + $choices{'resolution'} = \@resolutions; + + $self->{'choices'} = \%choices; + return $self->{'choices'}; +} + +1; + diff --git a/extensions/Ember/lib/WebService.pm b/extensions/Ember/lib/WebService.pm new file mode 100644 index 000000000..94990c881 --- /dev/null +++ b/extensions/Ember/lib/WebService.pm @@ -0,0 +1,790 @@ +# 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::Ember::WebService; + +use 5.10.1; +use strict; +use warnings; + +use parent qw(Bugzilla::WebService + Bugzilla::WebService::Bug + Bugzilla::WebService::Product); + +use Bugzilla::Bug; +use Bugzilla::Component; +use Bugzilla::Product; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Field; +use Bugzilla::Util qw(trick_taint); + +use Bugzilla::Extension::Ember::FakeBug; + +use Scalar::Util qw(blessed); +use Storable qw(dclone); + +use constant DATE_FIELDS => { + show => ['last_updated'], +}; + +use constant FIELD_TYPE_MAP => { + 0 => 'unknown', + 1 => 'freetext', + 2 => 'single_select', + 3 => 'multiple_select', + 4 => 'textarea', + 5 => 'datetime', + 6 => 'date', + 7 => 'bug_id', + 8 => 'bug_urls', + 9 => 'keywords', + 99 => 'extension' +}; + +use constant NON_EDIT_FIELDS => qw( + assignee_accessible + bug_group + bug_id + commenter + cclist_accessible + content + creation_ts + days_elapsed + everconfirmed + qacontact_accessible + reporter + reporter_accessible + restrict_comments + tag + votes +); + +use constant BUG_CHOICE_FIELDS => qw( + bug_status + component + product + resolution + target_milestone + version +); + +use constant DEFAULT_VALUE_MAP => { + op_sys => 'defaultopsys', + rep_platform => 'defaultplatform', + priority => 'defaultpriority', + bug_severity => 'defaultseverity' +}; + +sub API_NAMES { + # Internal field names converted to the API equivalents + my %api_names = reverse %{ Bugzilla::Bug::FIELD_MAP() }; + return \%api_names; +} + +############### +# API Methods # +############### + +sub create { + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->switch_to_shadow_db(); + + my $product = delete $params->{product}; + $product || ThrowCodeError('params_required', + { function => 'Ember.create', params => ['product'] }); + + my $product_obj = Bugzilla::Product->check($product); + + my $fake_bug = Bugzilla::Extension::Ember::FakeBug->new( + { product_obj => $product_obj, reporter_id => Bugzilla->user->id }); + + my @fields = $self->_get_fields($fake_bug); + + return { + fields => \@fields + }; +} + +sub show { + my ($self, $params) = @_; + my (@fields, $attachments, $comments, $data); + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + Bugzilla->switch_to_shadow_db(); + + # Throw error if token was provided and user is not logged + # in meaning token was invalid/expired. + if (exists $params->{token} && !$user->id) { + ThrowUserError('invalid_token'); + } + + my $bug_id = delete $params->{id}; + $bug_id || ThrowCodeError('params_required', + { function => 'Ember.show', params => ['id'] }); + + my $bug = Bugzilla::Bug->check($bug_id); + + my $bug_hash = $self->_bug_to_hash($bug, $params); + + # Only return changes since last_updated if provided + my $last_updated = delete $params->{last_updated}; + if ($last_updated) { + trick_taint($last_updated); + + my $updated_fields = + $dbh->selectcol_arrayref('SELECT fieldid FROM bugs_activity + WHERE bug_when > ? AND bug_id = ?', + undef, ($last_updated, $bug->id)); + + # Find any comments created since the last_updated date + $comments = $self->comments({ ids => $bug_id, new_since => $last_updated }); + $comments = $comments->{bugs}->{$bug_id}->{comments}; + + # Find any new attachments or modified attachments since the + # last_updated date + my $updated_attachments = + $dbh->selectcol_arrayref('SELECT attach_id FROM attachments + WHERE (creation_ts > ? OR modification_time > ?) + AND bug_id = ?', + undef, ($last_updated, $last_updated, $bug->id)); + if ($updated_attachments) { + $attachments = $self->attachments({ attachment_ids => $updated_attachments, + exclude_fields => ['data'] }); + $attachments = [ map { $attachments->{attachments}->{$_} } + keys %{ $attachments->{attachments} } ]; + } + + if (@$updated_fields || @$comments || @$updated_attachments) { + # Also add in the delta_ts value which is in the bugs_activity + # entries + push(@$updated_fields, get_field_id('delta_ts')); + @fields = $self->_get_fields($bug, $updated_fields); + } + } + # Return all the things + else { + @fields = $self->_get_fields($bug); + $comments = $self->comments({ ids => $bug_id }); + $comments = $comments->{bugs}->{$bug_id}->{comments}; + $attachments = $self->attachments({ ids => $bug_id, + exclude_fields => ['data'] }); + $attachments = $attachments->{bugs}->{$bug_id} || undef; + + } + + # Place the fields current value along with the field definition + foreach my $field (@fields) { + if (($field->{name} eq 'depends_on' + || $field->{name} eq 'blocks') + && scalar @{ $bug_hash->{$field->{name}} }) + { + my $bug_ids = delete $bug_hash->{$field->{name}}; + $user->visible_bugs($bug_ids); + my $bug_objs = Bugzilla::Bug->new_from_list($bug_ids); + + my @new_list; + foreach my $bug (@$bug_objs) { + my $data; + if ($user->can_see_bug($bug)) { + $data = { + id => $bug->id, + status => $bug->bug_status, + summary => $bug->short_desc + }; + } + else { + $data = { id => $bug->id }; + } + push(@new_list, $data); + } + $field->{current_value} = \@new_list; + } + else { + $field->{current_value} = delete $bug_hash->{$field->{name}} || ''; + } + } + + # Any left over bug values will be added to the field list + # These are extra fields that do not have a corresponding + # Field.pm object + if (!$last_updated) { + foreach my $key (keys %$bug_hash) { + my $field = { + name => $key, + current_value => $bug_hash->{$key} + }; + my $name = Bugzilla::Bug::FIELD_MAP()->{$key} || $key; + $field->{can_edit} = $self->_can_change_field($name, $bug); + push(@fields, $field); + } + } + + # Complete the return data + my $data = { id => $bug->id, fields => \@fields }; + + # Add the comments + $data->{comments} = $comments; + + # Add the attachments + $data->{attachments} = $attachments; + + return $data; +} + +sub search { + my ($self, $params) = @_; + + my $total; + if (exists $params->{offset} && exists $params->{limit}) { + my $count_params = dclone($params); + delete $count_params->{offset}; + delete $count_params->{limit}; + $count_params->{count_only} = 1; + $total = $self->SUPER::search($count_params); + } + + my $result = $self->SUPER::search($params); + $result->{total} = defined $total ? $total : scalar(@{ $result->{bugs} }); + return $result; +} + +################### +# Private Methods # +################### + +sub _get_fields { + my ($self, $bug, $field_ids) = @_; + my $user = Bugzilla->user; + + # Load the field objects we need + my @field_objs; + if ($field_ids) { + # Load just the fields that match the ids provided + @field_objs = @{ Bugzilla::Field->match({ id => $field_ids }) }; + + } + else { + # load up standard fields + @field_objs = @{ Bugzilla->fields({ custom => 0 }) }; + + # Load custom fields + my $cf_params = { product => $bug->product_obj }; + $cf_params->{component} = $bug->component_obj if $bug->can('component_obj'); + $cf_params->{bug_id} = $bug->id if $bug->id; + push(@field_objs, Bugzilla->active_custom_fields($cf_params)); + } + + my $return_groups = my $return_flags = $field_ids ? 0 : 1; + my @fields; + foreach my $field (@field_objs) { + $return_groups = 1 if $field->name eq 'bug_group'; + $return_flags = 1 if $field->name eq 'flagtypes.name'; + + # Skip any special fields containing . in the name such as + # for attachments.*, etc. + next if $field->name =~ /\./; + + # Remove time tracking fields if the user is privileged + next if (grep($field->name eq $_, TIMETRACKING_FIELDS) + && !Bugzilla->user->is_timetracker); + + # These fields should never be set by the user + next if grep($field->name eq $_, NON_EDIT_FIELDS); + + # We already selected a product so no need to display all choices + # Might as well skip classification for new bugs as well. + next if (!$bug->id && ($field->name eq 'product' || $field->name eq 'classification')); + + # Skip assigned_to and qa_contact for new bugs if user not in + # editbugs group + next if (!$bug->id + && ($field->name eq 'assigned_to' || $field->name eq 'qa_contact') + && !$user->in_group('editbugs', $bug->product_obj->id)); + + # Do not display obsolete fields or fields that should be displayed for create bug form + next if (!$bug->id && $field->custom + && ($field->obsolete || !$field->enter_bug)); + + push(@fields, $self->_field_to_hash($field, $bug)); + } + + # Add group information as separate field + if ($return_groups) { + push(@fields, { + description => $self->type('string', 'Groups'), + is_custom => $self->type('boolean', 0), + is_mandatory => $self->type('boolean', 0), + name => $self->type('string', 'groups'), + values => [ map { $self->_group_to_hash($_, $bug) } + @{ $bug->product_obj->groups_available } ] + }); + } + + # Add flag information as separate field + if ($return_flags) { + my $flag_hash; + if ($bug->id) { + foreach my $flag_type ('bug', 'attachment') { + my $flag_params = { + target_type => $flag_type, + product_id => $bug->product_obj->id, + component_id => $bug->component_obj->id, + bug_id => $bug->id, + active_or_has_flags => $bug->id, + }; + $flag_hash->{$flag_type} = Bugzilla::Flag->_flag_types($flag_params); + } + } + else { + my $flag_params = { is_active => 1 }; + $flag_hash = $bug->product_obj->flag_types($flag_params); + } + my @flag_values; + foreach my $flag_type ('bug', 'attachment') { + foreach my $flag (@{ $flag_hash->{$flag_type} }) { + push(@flag_values, $self->_flagtype_to_hash($flag, $bug)); + } + } + + push(@fields, { + description => $self->type('string', 'Flags'), + is_custom => $self->type('boolean', 0), + is_mandatory => $self->type('boolean', 0), + name => $self->type('string', 'flags'), + values => \@flag_values + }); + } + + return @fields; +} + +sub _group_to_hash { + my ($self, $group, $bug) = @_; + + my $data = { + description => $self->type('string', $group->description), + name => $self->type('string', $group->name) + }; + + if ($group->name eq $bug->product_obj->default_security_group) { + $data->{security_default} = $self->type('boolean', 1); + } + + return $data; +} + +sub _field_to_hash { + my ($self, $field, $bug) = @_; + + my $data = { + is_custom => $self->type('boolean', $field->custom), + description => $self->type('string', $field->description), + is_mandatory => $self->type('boolean', $field->is_mandatory), + }; + + if ($field->custom) { + $data->{type} = $self->type('string', FIELD_TYPE_MAP->{$field->type}); + } + + # Use the API name if one is present instead of the internal field name + my $field_name = $field->name; + $field_name = API_NAMES->{$field_name} || $field_name; + + if ($field_name eq 'longdesc') { + $field_name = $bug->id ? 'comment' : 'description'; + } + + $data->{name} = $self->type('string', $field_name); + + # Set can_edit true or false if we are editing a current bug + if ($bug->id) { + # 'delta_ts's can_edit is incorrectly set in fielddefs + $data->{can_edit} = $field->name eq 'delta_ts' + ? $self->type('boolean', 0) + : $self->_can_change_field($field, $bug); + } + + # description for creating a new bug, otherwise comment + + # FIXME 'version' and 'target_milestone' types are incorrectly set in fielddefs + if ($field->is_select || $field->name eq 'version' || $field->name eq 'target_milestone') { + $data->{values} = [ $self->_get_field_values($field, $bug) ]; + } + + # Add default values for specific fields if new bug + if (!$bug->id && DEFAULT_VALUE_MAP->{$field->name}) { + my $default_value = Bugzilla->params->{DEFAULT_VALUE_MAP->{$field->name}}; + $data->{default_value} = $default_value; + } + + return $data; +} + +sub _value_to_hash { + my ($self, $value, $bug) = @_; + + my $data = { name=> $self->type('string', $value->name) }; + + if ($bug->{bug_id}) { + $data->{is_active} = $self->type('boolean', $value->is_active); + } + + if ($value->can('sortkey')) { + $data->{sort_key} = $self->type('int', $value->sortkey || 0); + } + + if ($value->isa('Bugzilla::Component')) { + $data->{default_assignee} = $self->_user_to_hash($value->default_assignee); + $data->{initial_cc} = [ map { $self->_user_to_hash($_) } @{ $value->initial_cc } ]; + if (Bugzilla->params->{useqacontact} && $value->default_qa_contact) { + $data->{default_qa_contact} = $self->_user_to_hash($value->default_qa_contact); + } + } + + if ($value->can('description')) { + $data->{description} = $self->type('string', $value->description); + } + + return $data; +} + +sub _user_to_hash { + my ($self, $user) = @_; + + my $data = { + real_name => $self->type('string', $user->name) + }; + + if (Bugzilla->user->id) { + $data->{email} = $self->type('string', $user->email); + } + + return $data; +} + +sub _get_field_values { + my ($self, $field, $bug) = @_; + + # Certain fields are special and should use $bug->choices + # to determine editability and not $bug->check_can_change_field + my @values; + if (grep($field->name eq $_, BUG_CHOICE_FIELDS)) { + @values = @{ $bug->choices->{$field->name} }; + } + else { + # We need to get the values from the product for + # component, version, and milestones. + if ($field->name eq 'component') { + @values = @{ $bug->product_obj->components }; + } + elsif ($field->name eq 'target_milestone') { + @values = @{ $bug->product_obj->milestones }; + } + elsif ($field->name eq 'version') { + @values = @{ $bug->product_obj->versions }; + } + else { + @values = @{ $field->legal_values }; + } + } + + my @filtered_values; + foreach my $value (@values) { + next if !$bug->id && !$value->is_active; + next if $bug->id && !$self->_can_change_field($field, $bug, $value->name); + push(@filtered_values, $value); + } + + return map { $self->_value_to_hash($_, $bug) } @filtered_values; +} + +sub _can_change_field { + my ($self, $field, $bug, $value) = @_; + my $user = Bugzilla->user; + my $field_name = blessed $field ? $field->name : $field; + + # Cannot set resolution on bug creation + return $self->type('boolean', 0) if ($field_name eq 'resolution' && !$bug->{bug_id}); + + # Cannot edit an obsolete or inactive custom field + return $self->type('boolean', 0) if (blessed $field && $field->custom && $field->obsolete); + + # If not a multi-select or single-select, value is not provided + # and we just check if the field itself is editable by the user. + if (!defined $value) { + return $self->type('boolean', $bug->check_can_change_field($field_name, 0, 1)); + } + + return $self->type('boolean', $bug->check_can_change_field($field_name, '', $value)); +} + +sub _flag_to_hash { + my ($self, $flag) = @_; + + my $data = { + id => $self->type('int', $flag->id), + name => $self->type('string', $flag->name), + type_id => $self->type('int', $flag->type_id), + creation_date => $self->type('dateTime', $flag->creation_date), + modification_date => $self->type('dateTime', $flag->modification_date), + status => $self->type('string', $flag->status) + }; + + foreach my $field (qw(setter requestee)) { + my $field_id = $field . "_id"; + $data->{$field} = $self->_user_to_hash($flag->$field) if $flag->$field_id; + } + + $data->{type} = $flag->attach_id ? 'attachment' : 'bug'; + $data->{attach_id} = $flag->attach_id if $flag->attach_id; + + return $data; +} + +sub _flagtype_to_hash { + my ($self, $flagtype, $bug) = @_; + my $user = Bugzilla->user; + + my $cansetflag = $user->can_set_flag($flagtype); + my $canrequestflag = $user->can_request_flag($flagtype); + + my $data = { + id => $self->type('int' , $flagtype->id), + name => $self->type('string' , $flagtype->name), + description => $self->type('string' , $flagtype->description), + type => $self->type('string' , $flagtype->target_type), + is_requestable => $self->type('boolean', $flagtype->is_requestable), + is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), + is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable), + can_set_flag => $self->type('boolean', $cansetflag), + can_request_flag => $self->type('boolean', $canrequestflag) + }; + + my @values; + foreach my $value ('?','+','-') { + push(@values, $self->type('string', $value)); + } + $data->{values} = \@values; + + # if we're creating a bug, we need to return all valid flags for + # this product, as well as inclusions & exclusions so ember can + # display relevant flags once the component is selected + if (!$bug->id) { + my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $bug->product_obj->id); + my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $bug->product_obj->id); + # if we have both inclusions and exclusions, the exclusions are redundant + $exclusions = [] if @$inclusions && @$exclusions; + # no need to return anything if there's just "any component" + $data->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; + $data->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; + } + + return $data; +} + +sub _flagtype_clusions_to_hash { + my ($self, $clusions, $product_id) = @_; + my $result = []; + foreach my $key (keys %$clusions) { + my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); + if ($prod_id == 0 || $prod_id == $product_id) { + if ($comp_id) { + my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 }); + push @$result, $component->name; + } + else { + return [ '' ]; + } + } + } + return $result; +} + +sub rest_resources { + return [ + # create page - single product name + qr{^/ember/create/(.*)$}, { + GET => { + method => 'create', + params => sub { + return { product => $_[0] }; + } + } + }, + # create page - one or more products + qr{^/ember/create$}, { + GET => { + method => 'create' + } + }, + # show bug page - single bug id + qr{^/ember/show/(\d+)$}, { + GET => { + method => 'show', + params => sub { + return { id => $_[0] }; + } + } + }, + # show bug page - one or more bug ids + qr{^/ember/show$}, { + GET => { + method => 'show' + } + }, + # search - wrapper around SUPER::search which also includes the total + # number of bugs when using pagination + qr{^/ember/search$}, { + GET => { + method => 'search', + }, + }, + ]; +}; + +1; + +__END__ + +=head1 NAME + +Bugzilla::Extension::Ember::Webservice - The BMO Ember WebServices API + +=head1 DESCRIPTION + +This module contains API methods that are useful to user's of the Bugzilla Ember +based UI. + +=head1 METHODS + +See L<Bugzilla::WebService> for a description of how parameters are passed, +and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. + +=head2 create + +B<UNSTABLE> + +=over + +=item B<Description> + +This method returns the necessary information for the Bugzilla Ember UI to generate a +bug creation page. + +=item B<Params> + +You pass a field called C<product> that must be a valid Bugzilla product name. + +=over + +=item C<product> (string) - The Bugzilla product name. + +=back + +=item B<Returns> + +=over + +=back + +=item B<Errors> + +=over + +=back + +=item B<History> + +=over + +=item Added in BMO Bugzilla B<4.2>. + +=back + +=back + +=head2 show + +B<UNSTABLE> + +=over + +=item B<Description> + +This method returns the necessary information for the Bugzilla Ember UI to properly +generate a page to edit current bugs. + +=item B<Params> + +You pass a field called C<id> that is the current bug id. + +=over + +=item C<id> (int) - A bug id. + +=back + +=item B<Returns> + +=over + +=back + +=item B<Errors> + +=over + +=back + +=item B<History> + +=over + +=item Added in BMO Bugzilla B<4.0>. + +=back + +=back + +=head2 search + +B<UNSTABLE> + +=over + +=item B<Description> + +A wrapper around Bugzilla's C<search> method which also returns the total of +bugs matching a query, even if the limit and offset parameters are supplied. + +=item B<Params> + +As per Bugzilla::WebService::Bug::search() + +=item B<Returns> + +=over + +=back + +=item B<Errors> + +=over + +=back + +=item B<History> + +=over + +=back + +=back diff --git a/extensions/Ember/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Ember/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..c438af283 --- /dev/null +++ b/extensions/Ember/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,4 @@ +[% IF error == "invalid_token" %] + [% title = "Invalid Token Provided" %] + The token provided is either invalid or expired. You must log in again. +[% END %] |