diff --git a/app/assets/templates/work_packages/cost_object.html b/app/assets/templates/work_packages/cost_object.html
index 21a9d06ee8..016f8b1cc4 100644
--- a/app/assets/templates/work_packages/cost_object.html
+++ b/app/assets/templates/work_packages/cost_object.html
@@ -3,3 +3,4 @@
{{ costObject.props.subject }}
+-
diff --git a/app/assets/templates/work_packages/summarized_cost_entries.html b/app/assets/templates/work_packages/summarized_cost_entries.html
index e08348c62b..42d04b78fa 100644
--- a/app/assets/templates/work_packages/summarized_cost_entries.html
+++ b/app/assets/templates/work_packages/summarized_cost_entries.html
@@ -1,12 +1,13 @@
-
- {{ costType.props.units }}
-
- {{ costType.props.unit }}
- {{ costType.props.unitPlural }}
+
+ {{ costType.props.spentUnits }}
+
+ {{ costType.embedded.costType.props.unit }}
+ {{ costType.embedded.costType.props.unitPlural }}
,
+-
\ No newline at end of file
diff --git a/app/models/cost_type.rb b/app/models/cost_type.rb
index a9d48593d7..994dc28285 100644
--- a/app/models/cost_type.rb
+++ b/app/models/cost_type.rb
@@ -58,6 +58,10 @@ class CostType < ActiveRecord::Base
return nil
end
+ def visible?(user)
+ user.admin?
+ end
+
def to_s
name
end
@@ -92,6 +96,4 @@ class CostType < ActiveRecord::Base
rate.save!
end
end
-
-
end
diff --git a/doc/apiv3.apib b/doc/apiv3.apib
index dfde2d3a75..a44237c207 100644
--- a/doc/apiv3.apib
+++ b/doc/apiv3.apib
@@ -21,9 +21,6 @@ already present in the core API.
| id | Budget id | Integer | x > 0 | READ | |
| subject | Budget name | String | not empty | READ | |
-`spentTime` has its visibility condition changed! It is now only visible when the client is either allowed to view time entries, or if
-he is allowed to see his own time entries in projects where the costs module is enabled.
-
*Note: Further properties of budgets might be added at a future date, however they will require the view budget permission to be displayed.*
## Budget [/api/v3/budgets/{id}]
@@ -157,12 +154,330 @@ he is allowed to see his own time entries in projects where the costs module is
"message": "The specified project does not exist."
}
+# Group Cost Entries
+
+## Linked Properties:
+| Link | Description | Type | Constraints | Supported operations |
+|:-----------:|-------------------------------------------- | ------------- | --------------------- | -------------------- |
+| self | This cost entry | CostEntry | not null | READ |
+| project | The project in which this entry was logged | Project | not null | READ |
+| costType | The type of this entry | CostType | not null | READ |
+| user | The user logging this entry | User | not null | READ |
+| workPackage | The work package that got the entry logged | WorkPackage | not null | READ |
+
+## Properties
+| Property | Description | Type | Constraints | Supported operations | Condition |
+| :---------: | ------------------------------------------- | ----------- | ----------- | -------------------- | --------------------------- |
+| id | cost entry id | Integer | x > 0 | READ | |
+| spentUnits | The amount of units logged in this entry | Float | | READ | |
+| spentOn | The date when the units were spent | Date | | READ | |
+| createdAt | Time of creation | DateTime | | READ | |
+| updatedAt | Time of the most recent change to the entry | DateTime | | READ | |
+
+## Cost Entry [/api/v3/cost_entries/{id}]
+
++ Model
+ + Body
+
+ {
+ "_type": "CostEntry",
+ "_links": {
+ "self": {
+ "href": "/api/v3/cost_entries/1"
+ },
+ "project": {
+ "href": "/api/v3/projects/1"
+ },
+ "costType": {
+ "href": "/api/v3/cost_types/1"
+ },
+ "user": {
+ "href": "/api/v3/users/1"
+ },
+ "workPackage": {
+ "href": "/api/v3/work_packages/1"
+ }
+ },
+ "id": 1,
+ "spentUnits": 3.14,
+ "spentOn": "2015-03-31",
+ "createdAt": "2015-03-31T08:51:20Z",
+ "updatedAt": "2015-03-31T08:51:20Z"
+ }
+
+
+## view Cost Entry [GET]
+
++ Parameters
+ + id (required, integer, `1`) ... Cost Entry id
+
++ Response 200 (application/hal+json)
+
+ [Cost Entry][]
+
++ Response 403 (application/hal+json)
+
+ Returned if the client does not have sufficient permissions.
+
+ **Required permission:** view cost entries **or** view own cost entries (on cost entry's project)
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
+ "message": "You are not allowed to see this cost entry."
+ }
+
+## Cost Entries by work package [/api/v3/work_packages/{id}/cost_entries]
+
++ Model
+ + Body
+
+ {
+ "_links":
+ {
+ "self":
+ {
+ "href": "/api/v3/work_packages/1/cost_entries"
+ }
+ },
+ "total": 1,
+ "count": 1,
+ "_type": "Collection",
+ "_embedded":
+ {
+ "elements": [
+ {
+ "_type": "CostEntry",
+ "_links": {
+ "self": {
+ "href": "/api/v3/cost_entries/1"
+ },
+ "project": {
+ "href": "/api/v3/projects/1"
+ },
+ "costType": {
+ "href": "/api/v3/cost_types/1"
+ },
+ "user": {
+ "href": "/api/v3/users/1"
+ },
+ "workPackage": {
+ "href": "/api/v3/work_packages/1"
+ }
+ },
+ "id": 1,
+ "spentUnits": 3.14,
+ "spentOn": "2015-03-31",
+ "createdAt": "2015-03-31T08:51:20Z",
+ "updatedAt": "2015-03-31T08:51:20Z"
+ }
+ ]
+ }
+ }
+
+## list Cost Entries of a work package [GET]
+
++ Parameters
+ + id (required, integer, `1`) ... work package id
+
++ Response 200 (application/hal+json)
+
+ [Cost Entries by work package][]
+
++ Response 403 (application/hal+json)
+
+ Returned if the client does not have sufficient permissions.
+
+ **Required permission:** view cost entries **or** view own cost entries (on work package's project)
+
+ *Note that you will only receive this error, if you are at least allowed to see the corresponding work package.*
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
+ "message": "You are not allowed to see the cost entries of this work package."
+ }
+
++ Response 404 (application/hal+json)
+
+ Returned if the work package does not exist or the client does not have sufficient permissions
+ to see it.
+
+ **Required permission:** view work package
+
+ *Note: A client without sufficient permissions shall not be able to test for the existence of a work package.
+ That's why a 404 is returned here, even if a 403 might be more appropriate.*
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound",
+ "message": "The specified work package does not exist."
+ }
+
+## Work package costs per type [/api/v3/work_packages/{id}/summarized_costs_by_type]
+
+Returns a list of `AggregatedCostEntry`, with one entry per spent cost type.
+The spent units of all cost entries visible to the current user are summed up for each entry.
+
+An `AggregatedCostEntry` is a stripped down variant of a normal `CostEntry` which only has the link to
+a `CostType` and the amount of `spentUnits`.
+
+*Note that this is a preliminary endpoint. It is subject to future changes or removal.*
+
++ Model
+ + Body
+
+ {
+ "_links":
+ {
+ "self":
+ {
+ "href": "/api/v3/work_packages/1/summarized_costs_by_type"
+ }
+ },
+ "total": 1,
+ "count": 1,
+ "_type": "Collection",
+ "_embedded":
+ {
+ "elements": [
+ {
+ "_type": "AggregatedCostEntry",
+ "_links": {
+ "costType": {
+ "href": "/api/v3/cost_types/1"
+ }
+ },
+ "spentUnits": 31.4
+ }
+ ]
+ }
+ }
+
+## Show aggregated costs of a work package [GET]
+
++ Parameters
+ + id (required, integer, `1`) ... work package id
+
++ Response 200 (application/hal+json)
+
+ [Work package costs per type][]
+
++ Response 403 (application/hal+json)
+
+ Returned if the client does not have sufficient permissions.
+
+ **Required permission:** view cost entries **or** view own cost entries (on work package's project)
+
+ *Note that you will only receive this error, if you are at least allowed to see the corresponding work package.*
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
+ "message": "You are not allowed to see the cost entries of this work package."
+ }
+
++ Response 404 (application/hal+json)
+
+ Returned if the work package does not exist or the client does not have sufficient permissions
+ to see it.
+
+ **Required permission:** view work package
+
+ *Note: A client without sufficient permissions shall not be able to test for the existence of a work package.
+ That's why a 404 is returned here, even if a 403 might be more appropriate.*
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound",
+ "message": "The specified work package does not exist."
+ }
+
+# Group Cost Types
+
+## Linked Properties:
+| Link | Description | Type | Constraints | Supported operations |
+|:---------:|-------------------------------------------- | ------------- | --------------------- | -------------------- |
+| self | This cost type | CostType | not null | READ |
+
+## Properties
+| Property | Description | Type | Constraints | Supported operations | Condition |
+| :---------: | ------------------------------------------- | ----------- | ----------- | -------------------- | --------------------------- |
+| id | cost type id | Integer | x > 0 | READ | |
+| name | cost type name | String | not empty | READ | |
+| unit | The unit in which the costs are measured | String | not empty | READ | |
+| unitPlural | The pluralized form of the unit | String | not empty | READ | |
+| isDefault | `true` for the default type of new entries | Boolean | not null | READ | |
+
+## Cost Type [/api/v3/cost_types/{id}]
+
++ Model
+ + Body
+
+ {
+ "_type": "CostType",
+ "_links": {
+ "self": {
+ "href": "/api/v3/cost_types/1",
+ "title": "Energy cost"
+ }
+ },
+ "id": 1,
+ "name": "Energy cost",
+ "unit": "kWh",
+ "unitPlural": "kWh",
+ "isDefault": true
+ }
+
+
+## view Cost Type [GET]
+
++ Parameters
+ + id (required, integer, `1`) ... Cost Type id
+
++ Response 200 (application/hal+json)
+
+ [Cost Type][]
+
++ Response 403 (application/hal+json)
+
+ Returned if the client does not have sufficient permissions.
+
+ **Required permission:** view cost entries **or** view own cost entries (on any project)
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
+ "message": "You are not allowed to see cost types."
+ }
+
# Group Work Packages
The following properties are only added to work packages in projects where the costs module is activated.
If the costs module is not available on a given work package, there will be no additional properties.
## Linked Properties:
-| Link | Description | Type | Constraints | Supported operations |
-|:----------:|-------------------------------------------- | ------------- | --------------------- | -------------------- |
-| costObject | The budget associated to this work package | Budget | | READ / WRITE |
+| Link | Description | Type | Constraints | Supported operations |
+|:-----------:|-------------------------------------------- | --------------------------------- | --------------------- | -------------------- |
+| costObject | The budget associated to this work package | Budget | | READ / WRITE |
+| costsByType | List of accumulated costs per cost type | Collection of AggregatedCostEntry | | READ / WRITE |
+
+## Properties:
+| Link | Description | Type | Constraints | Supported operations |
+|:------------:|------------------------------------------------------------------- | ------------- | --------------------- | -------------------- |
+| overallCosts | The total amount of user visible costs logged on this work package | String | | READ / WRITE |
+
+`spentTime` has its visibility condition changed! It is now only visible when the client is either allowed to view time entries, or if
+he is allowed to see his own time entries in projects where the costs module is enabled.
diff --git a/frontend/app/openproject-costs-app.js b/frontend/app/openproject-costs-app.js
index 1479ce6ee9..d22bb15811 100644
--- a/frontend/app/openproject-costs-app.js
+++ b/frontend/app/openproject-costs-app.js
@@ -48,7 +48,7 @@ openprojectCostsApp.run(['HookService',
var costsAttributes = {
costObject: null,
overallCosts: null,
- summarizedCostEntries: 'spentUnits',
+ costsByType: null,
};
WorkPackagesOverviewService.addGroup('costs', position);
@@ -60,21 +60,25 @@ openprojectCostsApp.run(['HookService',
setupCostsAttributes();
}
+ HookService.register('workPackageAttributeEditableType', function(params) {
+ switch (params.type) {
+ case 'Budget':
+ return 'dropdown';
+ }
+ return null;
+ });
+
HookService.register('workPackageOverviewAttributes', function(params) {
var directive;
-
switch (params.type) {
- case "spentUnits":
- var summarizedCostEntries = params.workPackage.embedded.summarizedCostEntries;
-
- if (summarizedCostEntries && summarizedCostEntries.length > 0) {
- directive = "summarized-cost-entries";
+ case "Collection":
+ if (params.field !== 'costsByType') {
+ break;
}
+ directive = "summarized-cost-entries";
break;
- case "costObject":
- if (params.workPackage.embedded.costObject) {
- directive = "cost-object";
- }
+ case "Budget":
+ directive = "cost-object";
break;
}
diff --git a/frontend/app/work_packages/directives/cost-object-directive.js b/frontend/app/work_packages/directives/cost-object-directive.js
index 3180af323d..b96128b9a7 100644
--- a/frontend/app/work_packages/directives/cost-object-directive.js
+++ b/frontend/app/work_packages/directives/cost-object-directive.js
@@ -28,13 +28,28 @@
angular.module('openproject.workPackages.directives')
-.directive('costObject', [function() {
+.directive('costObject', ['$timeout', function($timeout) {
return {
restrict: 'E',
+ trasclude: true,
+ require: '^inplaceEditorDisplayPane',
templateUrl: '/assets/work_packages/cost_object.html',
- link: function(scope, element, attributes) {
- scope.costObject = scope.workPackage.embedded.costObject;
- scope.linkToCostObject = '/cost_objects/' + scope.costObject.props.id;
+ link: function(scope, element, attributes, displayPaneController) {
+ scope.$watch(function() {
+ return displayPaneController.getWorkPackage();
+ }, function(workPackage) {
+ scope.workPackage = workPackage;
+ scope.costObject = scope.workPackage.embedded.costObject;
+ if (scope.costObject) {
+ scope.linkToCostObject = '/cost_objects/' + scope.costObject.props.id;
+ }
+ $timeout(function() {
+ element.find('a').on('click', function(e) {
+ e.stopPropagation();
+ });
+ });
+ });
+
}
};
}]);
diff --git a/frontend/app/work_packages/directives/summarized-cost-entries-directive.js b/frontend/app/work_packages/directives/summarized-cost-entries-directive.js
index b4d6ab3d82..601e7067ba 100644
--- a/frontend/app/work_packages/directives/summarized-cost-entries-directive.js
+++ b/frontend/app/work_packages/directives/summarized-cost-entries-directive.js
@@ -31,17 +31,21 @@ angular.module('openproject.workPackages.directives')
.directive('summarizedCostEntries', ['PathHelper', function(PathHelper) {
return {
restrict: 'E',
+ trasclude: true,
+ require: '^inplaceEditorDisplayPane',
templateUrl: '/assets/work_packages/summarized_cost_entries.html',
- link: function(scope, element, attributes) {
- if (scope.workPackage.embedded.summarizedCostEntries) {
- scope.costTypes = scope.workPackage.embedded.summarizedCostEntries;
- }
-
+ link: function(scope, element, attributes, displayPaneController) {
+ scope.workPackage = displayPaneController.getWorkPackage();
+ scope.costTypes = scope
+ .workPackage
+ .embedded
+ .costsByType
+ .embedded.elements;
scope.linkToSummary = function(costType) {
var link = PathHelper.staticWorkPackagePath(scope.workPackage.props.id);
link += '/cost_entries?cost_type_id=' + costType.props.id;
- link += '&project_id=' + scope.workPackage.props.projectId;
+ link += '&project_id=' + scope.workPackage.embedded.project.props.id;
return link;
};
diff --git a/lib/api/v3/cost_entries/aggregated_cost_entry_representer.rb b/lib/api/v3/cost_entries/aggregated_cost_entry_representer.rb
new file mode 100644
index 0000000000..b58c3d5d79
--- /dev/null
+++ b/lib/api/v3/cost_entries/aggregated_cost_entry_representer.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module API
+ module V3
+ module CostEntries
+ # N.B. This class is currently quite specifically crafted for the aggregation of cost entries
+ # of a single work package by their type. This might be improved in the futureā¢
+ class AggregatedCostEntryRepresenter < ::API::Decorators::Single
+ def initialize(cost_type, units)
+ @cost_type = cost_type
+ @spent_units = units
+
+ super(nil)
+ end
+
+ linked_property :cost_type,
+ getter: -> { @cost_type },
+ embed_as: ::API::V3::CostTypes::CostTypeRepresenter
+
+ property :spent_units,
+ exec_context: :decorator,
+ getter: -> (*) { @spent_units }
+
+ def _type
+ 'AggregatedCostEntry'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/cost_entries/cost_entries_api.rb b/lib/api/v3/cost_entries/cost_entries_api.rb
new file mode 100644
index 0000000000..048a0181bc
--- /dev/null
+++ b/lib/api/v3/cost_entries/cost_entries_api.rb
@@ -0,0 +1,58 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'api/v3/cost_types/cost_type_representer'
+
+module API
+ module V3
+ module CostEntries
+ class CostEntriesAPI < ::API::OpenProjectAPI
+ resources :cost_entries do
+ route_param :id do
+ before do
+ @cost_entry = CostEntry.find(params[:id])
+
+ authorize(:view_cost_entries, context: @cost_entry.project) do
+ if current_user == @cost_entry.user
+ authorize(:view_own_cost_entries, context: @cost_entry.project)
+ else
+ raise API::Errors::Unauthorized
+ end
+ end
+ end
+
+ get do
+ CostEntryRepresenter.new(@cost_entry, current_user: current_user)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/cost_entries/cost_entries_by_work_package_api.rb b/lib/api/v3/cost_entries/cost_entries_by_work_package_api.rb
new file mode 100644
index 0000000000..9eb5b04f58
--- /dev/null
+++ b/lib/api/v3/cost_entries/cost_entries_by_work_package_api.rb
@@ -0,0 +1,60 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'api/v3/cost_types/cost_type_representer'
+
+module API
+ module V3
+ module CostEntries
+ class CostEntriesByWorkPackageAPI < ::API::OpenProjectAPI
+ before do
+ authorize_any([:view_cost_entries, :view_own_cost_entries],
+ projects: @work_package.project)
+ @cost_helper = ::OpenProject::Costs::AttributesHelper.new(@work_package, current_user)
+ end
+
+ resources :cost_entries do
+ get do
+ path = api_v3_paths.cost_entries_by_work_package(@work_package.id)
+ cost_entries = @cost_helper.cost_entries
+ CostEntryCollectionRepresenter.new(cost_entries,
+ cost_entries.count,
+ path)
+ end
+ end
+
+ resources :summarized_costs_by_type do
+ get do
+ WorkPackageCostsByTypeRepresenter.new(@work_package, current_user: current_user)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/cost_entries/cost_entry_collection_representer.rb b/lib/api/v3/cost_entries/cost_entry_collection_representer.rb
new file mode 100644
index 0000000000..b3d29c4c4e
--- /dev/null
+++ b/lib/api/v3/cost_entries/cost_entry_collection_representer.rb
@@ -0,0 +1,38 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module API
+ module V3
+ module CostEntries
+ class CostEntryCollectionRepresenter < ::API::Decorators::Collection
+ element_decorator ::API::V3::CostEntries::CostEntryRepresenter
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/cost_entries/cost_entry_representer.rb b/lib/api/v3/cost_entries/cost_entry_representer.rb
new file mode 100644
index 0000000000..a48d664a38
--- /dev/null
+++ b/lib/api/v3/cost_entries/cost_entry_representer.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module API
+ module V3
+ module CostEntries
+ class CostEntryRepresenter < ::API::Decorators::Single
+ self_link title_getter: -> (*) { nil }
+ linked_property :project, embed_as: ::API::V3::Projects::ProjectRepresenter
+ linked_property :user, embed_as: ::API::V3::Users::UserRepresenter
+ linked_property :cost_type, embed_as: ::API::V3::CostTypes::CostTypeRepresenter
+
+ # for now not embedded, because work packages are quite large
+ linked_property :work_package, title_getter: -> (*) { represented.work_package.subject }
+
+ property :id, render_nil: true
+ property :units, as: :spentUnits
+ property :spent_on,
+ exec_context: :decorator,
+ getter: -> (*) { datetime_formatter.format_date(represented.spent_on) }
+ property :created_on,
+ as: 'createdAt',
+ exec_context: :decorator,
+ getter: -> (*) { datetime_formatter.format_datetime(represented.created_on) }
+ property :updated_on,
+ as: 'updatedAt',
+ exec_context: :decorator,
+ getter: -> (*) { datetime_formatter.format_datetime(represented.updated_on) }
+
+ def _type
+ 'CostEntry'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/cost_entries/work_package_costs_by_type_representer.rb b/lib/api/v3/cost_entries/work_package_costs_by_type_representer.rb
new file mode 100644
index 0000000000..6f0173a694
--- /dev/null
+++ b/lib/api/v3/cost_entries/work_package_costs_by_type_representer.rb
@@ -0,0 +1,71 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module API
+ module V3
+ module CostEntries
+ # Ripped from ::API::Decorators::Collection, which does not support injecting
+ # decorators directly
+ # This class should use the Collection class directly (or inherit from it) in the future
+ class WorkPackageCostsByTypeRepresenter < ::API::Decorators::Single
+ link :self do
+ { href: api_v3_paths.summarized_work_package_costs_by_type(represented.id) }
+ end
+
+ property :total,
+ exec_context: :decorator,
+ getter: -> (*) { cost_helper.summarized_cost_entries.size }
+ property :count,
+ exec_context: :decorator,
+ getter: -> (*) { cost_helper.summarized_cost_entries.size }
+
+ collection :elements,
+ getter: -> (*) {
+ cost_helper.summarized_cost_entries.map { |kvp|
+ type = kvp[0]
+ units = kvp[1]
+ ::API::V3::CostEntries::AggregatedCostEntryRepresenter.new(type, units)
+ }
+ },
+ exec_context: :decorator,
+ embedded: true
+
+ private
+
+ def cost_helper
+ @cost_helper ||= ::OpenProject::Costs::AttributesHelper.new(represented, current_user)
+ end
+
+ def _type
+ 'Collection'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/cost_types/cost_type_representer.rb b/lib/api/v3/cost_types/cost_type_representer.rb
index 7c6c332fa6..e76644e4b6 100644
--- a/lib/api/v3/cost_types/cost_type_representer.rb
+++ b/lib/api/v3/cost_types/cost_type_representer.rb
@@ -17,49 +17,19 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#++
-require 'roar/decorator'
-require 'roar/json/hal'
-
module API
module V3
module CostTypes
- class CostTypeRepresenter < Roar::Decorator
- include Roar::JSON::HAL
- include Roar::Hypermedia
- include OpenProject::StaticRouting::UrlHelpers
-
- self.as_strategy = API::Utilities::CamelCasingStrategy.new
-
- def initialize(model, unit_summary, options = {}, *expand)
- @summary = unit_summary
- @work_package = options[:work_package]
- @current_user = options[:current_user]
-
- super(model)
- end
-
- property :_type, exec_context: :decorator
-
+ class CostTypeRepresenter < ::API::Decorators::Single
+ self_link
property :id, render_nil: true
property :name, render_nil: true
- property :units,
- getter: -> (*) {
- cost_entries = @work_package.cost_entries
- .visible(@current_user, @work_package.project)
- .where(cost_type_id: represented.id)
-
- cost_entries.sum(&:units)
- },
- exec_context: :decorator,
- render_nil: true
property :unit,
- exec_context: :decorator,
- getter: -> (*) { @summary[:unit] },
render_nil: true
property :unit_plural,
- exec_context: :decorator,
- getter: -> (*) { @summary[:unit_plural] },
render_nil: true
+ property :is_default,
+ getter: -> (*) { default }
def _type
'CostType'
diff --git a/lib/api/v3/cost_types/cost_types_api.rb b/lib/api/v3/cost_types/cost_types_api.rb
new file mode 100644
index 0000000000..3cbf784980
--- /dev/null
+++ b/lib/api/v3/cost_types/cost_types_api.rb
@@ -0,0 +1,56 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'api/v3/cost_types/cost_type_representer'
+
+module API
+ module V3
+ module CostTypes
+ class CostTypesAPI < ::API::OpenProjectAPI
+ resources :cost_types do
+ before do
+ authorize_any([:view_cost_entries, :view_own_cost_entries],
+ global: true,
+ user: current_user)
+ end
+
+ route_param :id do
+ before do
+ @cost_type = CostType.active.find(params[:id])
+ end
+
+ get do
+ CostTypeRepresenter.new(@cost_type, current_user: current_user)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/open_project/costs/attributes_helper.rb b/lib/open_project/costs/attributes_helper.rb
index 48c4925022..c73a26b072 100644
--- a/lib/open_project/costs/attributes_helper.rb
+++ b/lib/open_project/costs/attributes_helper.rb
@@ -20,8 +20,9 @@
module OpenProject::Costs
class AttributesHelper
- def initialize(work_package)
+ def initialize(work_package, user = User.current)
@work_package = work_package
+ @user = user
end
def overall_costs
@@ -29,7 +30,15 @@ module OpenProject::Costs
end
def summarized_cost_entries
- @summarized_cost_entries ||= compute_summarized_cost_entries
+ @summarized_cost_entries ||= cost_entries.calculate(:sum, :units, group: :cost_type)
+ end
+
+ def time_entries
+ @work_package.time_entries.visible(@user, @work_package.project)
+ end
+
+ def cost_entries
+ @cost_entries ||= @work_package.cost_entries.visible(@user, @work_package.project)
end
private
@@ -45,46 +54,19 @@ module OpenProject::Costs
sum_costs
end
- def compute_summarized_cost_entries
- return {} if cost_entries.blank? || !user_allowed_to?(:view_cost_entries, :view_own_cost_entries)
-
- last_cost_type = ""
-
- cost_entries.sort_by(&:id).each_with_object({}) do |entry, hash|
- if entry.cost_type == last_cost_type
- hash[last_cost_type][:units] += entry.units
- else
- last_cost_type = entry.cost_type
-
- hash[last_cost_type] = {}
- hash[last_cost_type][:units] = entry.units
- hash[last_cost_type][:unit] = entry.cost_type.unit
- hash[last_cost_type][:unit_plural] = entry.cost_type.unit_plural
- end
- end
- end
-
- def time_entries
- @work_package.time_entries.visible(User.current, @work_package.project)
- end
-
def material_costs
- cost_entries_with_rate = cost_entries.select{|c| c.costs_visible_by?(User.current)}
+ cost_entries_with_rate = cost_entries.select{|c| c.costs_visible_by?(@user)}
cost_entries_with_rate.blank? ? nil : cost_entries_with_rate.collect(&:real_costs).sum
end
def labor_costs
- time_entries_with_rate = time_entries.select{|c| c.costs_visible_by?(User.current)}
+ time_entries_with_rate = time_entries.select{|c| c.costs_visible_by?(@user)}
time_entries_with_rate.blank? ? nil : time_entries_with_rate.collect(&:real_costs).sum
end
- def cost_entries
- @cost_entries ||= @work_package.cost_entries.visible(User.current, @work_package.project)
- end
-
def user_allowed_to?(*privileges)
privileges.inject(false) do |result, privilege|
- result || User.current.allowed_to?(privilege, @work_package.project)
+ result || @user.allowed_to?(privilege, @work_package.project)
end
end
end
diff --git a/lib/open_project/costs/engine.rb b/lib/open_project/costs/engine.rb
index 804df9e055..643ea9ee39 100644
--- a/lib/open_project/costs/engine.rb
+++ b/lib/open_project/costs/engine.rb
@@ -106,6 +106,22 @@ module OpenProject::Costs
allow_attribute_update :work_package, :cost_object_id
+ add_api_path :cost_entry do |id|
+ "#{root}/cost_entries/#{id}"
+ end
+
+ add_api_path :cost_entries_by_work_package do |id|
+ "#{work_package(id)}/cost_entries"
+ end
+
+ add_api_path :summarized_work_package_costs_by_type do |id|
+ "#{work_package(id)}/summarized_costs_by_type"
+ end
+
+ add_api_path :cost_type do |id|
+ "#{root}/cost_types/#{id}"
+ end
+
add_api_path :budget do |id|
"#{root}/budgets/#{id}"
end
@@ -116,12 +132,18 @@ module OpenProject::Costs
add_api_endpoint 'API::V3::Root' do
mount ::API::V3::Budgets::BudgetsAPI
+ mount ::API::V3::CostEntries::CostEntriesAPI
+ mount ::API::V3::CostTypes::CostTypesAPI
end
add_api_endpoint 'API::V3::Projects::ProjectsAPI', :id do
mount ::API::V3::Budgets::BudgetsByProjectAPI
end
+ add_api_endpoint 'API::V3::WorkPackages::WorkPackagesAPI', :id do
+ mount ::API::V3::CostEntries::CostEntriesByWorkPackageAPI
+ end
+
extend_api_response(:v3, :work_packages, :work_package) do
include Redmine::I18n
include ActionView::Helpers::NumberHelper
@@ -152,12 +174,16 @@ module OpenProject::Costs
exec_context: :decorator,
if: -> (*) { represented.costs_enabled? }
- property :summarized_cost_entries,
- embedded: true,
- exec_context: :decorator,
- if: -> (*) {
- represented.costs_enabled? && current_user_allowed_to_view_summarized_cost_entries
- }
+ linked_property :costs_by_type,
+ title_getter: -> (*) { nil },
+ getter: -> (*) { represented },
+ path: :summarized_work_package_costs_by_type,
+ embed_as: ::API::V3::CostEntries::WorkPackageCostsByTypeRepresenter,
+ show_if: -> (*) {
+ represented.costs_enabled? &&
+ (current_user_allowed_to(:view_cost_entries) ||
+ current_user_allowed_to(:view_own_cost_entries))
+ }
property :spent_time,
getter: -> (*) do
@@ -168,26 +194,10 @@ module OpenProject::Costs
exec_context: :decorator,
if: -> (_) { user_has_time_entry_permissions? }
- send(:define_method, :current_user_allowed_to_view_summarized_cost_entries) do
- current_user_allowed_to(:view_cost_entries) ||
- current_user_allowed_to(:view_own_cost_entries)
- end
-
send(:define_method, :overall_costs) do
number_to_currency(self.attributes_helper.overall_costs)
end
- send(:define_method, :summarized_cost_entries) do
- self.attributes_helper.summarized_cost_entries
- .map do |c|
- ::API::V3::CostTypes::CostTypeRepresenter
- .new(c[0],
- c[1],
- work_package: represented,
- current_user: @current_user)
- end
- end
-
send(:define_method, :attributes_helper) do
@attributes_helper ||= OpenProject::Costs::AttributesHelper.new(represented)
end
@@ -227,6 +237,17 @@ module OpenProject::Costs
writable: false,
show_if: -> (*) { represented.project.costs_enabled? }
+ schema :costs_by_type,
+ type: 'Collection',
+ name_source: :spent_units,
+ required: false,
+ writable: false,
+ show_if: -> (*) {
+ represented.project.costs_enabled? &&
+ (current_user_allowed_to(:view_cost_entries) ||
+ current_user_allowed_to(:view_own_cost_entries))
+ }
+
schema_with_allowed_collection :cost_object,
type: 'Budget',
required: false,
diff --git a/lib/open_project/costs/hooks/work_packages_show_attributes.rb b/lib/open_project/costs/hooks/work_packages_show_attributes.rb
index 37329a4d0a..10d7332ad5 100644
--- a/lib/open_project/costs/hooks/work_packages_show_attributes.rb
+++ b/lib/open_project/costs/hooks/work_packages_show_attributes.rb
@@ -65,18 +65,18 @@ module OpenProject::Costs::Hooks
def summarized_cost_entry_links(cost_entries, work_package, create_link=true)
str_array = []
- cost_entries.each do |k, v|
- txt = pluralize(v[:units], v[:unit], v[:unit_plural])
+ cost_entries.each do |cost_type, units|
+ txt = pluralize(units, cost_type.unit, cost_type.unit_plural)
if create_link
# TODO why does this have project_id, work_package_id and cost_type_id params?
str_array << link_to(txt, { :controller => '/costlog',
:action => 'index',
:project_id => work_package.project,
:work_package_id => work_package,
- :cost_type_id => k },
- { :title => k.name })
+ :cost_type_id => cost_type },
+ { :title => cost_type.name })
else
- str_array << "#{txt}"
+ str_array << "#{txt}"
end
end
str_array.join(", ").html_safe
diff --git a/spec/factories/cost_entry_factory.rb b/spec/factories/cost_entry_factory.rb
index 5df2a1e576..ae1aa3888b 100644
--- a/spec/factories/cost_entry_factory.rb
+++ b/spec/factories/cost_entry_factory.rb
@@ -26,5 +26,7 @@ FactoryGirl.define do
spent_on Date.today
units 1
comments ''
+ created_on { Time.now }
+ updated_on { Time.now }
end
end
diff --git a/spec/factories/cost_type_factory.rb b/spec/factories/cost_type_factory.rb
index 03cf0b041c..c5f2385d4d 100644
--- a/spec/factories/cost_type_factory.rb
+++ b/spec/factories/cost_type_factory.rb
@@ -22,5 +22,9 @@ FactoryGirl.define do
sequence(:name) { |n| "ct no. #{n}" }
unit "singular_unit"
unit_plural "plural_unit"
+
+ trait :deleted do
+ deleted_at Time.now
+ end
end
end
diff --git a/spec/lib/api/v3/cost_entries/aggregated_cost_entry_representer_spec.rb b/spec/lib/api/v3/cost_entries/aggregated_cost_entry_representer_spec.rb
new file mode 100644
index 0000000000..86e83632c3
--- /dev/null
+++ b/spec/lib/api/v3/cost_entries/aggregated_cost_entry_representer_spec.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::CostEntries::AggregatedCostEntryRepresenter do
+ include API::V3::Utilities::PathHelper
+
+ let(:cost_entry) { FactoryGirl.build(:cost_entry, id: 42) }
+ let(:representer) { described_class.new(cost_entry.cost_type, cost_entry.units) }
+
+ subject { representer.to_json }
+
+ it 'has a type' do
+ is_expected.to be_json_eql('AggregatedCostEntry'.to_json).at_path('_type')
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'costType' }
+ let(:href) { api_v3_paths.cost_type cost_entry.cost_type.id }
+ let(:title) { cost_entry.cost_type.name }
+ end
+
+ it 'has spent units' do
+ is_expected.to be_json_eql(cost_entry.units.to_json).at_path('spentUnits')
+ end
+end
diff --git a/spec/lib/api/v3/cost_entries/cost_entry_representer_spec.rb b/spec/lib/api/v3/cost_entries/cost_entry_representer_spec.rb
new file mode 100644
index 0000000000..c6ca2fd810
--- /dev/null
+++ b/spec/lib/api/v3/cost_entries/cost_entry_representer_spec.rb
@@ -0,0 +1,85 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::CostEntries::CostEntryRepresenter do
+ include API::V3::Utilities::PathHelper
+
+ let(:cost_entry) { FactoryGirl.build(:cost_entry, id: 42) }
+ let(:representer) { described_class.new(cost_entry) }
+
+ subject { representer.to_json }
+
+ it 'has a type' do
+ is_expected.to be_json_eql('CostEntry'.to_json).at_path('_type')
+ end
+
+ it_behaves_like 'has an untitled link' do
+ let(:link) { 'self' }
+ let(:href) { api_v3_paths.cost_entry cost_entry.id }
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'project' }
+ let(:href) { api_v3_paths.project cost_entry.project.id }
+ let(:title) { cost_entry.project.name }
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'user' }
+ let(:href) { api_v3_paths.user cost_entry.user.id }
+ let(:title) { cost_entry.user.name }
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'costType' }
+ let(:href) { api_v3_paths.cost_type cost_entry.cost_type.id }
+ let(:title) { cost_entry.cost_type.name }
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'workPackage' }
+ let(:href) { api_v3_paths.work_package cost_entry.work_package.id }
+ let(:title) { cost_entry.work_package.subject }
+ end
+
+ it 'has an id' do
+ is_expected.to be_json_eql(cost_entry.id.to_json).at_path('id')
+ end
+
+ it 'has spent units' do
+ is_expected.to be_json_eql(cost_entry.units.to_json).at_path('spentUnits')
+ end
+
+ it_behaves_like 'has ISO 8601 date only' do
+ let(:date) { cost_entry.spent_on }
+ let(:json_path) { 'spentOn' }
+ end
+
+ it_behaves_like 'has UTC ISO 8601 date and time' do
+ let(:date) { cost_entry.created_on }
+ let(:json_path) { 'createdAt' }
+ end
+
+ it_behaves_like 'has UTC ISO 8601 date and time' do
+ let(:date) { cost_entry.updated_on }
+ let(:json_path) { 'updatedAt' }
+ end
+end
diff --git a/spec/lib/api/v3/cost_entries/work_package_costs_by_type_representer_spec.rb b/spec/lib/api/v3/cost_entries/work_package_costs_by_type_representer_spec.rb
new file mode 100644
index 0000000000..c441fa4b7f
--- /dev/null
+++ b/spec/lib/api/v3/cost_entries/work_package_costs_by_type_representer_spec.rb
@@ -0,0 +1,85 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::CostEntries::WorkPackageCostsByTypeRepresenter do
+ include API::V3::Utilities::PathHelper
+
+ let(:project) { FactoryGirl.create(:project) }
+ let(:work_package) { FactoryGirl.create(:work_package, project: project) }
+ let(:cost_type_A) { FactoryGirl.create(:cost_type) }
+ let(:cost_type_B) { FactoryGirl.create(:cost_type) }
+ let(:cost_entries_A) {
+ FactoryGirl.create_list(:cost_entry,
+ 2,
+ units: 1,
+ work_package: work_package,
+ project: project,
+ cost_type: cost_type_A)
+ }
+ let(:cost_entries_B) {
+ FactoryGirl.create_list(:cost_entry,
+ 3,
+ units: 2,
+ work_package: work_package,
+ project: project,
+ cost_type: cost_type_B)
+ }
+ let(:current_user) {
+ FactoryGirl.build(:user, member_in_project: project, member_through_role: role)
+ }
+ let(:role) { FactoryGirl.build(:role, permissions: [:view_cost_entries]) }
+
+ let(:representer) { described_class.new(work_package, current_user: current_user) }
+
+ subject { representer.to_json }
+
+ before do
+ # create the lists
+ cost_entries_A
+ cost_entries_B
+ end
+
+ it 'has a type' do
+ is_expected.to be_json_eql('Collection'.to_json).at_path('_type')
+ end
+
+ it 'has one element per type' do
+ is_expected.to have_json_size(2).at_path('_embedded/elements')
+ end
+
+ it 'indicates the cost types' do
+ elements = JSON.parse(subject)['_embedded']['elements']
+ types = elements.map { |entry| entry['_links']['costType']['href'] }
+ expect(types).to include(api_v3_paths.cost_type cost_type_A.id)
+ expect(types).to include(api_v3_paths.cost_type cost_type_B.id)
+ end
+
+ it 'aggregates the units' do
+ elements = JSON.parse(subject)['_embedded']['elements']
+ units_by_type = elements.inject({}) do |hash, entry|
+ hash[entry['_links']['costType']['href']] = entry['spentUnits']
+ hash
+ end
+
+ expect(units_by_type[api_v3_paths.cost_type cost_type_A.id]).to eql 2.0
+ expect(units_by_type[api_v3_paths.cost_type cost_type_B.id]).to eql 6.0
+ end
+end
diff --git a/spec/lib/api/v3/cost_types/cost_type_representer_spec.rb b/spec/lib/api/v3/cost_types/cost_type_representer_spec.rb
index 3c4bdd7e33..f28899481f 100644
--- a/spec/lib/api/v3/cost_types/cost_type_representer_spec.rb
+++ b/spec/lib/api/v3/cost_types/cost_type_representer_spec.rb
@@ -20,106 +20,40 @@
require 'spec_helper'
describe ::API::V3::CostTypes::CostTypeRepresenter do
- let(:project1) { FactoryGirl.create(:project) }
- let(:project2) { FactoryGirl.create(:project) }
- let(:role) {
- FactoryGirl.create(:role, permissions: [:view_work_package, :view_own_cost_entries])
- }
- let(:user1) {
- FactoryGirl.create(:user,
- member_in_projects: [project1, project2],
- member_through_role: role,
- created_on: 1.day.ago,
- updated_on: Date.today)
- }
- let(:user2) {
- FactoryGirl.create(:user,
- member_in_projects: [project1, project2],
- member_through_role: role,
- created_on: 1.day.ago,
- updated_on: Date.today)
- }
- let(:work_package1) { FactoryGirl.create(:work_package, project: project1) }
- let(:work_package2) { FactoryGirl.create(:work_package, project: project2) }
- let(:cost_type1) { FactoryGirl.create(:cost_type) }
- let(:cost_type2) { FactoryGirl.create(:cost_type) }
- let!(:cost_entry11) {
- FactoryGirl.create(:cost_entry,
- cost_type: cost_type1,
- work_package: work_package1,
- project: project1,
- units: 3,
- spent_on: Date.today,
- user_id: user1.id,
- comments: 'Entry 1')
- }
- let!(:cost_entry12) {
- FactoryGirl.create(:cost_entry,
- cost_type: cost_type2,
- work_package: work_package1,
- project: project1,
- units: 3,
- spent_on: Date.today,
- user_id: user1.id,
- comments: 'Entry 2')
- }
- let!(:cost_entry13) {
- FactoryGirl.create(:cost_entry,
- cost_type: cost_type1,
- work_package: work_package1,
- project: project1,
- units: 3,
- spent_on: Date.today,
- user_id: user2.id,
- comments: 'Entry 3')
- }
- let!(:cost_entry21) {
- FactoryGirl.create(:cost_entry,
- cost_type: cost_type1,
- work_package: work_package2,
- project: project2,
- units: 3,
- spent_on: Date.today,
- user: user1,
- comments: 'Entry 1')
- }
- let!(:cost_entry22) {
- FactoryGirl.create(:cost_entry,
- cost_type: cost_type2,
- work_package: work_package2,
- project: project2,
- units: 3,
- spent_on: Date.today,
- user: user1,
- comments: 'Entry 2')
- }
+ include API::V3::Utilities::PathHelper
- let(:representer) do
- described_class.new(cost_type1,
- { unit: 'tonne', unit_plural: 'tonnes' },
- work_package: work_package1,
- current_user: user1)
+ let(:cost_type) { FactoryGirl.build_stubbed(:cost_type) }
+ let(:representer) { described_class.new(cost_type) }
+
+ subject { representer.to_json }
+
+ it 'has a type' do
+ is_expected.to be_json_eql('CostType'.to_json).at_path('_type')
end
- context 'generation' do
- subject(:generated) { representer.to_json }
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'self' }
+ let(:href) { api_v3_paths.cost_type cost_type.id }
+ let(:title) { cost_type.name }
+ end
- it { is_expected.to include_json('CostType'.to_json).at_path('_type') }
+ it 'has an id' do
+ is_expected.to be_json_eql(cost_type.id.to_json).at_path('id')
+ end
- describe 'cost_type' do
- it { is_expected.to have_json_path('id') }
+ it 'has a name' do
+ is_expected.to be_json_eql(cost_type.name.to_json).at_path('name')
+ end
- it { is_expected.to have_json_path('name') }
+ it 'has a unit' do
+ is_expected.to be_json_eql(cost_type.unit.to_json).at_path('unit')
+ end
- it { is_expected.to have_json_path('units') }
- it { is_expected.to have_json_path('unit') }
- it { is_expected.to have_json_path('unitPlural') }
- end
+ it 'has a pluralized unit' do
+ is_expected.to be_json_eql(cost_type.unit_plural.to_json).at_path('unitPlural')
+ end
- describe 'units' do
- it 'shows only cost entries of type cost_type1 for user1 in project project1' do
- is_expected.to be_json_eql(cost_entry11.units.to_json).at_path('units')
- end
- end
+ it 'indicates if it is the default' do
+ is_expected.to be_json_eql(cost_type.default.to_json).at_path('isDefault')
end
end
diff --git a/spec/lib/api/v3/path_helper_spec.rb b/spec/lib/api/v3/path_helper_spec.rb
new file mode 100644
index 0000000000..6722027d33
--- /dev/null
+++ b/spec/lib/api/v3/path_helper_spec.rb
@@ -0,0 +1,70 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::Utilities::PathHelper do
+ let(:helper) { Class.new.tap { |c| c.extend(described_class) }.api_v3_paths }
+
+ describe '#cost_entry' do
+ subject { helper.cost_entry 42 }
+
+ it { is_expected.to eql('/api/v3/cost_entries/42') }
+ end
+
+ describe '#cost_entries_by_work_package' do
+ subject { helper.cost_entries_by_work_package 42 }
+
+ it { is_expected.to eql('/api/v3/work_packages/42/cost_entries') }
+ end
+
+ describe '#summarized_work_package_costs_by_type' do
+ subject { helper.summarized_work_package_costs_by_type 42 }
+
+ it { is_expected.to eql('/api/v3/work_packages/42/summarized_costs_by_type') }
+ end
+
+
+ describe '#cost_type' do
+ subject { helper.cost_type 42 }
+
+ it { is_expected.to eql('/api/v3/cost_types/42') }
+ end
+
+ describe '#budget' do
+ subject { helper.budget 42 }
+
+ it { is_expected.to eql('/api/v3/budgets/42') }
+ end
+
+ describe '#budgets_by_project' do
+ subject { helper.budgets_by_project 42 }
+
+ it { is_expected.to eql('/api/v3/projects/42/budgets') }
+ end
+end
diff --git a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb
index 539bff4995..145c8db2a7 100644
--- a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb
+++ b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb
@@ -20,6 +20,8 @@
require 'spec_helper'
describe ::API::V3::WorkPackages::WorkPackageRepresenter do
+ include API::V3::Utilities::PathHelper
+
let(:project) { FactoryGirl.create(:project) }
let(:role) { FactoryGirl.create(:role, permissions: [:view_time_entries,
:view_cost_entries,
@@ -88,8 +90,13 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
end
end
- describe 'embedded' do
- it { is_expected.to have_json_path('_embedded/summarizedCostEntries') }
+ it_behaves_like 'has an untitled link' do
+ let(:link) { 'costsByType' }
+ let(:href) { api_v3_paths.summarized_work_package_costs_by_type work_package.id }
+ end
+
+ it 'embeds the costsByType' do
+ is_expected.to have_json_path('_embedded/costsByType')
end
describe 'spentTime' do
diff --git a/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
index 60880eaa94..4152f09296 100644
--- a/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
+++ b/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
@@ -96,6 +96,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
let(:can_view_own_time_entries) { false }
before do
+ allow(current_user).to receive(:allowed_to?).and_return(false)
allow(current_user).to receive(:allowed_to?).with(:view_time_entries, work_package.project)
.and_return can_view_time_entries
allow(current_user).to receive(:allowed_to?).with(:view_own_time_entries, work_package.project)
@@ -157,12 +158,66 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
allow(schema.project).to receive(:costs_enabled?).and_return(false)
end
- it 'has no schema for budget' do
+ it 'has no schema for overallCosts' do
is_expected.not_to have_json_path('overallCosts')
end
end
end
+ describe 'costsByType' do
+ shared_examples_for 'costsByType visible' do
+ it_behaves_like 'has basic schema properties' do
+ let(:path) { 'costsByType' }
+ let(:type) { 'Collection' }
+ let(:name) { I18n.t('activerecord.attributes.work_package.spent_units') }
+ let(:required) { false }
+ let(:writable) { false }
+ end
+ end
+
+ shared_examples_for 'costsByType not visible' do
+ it { is_expected.not_to have_json_path('costsByType') }
+ end
+
+ let(:can_view_cost_entries) { false }
+ let(:can_view_own_cost_entries) { false }
+
+ before do
+ allow(current_user).to receive(:allowed_to?).and_return(false)
+ allow(current_user).to receive(:allowed_to?).with(:view_cost_entries, work_package.project)
+ .and_return can_view_cost_entries
+ allow(current_user).to receive(:allowed_to?).with(:view_own_cost_entries, work_package.project)
+ .and_return can_view_own_cost_entries
+ end
+
+ context 'costs disabled, but all permissions' do
+ let(:can_view_cost_entries) { true }
+ let(:can_view_own_cost_entries) { true }
+
+ before do
+ allow(schema.project).to receive(:costs_enabled?).and_return(false)
+ end
+
+ it_behaves_like 'costsByType not visible'
+ end
+
+ context 'costs enabled' do
+ context 'no permissions' do
+ it_behaves_like 'costsByType not visible'
+ end
+
+ context 'can only view own cost entries' do
+ let(:can_view_own_cost_entries) { true }
+ it_behaves_like 'costsByType visible'
+ end
+
+ context 'can view all cost entries' do
+ let(:can_view_cost_entries) { true }
+ it_behaves_like 'costsByType visible'
+ end
+ end
+ end
+
describe 'budget' do
it_behaves_like 'has basic schema properties' do
let(:path) { 'costObject' }
diff --git a/spec/requests/api/budget_resource_spec.rb b/spec/requests/api/budgets/budget_resource_spec.rb
similarity index 100%
rename from spec/requests/api/budget_resource_spec.rb
rename to spec/requests/api/budgets/budget_resource_spec.rb
diff --git a/spec/requests/api/cost_entries/cost_entries_by_work_package_resource_spec.rb b/spec/requests/api/cost_entries/cost_entries_by_work_package_resource_spec.rb
new file mode 100644
index 0000000000..0da265b6b4
--- /dev/null
+++ b/spec/requests/api/cost_entries/cost_entries_by_work_package_resource_spec.rb
@@ -0,0 +1,110 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Cost Entry resource' do
+ include Rack::Test::Methods
+ include API::V3::Utilities::PathHelper
+
+ let(:current_user) {
+ FactoryGirl.create(:user, member_in_project: project, member_through_role: role)
+ }
+ let(:role) { FactoryGirl.create(:role, permissions: permissions) }
+ let(:permissions) { [:view_cost_entries] }
+ let(:project) { FactoryGirl.create(:project) }
+ let(:work_package) { FactoryGirl.create(:work_package, project: project) }
+ subject(:response) { last_response }
+
+ let(:cost_entry) {
+ FactoryGirl.build(:cost_entry,
+ project: project,
+ work_package: work_package,
+ user: current_user)
+ }
+
+ before do
+ allow(User).to receive(:current).and_return current_user
+ cost_entry.save!
+
+ get get_path
+ end
+
+ describe 'work_packages/:id/cost_entries' do
+ let(:get_path) { api_v3_paths.cost_entries_by_work_package work_package.id }
+
+ context 'user can see any cost entries' do
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'user can see own cost entries' do
+ let(:permissions) { [:view_own_cost_entries] }
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'user has no cost entry permissions' do
+ let(:permissions) { [] }
+
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+ end
+
+ describe 'work_packages/:id/summarized_costs_by_type' do
+ let(:get_path) { api_v3_paths.summarized_work_package_costs_by_type work_package.id }
+
+ context 'user can see any cost entries' do
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'user can see own cost entries' do
+ let(:permissions) { [:view_own_cost_entries] }
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'user has no cost entry permissions' do
+ let(:permissions) { [] }
+
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+ end
+end
diff --git a/spec/requests/api/cost_entries/cost_entry_resource_spec.rb b/spec/requests/api/cost_entries/cost_entry_resource_spec.rb
new file mode 100644
index 0000000000..19aa6ce9a8
--- /dev/null
+++ b/spec/requests/api/cost_entries/cost_entry_resource_spec.rb
@@ -0,0 +1,103 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Cost Entry resource' do
+ include Rack::Test::Methods
+ include API::V3::Utilities::PathHelper
+
+ let(:current_user) {
+ FactoryGirl.create(:user, member_in_project: project, member_through_role: role)
+ }
+ let(:role) { FactoryGirl.create(:role, permissions: permissions) }
+ let(:permissions) { [:view_cost_entries] }
+ let(:project) { FactoryGirl.create(:project) }
+ subject(:response) { last_response }
+
+ let(:cost_entry) { FactoryGirl.build(:cost_entry, project: project) }
+
+ before do
+ allow(User).to receive(:current).and_return current_user
+ cost_entry.save!
+
+ get get_path
+ end
+
+ describe 'cost_entries/:id' do
+ let(:get_path) { api_v3_paths.cost_entry cost_entry.id }
+
+ context 'user can see cost entries' do
+ context 'valid id' do
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'invalid id' do
+ let(:get_path) { api_v3_paths.cost_type 'bogus' }
+
+ it_behaves_like 'not found' do
+ let(:id) { 'bogus' }
+ end
+ end
+ end
+
+ context 'user can only see own cost entries' do
+ let(:permissions) { [:view_own_cost_entries] }
+
+ context 'cost entry is not his own' do
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+
+ context 'cost entry is his own' do
+ let(:cost_entry) { FactoryGirl.build(:cost_entry, project: project, user: current_user) }
+
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+ end
+
+ context 'user has no cost entry permissions' do
+ let(:permissions) { [] }
+
+ describe 'he can\'t even see own cost entries' do
+ let(:cost_entry) { FactoryGirl.build(:cost_entry, project: project, user: current_user) }
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/cost_types/cost_type_resource_spec.rb b/spec/requests/api/cost_types/cost_type_resource_spec.rb
new file mode 100644
index 0000000000..740a32372c
--- /dev/null
+++ b/spec/requests/api/cost_types/cost_type_resource_spec.rb
@@ -0,0 +1,87 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Cost Type resource' do
+ include Rack::Test::Methods
+ include API::V3::Utilities::PathHelper
+
+ let(:current_user) {
+ FactoryGirl.create(:user, member_in_project: project, member_through_role: role)
+ }
+ let(:role) { FactoryGirl.create(:role, permissions: [:view_cost_entries]) }
+ let(:project) { FactoryGirl.create(:project) }
+ subject(:response) { last_response }
+
+ let!(:cost_type) { FactoryGirl.create(:cost_type) }
+
+ before do
+ allow(User).to receive(:current).and_return current_user
+
+ get get_path
+ end
+
+ describe 'cost_types/:id' do
+ let(:get_path) { api_v3_paths.cost_type cost_type.id }
+
+ context 'user can see cost entries' do
+ context 'valid id' do
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'cost type deleted' do
+ let!(:cost_type) { FactoryGirl.create(:cost_type, :deleted) }
+
+ it_behaves_like 'not found' do
+ let(:id) { cost_type.id }
+ end
+ end
+
+ context 'invalid id' do
+ let(:get_path) { api_v3_paths.cost_type 'bogus' }
+
+ it_behaves_like 'not found' do
+ let(:id) { 'bogus' }
+ end
+ end
+ end
+
+ context 'user can\'t see cost entries' do
+ let(:current_user) { FactoryGirl.create(:user) }
+
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+ end
+end
diff --git a/spec/requests/api/work_package_form_resource_spec.rb b/spec/requests/api/work_packages/work_package_form_resource_spec.rb
similarity index 100%
rename from spec/requests/api/work_package_form_resource_spec.rb
rename to spec/requests/api/work_packages/work_package_form_resource_spec.rb
diff --git a/spec/requests/api/work_package_resource_spec.rb b/spec/requests/api/work_packages/work_package_resource_spec.rb
similarity index 100%
rename from spec/requests/api/work_package_resource_spec.rb
rename to spec/requests/api/work_packages/work_package_resource_spec.rb