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