Merge pull request #130 from 0xF013/feature/add-hook-for-edit-type

changes hooks for new layout of details pane
pull/6827/head
Jan Sandbrink 10 years ago
commit fce9f390a1
  1. 1
      app/assets/templates/work_packages/cost_object.html
  2. 11
      app/assets/templates/work_packages/summarized_cost_entries.html
  3. 6
      app/models/cost_type.rb
  4. 327
      doc/apiv3.apib
  5. 26
      frontend/app/openproject-costs-app.js
  6. 23
      frontend/app/work_packages/directives/cost-object-directive.js
  7. 16
      frontend/app/work_packages/directives/summarized-cost-entries-directive.js
  8. 47
      lib/api/v3/cost_entries/aggregated_cost_entry_representer.rb
  9. 58
      lib/api/v3/cost_entries/cost_entries_api.rb
  10. 60
      lib/api/v3/cost_entries/cost_entries_by_work_package_api.rb
  11. 38
      lib/api/v3/cost_entries/cost_entry_collection_representer.rb
  12. 52
      lib/api/v3/cost_entries/cost_entry_representer.rb
  13. 71
      lib/api/v3/cost_entries/work_package_costs_by_type_representer.rb
  14. 38
      lib/api/v3/cost_types/cost_type_representer.rb
  15. 56
      lib/api/v3/cost_types/cost_types_api.rb
  16. 46
      lib/open_project/costs/attributes_helper.rb
  17. 65
      lib/open_project/costs/engine.rb
  18. 10
      lib/open_project/costs/hooks/work_packages_show_attributes.rb
  19. 2
      spec/factories/cost_entry_factory.rb
  20. 4
      spec/factories/cost_type_factory.rb
  21. 43
      spec/lib/api/v3/cost_entries/aggregated_cost_entry_representer_spec.rb
  22. 85
      spec/lib/api/v3/cost_entries/cost_entry_representer_spec.rb
  23. 85
      spec/lib/api/v3/cost_entries/work_package_costs_by_type_representer_spec.rb
  24. 120
      spec/lib/api/v3/cost_types/cost_type_representer_spec.rb
  25. 70
      spec/lib/api/v3/path_helper_spec.rb
  26. 11
      spec/lib/api/v3/work_packages/work_package_representer_spec.rb
  27. 57
      spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
  28. 0
      spec/requests/api/budgets/budget_resource_spec.rb
  29. 110
      spec/requests/api/cost_entries/cost_entries_by_work_package_resource_spec.rb
  30. 103
      spec/requests/api/cost_entries/cost_entry_resource_spec.rb
  31. 87
      spec/requests/api/cost_types/cost_type_resource_spec.rb
  32. 0
      spec/requests/api/work_packages/work_package_form_resource_spec.rb
  33. 0
      spec/requests/api/work_packages/work_package_resource_spec.rb

@ -3,3 +3,4 @@
{{ costObject.props.subject }}
</a>
</span>
<span ng-if="!costObject">-</span>

@ -1,12 +1,13 @@
<span class="costs costTypes" ng-if="costTypes">
<span ng-repeat="costType in costTypes">
<a ng-href="{{ linkToSummary(costType) }}" title="{{ costType.props.name }}">
{{ costType.props.units }}
<span class="unit" ng-switch="costType.props.units">
<span ng-switch-when="1">{{ costType.props.unit }}</span>
<span ng-switch-default>{{ costType.props.unitPlural }}</span>
<a ng-href="{{ linkToSummary(costType.embedded.costType) }}" title="{{ costType.embedded.costType.props.name }}">
{{ costType.props.spentUnits }}
<span class="unit" ng-switch="costType.props.spentUnits">
<span ng-switch-when="1">{{ costType.embedded.costType.props.unit }}</span>
<span ng-switch-default>{{ costType.embedded.costType.props.unitPlural }}</span>
</span>
</a>
<span class="separator" ng-if="!$last">,</span>
</span>
</span>
<span ng-if="!costTypes.length">-</span>

@ -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

@ -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.

@ -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;
}

@ -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();
});
});
});
}
};
}]);

@ -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;
};

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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'

@ -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

@ -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

@ -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,

@ -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 << "<span title=\"#{h(k.name)}\">#{txt}</span>"
str_array << "<span title=\"#{h(cost_type.name)}\">#{txt}</span>"
end
end
str_array.join(", ").html_safe

@ -26,5 +26,7 @@ FactoryGirl.define do
spent_on Date.today
units 1
comments ''
created_on { Time.now }
updated_on { Time.now }
end
end

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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' }

@ -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

@ -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

@ -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
Loading…
Cancel
Save