From dbfd6207290d1eee53fddec4c7c3b4aac0b2d47a Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Wed, 8 Apr 2015 18:48:36 +0100 Subject: Bug 1051056: The REST API needs to be versioned so that new changes can be made that do not break compatibility r=dylan,a=glob --- Bugzilla/API/1_0/Resource/Product.pm | 1013 ++++++++++++++++++++++++++++++++++ 1 file changed, 1013 insertions(+) create mode 100644 Bugzilla/API/1_0/Resource/Product.pm (limited to 'Bugzilla/API/1_0/Resource/Product.pm') diff --git a/Bugzilla/API/1_0/Resource/Product.pm b/Bugzilla/API/1_0/Resource/Product.pm new file mode 100644 index 000000000..0f393e207 --- /dev/null +++ b/Bugzilla/API/1_0/Resource/Product.pm @@ -0,0 +1,1013 @@ +# 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::API::1_0::Resource::Product; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Constants; +use Bugzilla::API::1_0::Util; + +use Bugzilla::Product; +use Bugzilla::User; +use Bugzilla::Error; +use Bugzilla::Constants; + +use Moo; + +extends 'Bugzilla::API::1_0::Resource'; + +############## +# Constants # +############## + +use constant READ_ONLY => qw( + get + get_accessible_products + get_enterable_products + get_selectable_products +); + +use constant PUBLIC_METHODS => qw( + create + get + get_accessible_products + get_enterable_products + get_selectable_products + update +); + +use constant MAPPED_FIELDS => { + has_unconfirmed => 'allows_unconfirmed', + is_open => 'is_active', +}; + +use constant MAPPED_RETURNS => { + allows_unconfirmed => 'has_unconfirmed', + defaultmilestone => 'default_milestone', + isactive => 'is_open', +}; + +use constant FIELD_MAP => { + has_unconfirmed => 'allows_unconfirmed', + is_open => 'isactive', +}; + +sub REST_RESOURCES { + my $rest_resources = [ + qr{^/product_accessible$}, { + GET => { + method => 'get_accessible_products' + } + }, + qr{^/product_enterable$}, { + GET => { + method => 'get_enterable_products' + } + }, + qr{^/product_selectable$}, { + GET => { + method => 'get_selectable_products' + } + }, + qr{^/product$}, { + GET => { + method => 'get' + }, + POST => { + method => 'create', + success_code => STATUS_CREATED + } + }, + qr{^/product/([^/]+)$}, { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + } + }, + ]; + return $rest_resources; +} + +############ +# Methods # +############ + +BEGIN { *get_products = \&get } + +# Get the ids of the products the user can search +sub get_selectable_products { + Bugzilla->switch_to_shadow_db(); + return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]}; +} + +# Get the ids of the products the user can enter bugs against +sub get_enterable_products { + Bugzilla->switch_to_shadow_db(); + return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]}; +} + +# Get the union of the products the user can search and enter bugs against. +sub get_accessible_products { + Bugzilla->switch_to_shadow_db(); + return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]}; +} + +# Get a list of actual products, based on list of ids or names +sub get { + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + my $user = Bugzilla->user; + + defined $params->{ids} || defined $params->{names} || defined $params->{type} + || ThrowCodeError("params_required", { function => "Product.get", + params => ['ids', 'names', 'type'] }); + Bugzilla->switch_to_shadow_db(); + + my $products = []; + if (defined $params->{type}) { + my %product_hash; + foreach my $type (@{ $params->{type} }) { + my $result = []; + if ($type eq 'accessible') { + $result = $user->get_accessible_products(); + } + elsif ($type eq 'enterable') { + $result = $user->get_enterable_products(); + } + elsif ($type eq 'selectable') { + $result = $user->get_selectable_products(); + } + else { + ThrowUserError('get_products_invalid_type', + { type => $type }); + } + map { $product_hash{$_->id} = $_ } @$result; + } + $products = [ values %product_hash ]; + } + else { + $products = $user->get_accessible_products; + } + + my @requested_products; + + if (defined $params->{ids}) { + # Create a hash with the ids the user wants + my %ids = map { $_ => 1 } @{$params->{ids}}; + + # Return the intersection of this, by grepping the ids from $products. + push(@requested_products, + grep { $ids{$_->id} } @$products); + } + + if (defined $params->{names}) { + # Create a hash with the names the user wants + my %names = map { lc($_) => 1 } @{$params->{names}}; + + # Return the intersection of this, by grepping the names + # from $products, union'ed with products found by ID to + # avoid duplicates + foreach my $product (grep { $names{lc $_->name} } + @$products) { + next if grep { $_->id == $product->id } + @requested_products; + push @requested_products, $product; + } + } + + # If we just requested a specific type of products without + # specifying ids or names, then return the entire list. + if (!defined $params->{ids} && !defined $params->{names}) { + @requested_products = @$products; + } + + # Now create a result entry for each. + my @products = map { $self->_product_to_hash($params, $_) } + @requested_products; + return { products => \@products }; +} + +sub create { + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", { group => "editcomponents", + action => "add", + object => "products"}); + # Create product + my $args = { + name => $params->{name}, + description => $params->{description}, + version => $params->{version}, + defaultmilestone => $params->{default_milestone}, + # create_series has no default value. + create_series => defined $params->{create_series} ? + $params->{create_series} : 1 + }; + foreach my $field (qw(has_unconfirmed is_open classification)) { + if (defined $params->{$field}) { + my $name = FIELD_MAP->{$field} || $field; + $args->{$name} = $params->{$field}; + } + } + my $product = Bugzilla::Product->create($args); + return { id => as_int($product->id) }; +} + +sub update { + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", { group => "editcomponents", + action => "edit", + object => "products" }); + + defined($params->{names}) || defined($params->{ids}) + || ThrowCodeError('params_required', + { function => 'Product.update', params => ['ids', 'names'] }); + + my $product_objects = params_to_objects($params, 'Bugzilla::Product'); + + my $values = translate($params, MAPPED_FIELDS); + + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; + + $dbh->bz_start_transaction(); + foreach my $product (@$product_objects) { + $product->set_all($values); + } + + my %changes; + foreach my $product (@$product_objects) { + my $returned_changes = $product->update(); + $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $product (@$product_objects) { + my %hash = ( + id => $product->id, + changes => {}, + ); + + foreach my $field (keys %{ $changes{$product->id} }) { + my $change = $changes{$product->id}->{$field}; + $hash{changes}{$field} = { + removed => as_string($change->[0]), + added => as_string($change->[1]) + }; + } + + push(@result, \%hash); + } + + return { products => \@result }; +} + +sub _product_to_hash { + my ($self, $params, $product) = @_; + + my $field_data = { + id => as_int($product->id), + name => as_string($product->name), + description => as_string($product->description), + is_active => as_boolean($product->is_active), + default_milestone => as_string($product->default_milestone), + has_unconfirmed => as_boolean($product->allows_unconfirmed), + classification => as_string($product->classification->name), + }; + if (filter_wants($params, 'components')) { + $field_data->{components} = [map { + $self->_component_to_hash($_, $params) + } @{$product->components}]; + } + if (filter_wants($params, 'versions')) { + $field_data->{versions} = [map { + $self->_version_to_hash($_, $params) + } @{$product->versions}]; + } + if (filter_wants($params, 'milestones')) { + $field_data->{milestones} = [map { + $self->_milestone_to_hash($_, $params) + } @{$product->milestones}]; + } + return filter($params, $field_data); +} + +sub _component_to_hash { + my ($self, $component, $params) = @_; + my $field_data = filter $params, { + id => as_int($component->id), + name => as_string($component->name), + description => as_string($component->description), + default_assigned_to => + as_email($component->default_assignee->login), + default_qa_contact => + as_email($component->default_qa_contact ? + $component->default_qa_contact->login : ""), + sort_key => 0, # sort_key is returned to match Bug.fields + is_active => as_boolean($component->is_active), + }, undef, 'components'; + + if (filter_wants($params, 'flag_types', undef, 'components')) { + $field_data->{flag_types} = { + bug => + [map { + $self->_flag_type_to_hash($_) + } @{$component->flag_types->{'bug'}}], + attachment => + [map { + $self->_flag_type_to_hash($_) + } @{$component->flag_types->{'attachment'}}], + }; + } + + return $field_data; +} + +sub _flag_type_to_hash { + my ($self, $flag_type, $params) = @_; + return filter $params, { + id => as_int($flag_type->id), + name => as_string($flag_type->name), + description => as_string($flag_type->description), + cc_list => as_string($flag_type->cc_list), + sort_key => as_int($flag_type->sortkey), + is_active => as_boolean($flag_type->is_active), + is_requestable => as_boolean($flag_type->is_requestable), + is_requesteeble => as_boolean($flag_type->is_requesteeble), + is_multiplicable => as_boolean($flag_type->is_multiplicable), + grant_group => as_int($flag_type->grant_group_id), + request_group => as_int($flag_type->request_group_id), + }, undef, 'flag_types'; +} + +sub _version_to_hash { + my ($self, $version, $params) = @_; + return filter $params, { + id => as_int($version->id), + name => as_string($version->name), + sort_key => 0, # sort_key is returened to match Bug.fields + is_active => as_boolean($version->is_active), + }, undef, 'versions'; +} + +sub _milestone_to_hash { + my ($self, $milestone, $params) = @_; + return filter $params, { + id => as_int($milestone->id), + name => as_string($milestone->name), + sort_key => as_int($milestone->sortkey), + is_active => as_boolean($milestone->is_active), + }, undef, 'milestones'; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Resource::Product - The Product API + +=head1 DESCRIPTION + +This part of the Bugzilla API allows you to list the available Products and +get information about them. + +=head1 METHODS + +=head2 get_selectable_products + +=over + +=item B + +Returns a list of the ids of the products the user can search on. + +=item B + +GET /rest/product_selectable + +the returned data format is same as below. + +=item B (none) + +=item B + +A hash containing one item, C, that contains an array of product +ids. + +=item B (none) + +=item B + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 get_enterable_products + +=over + +=item B + +Returns a list of the ids of the products the user can enter bugs +against. + +=item B + +GET /rest/product_enterable + +the returned data format is same as below. + +=item B (none) + +=item B + +A hash containing one item, C, that contains an array of product +ids. + +=item B (none) + +=item B + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 get_accessible_products + +=over + +=item B + +Returns a list of the ids of the products the user can search or enter +bugs against. + +=item B + +GET /rest/product_accessible + +the returned data format is same as below. + +=item B (none) + +=item B + +A hash containing one item, C, that contains an array of product +ids. + +=item B (none) + +=item B + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 get + +=over + +=item B + +Returns a list of information about the products passed to it. + +B: You must at least specify one of C or C. + +B: Can also be called as "get_products" for compatibilty with Bugzilla 3.0 API. + +=item B + +To return information about a specific groups of products such as +C, C, or C: + +GET /rest/product?type=accessible + +To return information about a specific product by C or C: + +GET /rest/product/ + +You can also return information about more than one specific product +by using the following in your query string: + +GET /rest/product?ids=1&ids=2&ids=3 or GET /product?names=ProductOne&names=Product2 + +the returned data format is same as below. + +=item B + +In addition to the parameters below, this method also accepts the +standard L and +L arguments. + +This RPC call supports sub field restrictions. + +=over + +=item C + +An array of product ids + +=item C + +An array of product names + +=item C + +The group of products to return. Valid values are: C (default), +C, and C. C can be a single value or an array +of values if more than one group is needed with duplicates removed. + +=back + +=item B + +A hash containing one item, C, that is an array of +hashes. Each hash describes a product, and has the following items: + +=over + +=item C + +C An integer id uniquely identifying the product in this installation only. + +=item C + +C The name of the product. This is a unique identifier for the +product. + +=item C + +C A description of the product, which may contain HTML. + +=item C + +C A boolean indicating if the product is active. + +=item C + +C The name of the default milestone for the product. + +=item C + +C Indicates whether the UNCONFIRMED bug status is available +for this product. + +=item C + +C The classification name for the product. + +=item C + +C An array of hashes, where each hash describes a component, and has the +following items: + +=over + +=item C + +C An integer id uniquely identifying the component in this installation +only. + +=item C + +C The name of the component. This is a unique identifier for this +component. + +=item C + +C A description of the component, which may contain HTML. + +=item C + +C The login name of the user to whom new bugs will be assigned by +default. + +=item C + +C The login name of the user who will be set as the QA Contact for +new bugs by default. Empty string if the QA contact is not defined. + +=item C + +C Components, when displayed in a list, are sorted first by this integer +and then secondly by their name. + +=item C + +C A boolean indicating if the component is active. Inactive +components are not enabled for new bugs. + +=item C + +A hash containing the two items C and C that each contains an +array of hashes, where each hash describes a flagtype, and has the +following items: + +=over + +=item C + +C Returns the ID of the flagtype. + +=item C + +C Returns the name of the flagtype. + +=item C + +C Returns the description of the flagtype. + +=item C + +C Returns the concatenated CC list for the flagtype, as a single string. + +=item C + +C Returns the sortkey of the flagtype. + +=item C + +C Returns whether the flagtype is active or disabled. Flags being +in a disabled flagtype are not deleted. It only prevents you from +adding new flags to it. + +=item C + +C Returns whether you can request for the given flagtype +(i.e. whether the '?' flag is available or not). + +=item C + +C Returns whether you can ask someone specifically or not. + +=item C + +C Returns whether you can have more than one flag for the given +flagtype in a given bug/attachment. + +=item C + +C the group id that is allowed to grant/deny flags of this type. +If the item is not included all users are allowed to grant/deny this +flagtype. + +=item C + +C the group id that is allowed to request the flag if the flag +is of the type requestable. If the item is not included all users +are allowed request this flagtype. + +=back + +=back + +=item C + +C An array of hashes, where each hash describes a version, and has the +following items: C, C and C. + +=item C + +C An array of hashes, where each hash describes a milestone, and has the +following items: C, C and C. + +=back + +Note, that if the user tries to access a product that is not in the +list of accessible products for the user, or a product that does not +exist, that is silently ignored, and no information about that product +is returned. + +=item B (none) + +=item B + +=over + +=item In Bugzilla B<4.2>, C was added as an input parameter. + +=item In Bugzilla B<4.2>, C, C, C, +C, C and C were added to +the fields returned by C as a replacement for C, which has +been removed. + +=item In Bugzilla B<4.4>, C was added to the fields returned +by C. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 Product Creation and Modification + +=head2 create + +=over + +=item B + +This allows you to create a new product in Bugzilla. + +=item B + +POST /rest/product + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B + +Some params must be set, or an error will be thrown. These params are +marked B. + +=over + +=item C + +B C The name of this product. Must be globally unique +within Bugzilla. + +=item C + +B C A description for this product. Allows some simple HTML. + +=item C + +B C The default version for this product. + +=item C + +C Allow the UNCONFIRMED status to be set on bugs in this product. +Default: true. + +=item C + +C The name of the Classification which contains this product. + +=item C + +C The default milestone for this product. Default '---'. + +=item C + +C True if the product is currently allowing bugs to be entered +into it. Default: true. + +=item C + +C True if you want series for New Charts to be created for this +new product. Default: true. + +=back + +=item B + +A hash with one element, id. This is the id of the newly-filed product. + +=item B + +=over + +=item 51 (Classification does not exist) + +You must specify an existing classification name. + +=item 700 (Product blank name) + +You must specify a non-blank name for this product. + +=item 701 (Product name too long) + +The name specified for this product was longer than the maximum +allowed length. + +=item 702 (Product name already exists) + +You specified the name of a product that already exists. +(Product names must be globally unique in Bugzilla.) + +=item 703 (Product must have description) + +You must specify a description for this product. + +=item 704 (Product must have version) + +You must specify a version for this product. + +=back + +=item B + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 update + +=over + +=item B + +This allows you to update a product in Bugzilla. + +=item B + +PUT /rest/product/ + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C and C params will be overridden as +it is pulled from the URL path. + +=item B + +B The following parameters specify which products you are updating. +You must set one or both of these parameters. + +=over + +=item C + +C of Cs. Numeric ids of the products that you wish to update. + +=item C + +C of Cs. Names of the products that you wish to update. + +=back + +B The following parameters specify the new values you want to set for +the products you are updating. + +=over + +=item C + +C A new name for this product. If you try to set this while updating more +than one product, an error will occur, as product names must be unique. + +=item C + +C When a new bug is filed, what milestone does it get by default if the +user does not choose one? Must represent a milestone that is valid for this product. + +=item C + +C Update the long description for these products to this value. + +=item C + +C Allow the UNCONFIRMED status to be set on bugs in products. + +=item C + +C True if the product is currently allowing bugs to be entered +into it, False otherwise. + +=back + +=item B + +A C with a single field "products". This points to an array of hashes +with the following fields: + +=over + +=item C + +C The id of the product that was updated. + +=item C + +C The changes that were actually done on this product. The keys are +the names of the fields that were changed, and the values are a hash +with two keys: + +=over + +=item C + +C The value that this field was changed to. + +=item C + +C The value that was previously set in this field. + +=back + +Note that booleans will be represented with the strings '1' and '0'. + +Here's an example of what a return value might look like: + + { + products => [ + { + id => 123, + changes => { + name => { + removed => 'FooName', + added => 'BarName' + }, + has_unconfirmed => { + removed => '1', + added => '0', + } + } + } + ] + } + +=item B + +=over + +=item 700 (Product blank name) + +You must specify a non-blank name for this product. + +=item 701 (Product name too long) + +The name specified for this product was longer than the maximum +allowed length. + +=item 702 (Product name already exists) + +You specified the name of a product that already exists. +(Product names must be globally unique in Bugzilla.) + +=item 703 (Product must have description) + +You must specify a description for this product. + +=item 705 (Product must define a default milestone) + +You must define a default milestone. + +=back + +=back + +=item B + +=over + +=item Added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 B + +=over + +=item REST_RESOURCES + +=item get_products + +=back -- cgit v1.2.3-24-g4f1b