Merge branch 'dev' into feature/18444-gulp-livingstyleguide

pull/2695/head
Alex Coles 10 years ago
commit a44b768619
  1. 12
      app/assets/javascripts/work_packages.js.erb
  2. 51
      app/assets/stylesheets/content/_forms.md
  3. 13
      app/assets/stylesheets/content/_forms.sass
  4. 2
      app/assets/stylesheets/content/_tables.sass
  5. 6
      app/controllers/versions_controller.rb
  6. 59
      app/helpers/settings_helper.rb
  7. 4
      app/models/work_package.rb
  8. 2
      app/views/messages/_form.html.erb
  9. 2
      app/views/settings/_work_packages.html.erb
  10. 30
      app/views/versions/create.js.erb
  11. 2
      frontend/app/templates/work_packages.list.details.html
  12. 6
      frontend/app/work_packages/controllers/work-package-details-controller.js
  13. 15
      frontend/tests/unit/tests/work_packages/controllers/work-package-details-controller-test.js
  14. 67
      lib/api/contracts/model_contract.rb
  15. 27
      lib/api/decorators/schema.rb
  16. 26
      lib/api/decorators/single.rb
  17. 9
      lib/api/root.rb
  18. 3
      lib/api/v3/projects/projects_api.rb
  19. 1
      lib/api/v3/root.rb
  20. 38
      lib/api/v3/types/type_collection_representer.rb
  21. 57
      lib/api/v3/types/type_representer.rb
  22. 60
      lib/api/v3/types/types_api.rb
  23. 50
      lib/api/v3/types/types_by_project_api.rb
  24. 12
      lib/api/v3/utilities/path_helper.rb
  25. 2
      lib/api/v3/versions/projects_by_version_api.rb
  26. 4
      lib/api/v3/versions/versions_api.rb
  27. 6
      lib/api/v3/versions/versions_by_project_api.rb
  28. 25
      lib/api/v3/work_packages/form/form_api.rb
  29. 5
      lib/api/v3/work_packages/form/work_package_attribute_links_representer.rb
  30. 16
      lib/api/v3/work_packages/schema/work_package_schema.rb
  31. 17
      lib/api/v3/work_packages/schema/work_package_schema_representer.rb
  32. 129
      lib/api/v3/work_packages/work_package_contract.rb
  33. 54
      lib/api/v3/work_packages/work_package_representer.rb
  34. 44
      lib/api/v3/work_packages/work_packages_api.rb
  35. 20
      lib/open_project/plugins/acts_as_op_engine.rb
  36. 19
      lib/plugins/acts_as_customizable/lib/acts_as_customizable.rb
  37. 11
      spec/controllers/versions_controller_spec.rb
  38. 20
      spec/factories/type_factory.rb
  39. 47
      spec/helpers/settings_helper_spec.rb
  40. 106
      spec/lib/api/v3/types/type_representer_spec.rb
  41. 26
      spec/lib/api/v3/utilities/path_helper_spec.rb
  42. 56
      spec/lib/api/v3/work_packages/schema/work_package_schema_spec.rb
  43. 9
      spec/lib/api/v3/work_packages/work_package_representer_spec.rb
  44. 163
      spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
  45. 3
      spec/models/work_package_spec.rb
  46. 10
      spec/requests/api/v3/project_resource_spec.rb
  47. 113
      spec/requests/api/v3/types/type_resource_spec.rb
  48. 95
      spec/requests/api/v3/types/types_by_project_resource_spec.rb
  49. 66
      spec/requests/api/v3/work_package_resource_spec.rb
  50. 42
      spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb

@ -35,16 +35,25 @@ var WorkPackage = WorkPackage || {};
var init;
// TODO: remove this, once the issue edit view is completely angular based
var bodyInjector = function() { return angular.element('body').injector(); };
var manuallyCompile = function(button) {
if (!window.angular || button.length < 1) {
return;
}
angular.element('body').injector().invoke(['$compile', function($compile) {
bodyInjector().invoke(['$compile', function($compile) {
var scope = angular.element(button).scope();
$compile(button)(scope);
}])
};
var initializeAtWho = function(section) {
var textareas = section.find('textarea');
bodyInjector().invoke(['AutoCompleteHelper', function(AutoCompleteHelper) {
AutoCompleteHelper.enableTextareaAutoCompletion(textareas);
}]);
}
init = function () {
$.ajaxAppend({
@ -68,6 +77,7 @@ var WorkPackage = WorkPackage || {};
// TODO: see last todo
var previewButton = update.find(".preview.button")
manuallyCompile(previewButton);
initializeAtWho(update);
}
});

@ -586,7 +586,7 @@
</span>
</div>
</fieldset>
```
# Forms: Text fields
@ -880,3 +880,52 @@ _with no classes applied (uses default Foundation form styling)_
<option>three</option>
</select>
```
# Forms: Checkbox Matrices
```
<table class="form--matrix">
<thead>
<tr class="form--matrix-header-row">
<th class="form--matrix-header-cell">Attribute name</th>
<th class="form--matrix-header-cell">User access</th>
<th class="form--matrix-header-cell">Admin access</th>
</tr>
</thead>
<tbody>
<tr class="form--matrix-row">
<td class="form--matrix-cell">
Project
</td>
<td class="form--matrix-checkbox-cell">
<span class="form--check-box-container"><input class="form--check-box" id="attributes_project" name="settings[attributes][]" type="checkbox" value="project"></span>
</td>
<td class="form--matrix-checkbox-cell">
<span class="form--check-box-container"><input class="form--check-box" id="admin_attributes_project" name="settings[admin_attributes][]" type="checkbox" value="project"></span>
</td>
</tr>
<tr class="form--matrix-row">
<td class="form--matrix-cell">
Type
</td>
<td class="form--matrix-checkbox-cell">
<span class="form--check-box-container"><input checked="checked" class="form--check-box" id="attributes_type" name="settings[attributes][]" type="checkbox" value="type"></span>
</td>
<td class="form--matrix-checkbox-cell">
<span class="form--check-box-container"><input class="form--check-box" id="admin_attributes_type" name="settings[admin_attributes][]" type="checkbox" value="type"></span>
</td>
</tr>
<tr class="form--matrix-row">
<td class="form--matrix-cell">
Parent
</td>
<td class="form--matrix-checkbox-cell">
<span class="form--check-box-container"><input class="form--check-box" id="attributes_parent" name="settings[attributes][]" type="checkbox" value="parent"></span>
</td>
<td class="form--matrix-checkbox-cell">
<span class="form--check-box-container"><input class="form--check-box" id="admin_attributes_parent" name="settings[admin_attributes][]" type="checkbox" value="parent"></span>
</td>
</tr>
</tbody>
</table>
```

@ -424,7 +424,7 @@ fieldset.form--fieldset
color: $form-label-color
margin-bottom: 0.8rem
.form--table
.form--matrix
border: $content-form-input-border
border-radius: 2px
background: none
@ -432,23 +432,24 @@ fieldset.form--fieldset
font-size: 0.9rem
line-height: 1.5
.form--table-header-row
.form--matrix-header-row
font-weight: bold
border-bottom: $content-form-input-border
background-color: $gray-light
.form--table-header-row-cell
.form--matrix-header-cell
padding: 0 1rem
.form--table-row
.form--matrix-row
&:nth-child(odd)
background-color: $gray-light
&:nth-child(even)
background-color: white
.form--table-checkbox-cell
width: 1rem
.form--matrix-checkbox-cell
min-width: 1rem
max-width: 4rem
text-align: center
.form--column

@ -350,8 +350,6 @@ table#time-report
#content table
&.issues
width: 100%
th
font-weight: normal
&.issues td
border: 0 none
padding: 3px 6px

@ -93,11 +93,7 @@ class VersionsController < ApplicationController
redirect_to controller: '/projects', action: 'settings', tab: 'versions', id: @project
end
format.js do
# IE doesn't support the replace_html rjs method for select box options
render(:update) {|page|
page.replace 'work_package_fixed_version_id',
content_tag('select', '<option></option>'.html_safe + version_options_for_select(@project.shared_versions.open, @version).html_safe, id: 'work_package_fixed_version_id', name: 'work_package[fixed_version_id]')
}
render locals: { versions: @project.shared_versions.open, version: @version }
end
end
else

@ -74,31 +74,11 @@ module SettingsHelper
end
end
def settings_multiselect(settings, choices, options = {})
('<table>' +
'<thead>' +
'<tr>' +
'<th>' + I18n.t(options[:label_choices] || :label_choices) + '</th>' +
settings.map do |setting|
'<th>' + hidden_field_tag("settings[#{setting}][]", '') + I18n.t('setting_' + setting.to_s) + '</th>'
end.join +
'</tr>' +
'</thead>' +
'<tbody>' +
choices.map do |choice|
text, value = (choice.is_a?(Array)) ? choice : [choice, choice]
'<tr>' +
'<td>' + h(text) + '</td>' +
settings.map do |setting|
'<td align="center">' +
styled_check_box_tag("settings[#{setting}][]", value,
Setting.send(setting).include?(value),
id: "#{setting}_#{value}", class: 'form--check-box') + '</td>'
end.join +
'</tr>'
end.join +
'</tbody>' +
'</table>').html_safe
def settings_matrix(settings, choices, options = {})
content_tag(:table, class: 'form--matrix') do
content_tag(:thead, build_settings_matrix_head(settings, options)) +
content_tag(:tbody, build_settings_matrix_body(settings, choices))
end
end
def setting_text_field(setting, options = {})
@ -152,4 +132,33 @@ module SettingsHelper
block.call
end
end
def build_settings_matrix_head(settings, options = {})
content_tag(:tr, class: 'form--matrix-header-row') do
content_tag(:th, I18n.t(options[:label_choices] || :label_choices),
class: 'form--matrix-header-cell') +
settings.map do |setting|
content_tag(:th, class: 'form--matrix-header-cell') do
hidden_field_tag("settings[#{setting}][]", '') +
I18n.t("setting_#{setting}")
end
end.join.html_safe
end
end
def build_settings_matrix_body(settings, choices)
choices.map do |choice|
text, value = (choice.is_a?(Array)) ? choice : [choice, choice]
content_tag(:tr, class: 'form--matrix-row') do
content_tag(:td, text, class: 'form--matrix-cell') +
settings.map do |setting|
content_tag(:td, class: 'form--matrix-checkbox-cell') do
styled_check_box_tag("settings[#{setting}][]", value,
Setting.send(setting).include?(value),
id: "#{setting}_#{value}")
end
end.join.html_safe
end
end.join.html_safe
end
end

@ -408,6 +408,10 @@ class WorkPackage < ActiveRecord::Base
project.categories
end
def assignable_types
project.types
end
# Versions that the work_package can be assigned to
# A work_package can be assigned to:
# * any open, shared version of the project the wp belongs to

@ -43,7 +43,7 @@ See doc/COPYRIGHT.rdoc for more details.
</div>
<% end %>
<div class="form--field">
<%= f.text_area :content, label: l(:description_message_content), data: { wp_autocomplete_url: work_packages_auto_complete_path(project_id: @project, format: :json), :'ng-non-bindable' => '' } %>
<%= f.text_area :content, label: l(:description_message_content), class: 'wiki-edit', data: {:'ng-non-bindable' => '' }, 'data-wp_autocomplete_url' => work_packages_auto_complete_path(project_id: @project, format: :json) %>
<%= wikitoolbar_for 'message_content' %>
</div>
<%= render :partial => 'attachments/form' %>

@ -36,7 +36,7 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="form--field"><%= setting_text_field :work_packages_export_limit, :size => 6 %></div>
</section>
<fieldset class="form--fieldset"><legend class="form--fieldset-legend"><%= l(:setting_column_options) %></legend>
<%= settings_multiselect([:work_package_list_default_columns, :work_package_list_summable_columns],
<%= settings_matrix([:work_package_list_default_columns, :work_package_list_summable_columns],
Query.new.available_columns.collect {|c| [c.caption, c.name.to_s]}, :label_choices => :setting_work_package_properties) %>
</fieldset>
<%= styled_submit_tag l(:button_save), class: '-highlight' %>

@ -0,0 +1,30 @@
<%#-- 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.
++#%>
jQuery('#work_package_fixed_version_id').empty().append("<%= escape_javascript(version_options_for_select(versions, version)) %>");

@ -30,7 +30,7 @@
<span class="hidden-for-sighted" tabindex="-1" focus ng-bind="focusAnchorLabel">
</span>
<div class="select-type">{{ workPackage.props.type }}:&nbsp;</div>
<div class="select-type">{{ type.props.name }}:&nbsp;</div>
<div
class="wp-subject"

@ -115,6 +115,9 @@ module.exports = function($scope,
$scope.showStaticPagePath = PathHelper.staticWorkPackagePath($scope.workPackage.props.id);
// Type
$scope.type = workPackage.embedded.type;
// Author
$scope.author = workPackage.embedded.author;
$scope.authorPath = PathHelper.staticUserPath($scope.author.props.id);
@ -151,9 +154,6 @@ module.exports = function($scope,
relationTypeIterator(key);
}
}
// Author
$scope.author = workPackage.embedded.author;
}
$scope.toggleWatch = function() {

@ -185,4 +185,19 @@ describe('WorkPackageDetailsController', function() {
expect(scope.showStaticPagePath).to.eql('/work_packages/99');
});
});
describe('type', function() {
var type = { 'type': 'Type',
'name': 'type0815' };
beforeEach(function() {
workPackage.embedded.type = type;
buildController();
});
it('is the embedded type', function() {
expect(scope.type).to.eql(type);
});
});
});

@ -0,0 +1,67 @@
#-- 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 'reform'
require 'reform/form/active_model/model_validations'
module API
module Contracts
class ModelContract < Reform::Contract
def self.writable_attributes
@writable_attributes ||= []
end
def self.attribute_validations
@attribute_validations ||= []
end
def self.attribute(*attributes, &block)
writable_attributes.concat attributes.map(&:to_s)
if block
attribute_validations << block
end
end
validate :readonly_attributes_unchanged
validate :run_attribute_validations
private
def readonly_attributes_unchanged
changed_attributes = model.changed - self.class.writable_attributes
errors.add :error_readonly, changed_attributes unless changed_attributes.empty?
end
def run_attribute_validations
self.class.attribute_validations.each { |validation| instance_exec(&validation) }
end
end
end
end

@ -46,10 +46,11 @@ module API
exec_context: :decorator,
getter: -> (*) {
name = call_or_translate(name_source)
schema = ::API::Decorators::PropertySchemaRepresenter.new(type: type,
name: name,
required: required,
writable: writable)
schema = ::API::Decorators::PropertySchemaRepresenter.new(
type: type,
name: name,
required: call_or_use(required),
writable: call_or_use(writable))
schema.min_length = min_length
schema.max_length = max_length
schema.regular_expression = regular_expression
@ -74,8 +75,8 @@ module API
representer = ::API::Decorators::AllowedValuesByLinkRepresenter.new(
type: type,
name: call_or_translate(name_source),
required: required,
writable: writable)
required: call_or_use(required),
writable: call_or_use(writable))
if represented.defines_assignable_values?
representer.allowed_values_href = instance_eval(&href_callback)
@ -104,8 +105,8 @@ module API
current_user: current_user,
value_representer: value_representer,
link_factory: -> (value) { instance_exec(value, &link_factory) },
required: required,
writable: writable)
required: call_or_use(required),
writable: call_or_use(writable))
if represented.defines_assignable_values?
representer.allowed_values = instance_exec(&values_callback)
@ -127,9 +128,17 @@ module API
private
def call_or_use(object)
if object.respond_to? :call
instance_exec(&object)
else
object
end
end
def call_or_translate(object)
if object.respond_to? :call
object.call
instance_exec(&object)
else
self.class.represented_class.human_attribute_name(object)
end

@ -65,18 +65,36 @@ module API
path: property,
association: property,
title_getter: -> (*) { represented.send(association).name },
show_if: -> (*) { true })
show_if: -> (*) { true },
embed_as: nil)
link property do
next unless instance_eval(&show_if)
value = represented.send(association)
link_object = { href: (api_v3_paths.send(path, value.id) if value) }
link_object[:title] = instance_eval(&title_getter) if value
if value
{
href: api_v3_paths.send(path, value.id),
title: instance_eval(&title_getter)
}
else
{ href: nil }
end
end
link_object
if embed_as
embed_property property,
association: association,
decorator: embed_as
end
end
def self.embed_property(property, association: property, decorator:)
property association,
as: property.to_s.camelize(:lower),
embedded: true,
decorator: decorator
end
protected
def current_user

@ -104,14 +104,19 @@ module API
# checks whether the user has
# any of the provided permission in any of the provided
# projects
def authorize_any(permissions, projects, user: current_user)
def authorize_any(permissions, projects: nil, global: false, user: current_user)
raise ArgumentError if projects.nil? && !global
projects = Array(projects)
authorized = permissions.any? do |permission|
allowed_condition = Project.allowed_to_condition(user, permission)
allowed_projects = Project.where(allowed_condition)
!(allowed_projects & projects).empty?
if global
allowed_projects.any?
else
!(allowed_projects & projects).empty?
end
end
raise API::Errors::Unauthorized unless authorized

@ -51,7 +51,8 @@ module API
mount API::V3::Projects::AvailableAssigneesAPI
mount API::V3::Projects::AvailableResponsiblesAPI
mount API::V3::Categories::CategoriesByProjectAPI
mount API::V3::Versions::ProjectsVersionsAPI
mount API::V3::Versions::VersionsByProjectAPI
mount API::V3::Types::TypesByProjectAPI
end
end
end

@ -45,6 +45,7 @@ module API
mount ::API::V3::Render::RenderAPI
mount ::API::V3::Statuses::StatusesAPI
mount ::API::V3::StringObjects::StringObjectsAPI
mount ::API::V3::Types::TypesAPI
mount ::API::V3::Users::UsersAPI
mount ::API::V3::Versions::VersionsAPI
mount ::API::V3::WorkPackages::WorkPackagesAPI

@ -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 status 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 status 2
# of the License, or (at your option) any later status.
#
# 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 Types
class TypeCollectionRepresenter < ::API::Decorators::Collection
element_decorator ::API::V3::Types::TypeRepresenter
end
end
end
end

@ -0,0 +1,57 @@
#-- 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 Types
class TypeRepresenter < ::API::Decorators::Single
self_link
property :id
property :name
property :color,
getter: -> (*) { color.hexcode if color },
render_nil: true
property :position
property :is_default
property :is_milestone
property :created_at,
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.created_at) }
property :updated_at,
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.updated_at) }
def _type
'Type'
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.
#++
module API
module V3
module Types
class TypesAPI < Grape::API
resources :types do
before do
authorize_any([:view_work_packages, :manage_types], global: true)
end
get do
types = Type.all
TypeCollectionRepresenter.new(types,
types.count,
api_v3_paths.types)
end
namespace ':id' do
before do
type = Type.find(params[:id])
@representer = ::API::V3::Types::TypeRepresenter.new(type)
end
get do
@representer
end
end
end
end
end
end
end

@ -0,0 +1,50 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-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.
#
# 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 Types
class TypesByProjectAPI < Grape::API
resources :types do
before do
authorize_any [:view_work_packages, :manage_types], projects: @project
end
get do
types = @project.types
TypeCollectionRepresenter.new(types,
types.count,
api_v3_paths.types_by_project(@project.id),
context: { current_user: current_user })
end
end
end
end
end
end

@ -116,6 +116,18 @@ module API
"#{root}/string_objects/#{::ERB::Util::url_encode(value)}"
end
def self.types
"#{root}/types"
end
def self.types_by_project(project_id)
"#{project(project_id)}/types"
end
def self.type(id)
"#{types}/#{id}"
end
def self.users
"#{root}/users"
end

@ -30,7 +30,7 @@
module API
module V3
module Versions
class VersionsProjectsAPI < Grape::API
class ProjectsByVersionAPI < Grape::API
resources :projects do
before do
@projects = @version.projects.visible(current_user).all

@ -47,7 +47,7 @@ module API
permissions = [:view_work_packages, :manage_versions]
authorize_any(permissions, projects, user: current_user)
authorize_any(permissions, projects: projects, user: current_user)
end
def context
@ -59,7 +59,7 @@ module API
VersionRepresenter.new(@version, context)
end
mount API::V3::Versions::VersionsProjectsAPI
mount API::V3::Versions::ProjectsByVersionAPI
end
end
end

@ -30,18 +30,18 @@
module API
module V3
module Versions
class ProjectsVersionsAPI < Grape::API
class VersionsByProjectAPI < Grape::API
resources :versions do
before do
@versions = @project.shared_versions.all
authorize_any [:view_work_packages, :manage_versions], @project
authorize_any [:view_work_packages, :manage_versions], projects: @project
end
get do
VersionCollectionRepresenter.new(@versions,
@versions.count,
api_v3_paths.versions(@project.identifier),
api_v3_paths.versions(@project.id),
context: { current_user: current_user })
end
end

@ -31,25 +31,18 @@ module API
module WorkPackages
module Form
class FormAPI < Grape::API
helpers do
def process_form_request
write_work_package_attributes
write_request_valid?
post '/form' do
write_work_package_attributes
write_request_valid?
error = ::API::Errors::ErrorBase.create(@representer.represented.errors)
error = ::API::Errors::ErrorBase.create(@work_package.errors)
if error.is_a? ::API::Errors::Validation
status 200
FormRepresenter.new(@representer.represented, current_user: current_user)
else
fail error
end
if error.is_a? ::API::Errors::Validation
status 200
FormRepresenter.new(@work_package, current_user: current_user)
else
fail error
end
end
post '/form' do
process_form_request
end
end
end

@ -72,6 +72,11 @@ module API
}
end
linked_property(property_name: :type,
namespace: :types,
method: :type_id,
path: :type)
linked_property(property_name: :status,
namespace: :statuses,
method: :status_id,

@ -35,7 +35,7 @@ module API
module WorkPackages
module Schema
class WorkPackageSchema
attr_reader :project
attr_reader :project, :type
def initialize(work_package: nil, project: nil, type: nil)
raise ArgumentError unless work_package || (project && type)
@ -54,14 +54,21 @@ module API
status_origin = @work_package
# do not allow to skip statuses without intermediate saving
# do not allow to skip statuses without intermediately saving the work package
# we therefore take the original status of the work_package, while preserving all
# other changes to it (e.g. type, assignee, etc.)
if @work_package.persisted? && @work_package.status_id_changed?
status_origin = @work_package.class.find(@work_package.id)
status_origin = @work_package.clone
status_origin.status = Status.find_by_id(@work_package.status_id_was)
end
status_origin.new_statuses_allowed_to(user)
end
def assignable_types
@work_package.assignable_types if defines_assignable_values?
end
def assignable_versions
@work_package.assignable_versions if defines_assignable_values?
end
@ -75,6 +82,9 @@ module API
end
def available_custom_fields
# we might have received a (currently) invalid work package
return [] if @project.nil? || @type.nil?
@project.all_work_package_custom_fields & @type.custom_fields.all
end

@ -123,10 +123,6 @@ module API
type: 'Project',
writable: false
schema :type,
type: 'Type',
writable: false
schema_with_allowed_link :assignee,
type: 'User',
required: false,
@ -141,6 +137,19 @@ module API
api_v3_paths.available_responsibles(represented.project.id)
}
schema_with_allowed_collection :type,
type: 'Type',
values_callback: -> (*) {
represented.assignable_types
},
value_representer: Types::TypeRepresenter,
link_factory: -> (type) {
{
href: api_v3_paths.type(type.id),
title: type.name
}
}
schema_with_allowed_collection :status,
type: 'Status',
values_callback: -> (*) {

@ -27,30 +27,58 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'reform'
require 'reform/form/active_model/model_validations'
module API
module V3
module WorkPackages
class WorkPackageContract < Reform::Contract
def self.writable_attributes
@writable_attributes ||= %w(
lock_version
subject
parent_id
description
start_date
due_date
status_id
assigned_to_id
responsible_id
priority_id
category_id
fixed_version_id
done_ratio
estimated_hours
)
class WorkPackageContract < ::API::Contracts::ModelContract
attribute :subject
attribute :description
attribute :start_date, :due_date
attribute :status_id
attribute :type_id
attribute :priority_id
attribute :category_id
attribute :fixed_version_id
attribute :lock_version do
errors.add :error_conflict, '' if model.lock_version.nil? || model.lock_version_changed?
end
attribute :parent_id do
if model.changed.include? 'parent_id'
errors.add :error_unauthorized, '' unless @can.allowed?(model, :manage_subtasks)
end
end
attribute :assigned_to_id do
validate_people_visible :assignee,
'assigned_to_id',
model.project.possible_assignee_members
end
attribute :responsible_id do
validate_people_visible :responsible,
'responsible_id',
model.project.possible_responsible_members
end
attribute :done_ratio do
if model.changed.include?('done_ratio')
# TODO Allow multiple errors as soon as they have separate messages
if !model.leaf?
errors.add :error_readonly, 'done_ratio'
elsif Setting.work_package_done_ratio == 'status'
errors.add :error_readonly, 'done_ratio'
elsif Setting.work_package_done_ratio == 'disabled'
errors.add :error_readonly, 'done_ratio'
end
end
end
attribute :estimated_hours do
if !model.leaf? && model.changed.include?('estimated_hours')
errors.add :error_readonly, 'estimated_hours'
end
end
def initialize(object, user)
@ -62,19 +90,15 @@ module API
validate :user_allowed_to_access
validate :user_allowed_to_edit
validate :user_allowed_to_edit_parent
validate :lock_version_valid
validate :readonly_attributes_unchanged
validate :assignee_visible
validate :responsible_visible
validate :estimated_hours_valid
validate :done_ratio_valid
extend Reform::Form::ActiveModel::ModelValidations
copy_validations_from WorkPackage
private
# TODO: when someone every fixes the way errors are added in the contract:
# find a solution to ensure that THIS validation supersedes others (i.e. show 404 if
# there is no access allowed)
def user_allowed_to_access
unless ::WorkPackage.visible(@user).exists?(model)
errors.add :error_not_found, I18n.t('api_v3.errors.code_404')
@ -85,54 +109,7 @@ module API
errors.add :error_unauthorized, '' unless @can.allowed?(model, :edit)
end
def user_allowed_to_edit_parent
if parent_changed?
errors.add :error_unauthorized, '' unless @can.allowed?(model, :manage_subtasks)
end
end
def parent_changed?
model.changed.include? 'parent_id'
end
def lock_version_valid
errors.add :error_conflict, '' if model.lock_version.nil? || model.lock_version_changed?
end
def readonly_attributes_unchanged
changed_attributes = model.changed - self.class.writable_attributes
errors.add :error_readonly, changed_attributes unless changed_attributes.empty?
end
def assignee_visible
people_visible :assignee, 'assigned_to_id', model.project.possible_assignee_members
end
def responsible_visible
people_visible :responsible, 'responsible_id', model.project.possible_responsible_members
end
def estimated_hours_valid
if !model.leaf? && model.changed.include?('estimated_hours')
errors.add :error_readonly, 'estimated_hours'
end
end
def done_ratio_valid
if model.changed.include?('done_ratio')
# TODO Allow multiple errors as soon as they have separate messages
if !model.leaf?
errors.add :error_readonly, 'done_ratio'
elsif Setting.work_package_done_ratio == 'status'
errors.add :error_readonly, 'done_ratio'
elsif Setting.work_package_done_ratio == 'disabled'
errors.add :error_readonly, 'done_ratio'
end
end
end
def people_visible(attribute, id_attribute, list)
def validate_people_visible(attribute, id_attribute, list)
id = model[id_attribute]
return if id.nil? || !model.changed.include?(id_attribute)

@ -102,11 +102,15 @@ module API
} if current_user_allowed_to(:move_work_packages)
end
linked_property :status
linked_property :type, embed_as: ::API::V3::Types::TypeRepresenter
linked_property :status, embed_as: ::API::V3::Statuses::StatusRepresenter
linked_property :author, path: :user
linked_property :responsible, path: :user
linked_property :assignee, path: :user, association: :assigned_to
linked_property :author, path: :user, embed_as: ::API::V3::Users::UserRepresenter
linked_property :responsible, path: :user, embed_as: ::API::V3::Users::UserRepresenter
linked_property :assignee,
path: :user,
association: :assigned_to,
embed_as: ::API::V3::Users::UserRepresenter
link :availableWatchers do
{
@ -190,7 +194,9 @@ module API
} if current_user_allowed_to(:view_time_entries)
end
linked_property :category
linked_property :category, embed_as: ::API::V3::Categories::CategoryRepresenter
linked_property :priority, embed_as: ::API::V3::Priorities::PriorityRepresenter
linked_property :project, embed_as: ::API::V3::Projects::ProjectRepresenter
linked_property :version,
association: :fixed_version,
@ -198,10 +204,6 @@ module API
represented.fixed_version.to_s_for_project(represented.project)
}
linked_property :project
linked_property :priority
links :children do
visible_children.map do |child|
{ href: "#{root_path}api/v3/work_packages/#{child.id}", title: child.subject }
@ -211,7 +213,6 @@ module API
property :id, render_nil: true
property :lock_version
property :subject, render_nil: true
property :type, getter: -> (*) { type.try(:name) }, render_nil: true
property :description,
exec_context: :decorator,
getter: -> (*) {
@ -260,41 +261,12 @@ module API
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.updated_at) }
property :status,
embedded: true,
class: ::Status,
decorator: ::API::V3::Statuses::StatusRepresenter
property :author,
embedded: true,
class: ::User,
decorator: ::API::V3::Users::UserRepresenter
property :responsible,
embedded: true,
class: ::User,
decorator: ::API::V3::Users::UserRepresenter
property :assigned_to,
as: :assignee,
embedded: true,
class: ::User,
decorator: ::API::V3::Users::UserRepresenter
property :category,
embedded: true,
class: ::Category,
decorator: ::API::V3::Categories::CategoryRepresenter
property :priority,
embedded: true,
class: ::IssuePriority,
decorator: ::API::V3::Priorities::PriorityRepresenter
property :activities, embedded: true, exec_context: :decorator
property :version,
embedded: true,
exec_context: :decorator
property :project,
embedded: true,
class: ::Project,
decorator: ::API::V3::Projects::ProjectRepresenter
exec_context: :decorator,
if: ->(*) { represented.fixed_version.present? }
property :watchers,
embedded: true,
exec_context: :decorator,

@ -38,29 +38,44 @@ module API
helpers do
attr_reader :work_package
def work_package_representer
WorkPackages::WorkPackageRepresenter.create(@work_package,
current_user: current_user)
end
def write_work_package_attributes
if request_body
payload = ::API::V3::WorkPackages::Form::WorkPackagePayloadRepresenter.create(
@work_package,
enforce_lock_version_validation: true)
begin
payload.from_json(request_body.to_json)
# we need to merge the JSON two times:
# In Pass 1 the representer only has custom fields for the current WP type
# After Pass 1 the correct type information is merged into the WP
# In Pass 2 the representer is created with the new type info and will be able
# to also parse custom fields successfully
merge_json_into_work_package!(request_body.to_json)
merge_json_into_work_package!(request_body.to_json)
rescue ::API::Errors::Form::InvalidResourceLink => e
fail ::API::Errors::Validation.new(e.message)
end
end
end
# merges the given JSON representation into @work_package
def merge_json_into_work_package!(json)
payload = ::API::V3::WorkPackages::Form::WorkPackagePayloadRepresenter.create(
@work_package,
enforce_lock_version_validation: true)
payload.from_json(json)
end
def request_body
env['api.request.body']
end
def write_request_valid?
contract = WorkPackageContract.new(@representer.represented, current_user)
contract = WorkPackageContract.new(@work_package, current_user)
contract_valid = contract.validate
represented_valid = @representer.represented.valid?
represented_valid = @work_package.valid?
return true if contract_valid && represented_valid
@ -68,7 +83,7 @@ module API
# order to have them available at one place.
contract.errors.keys.each do |key|
contract.errors[key].each do |message|
@representer.represented.errors.add(key, message)
@work_package.errors.add(key, message)
end
end
@ -78,14 +93,12 @@ module API
before do
@work_package = WorkPackage.find(params[:id])
@representer = WorkPackages::WorkPackageRepresenter.create(work_package,
current_user: current_user)
end
get do
authorize({ controller: :work_packages_api, action: :get },
context: @work_package.project)
@representer
work_package_representer
end
patch do
@ -93,15 +106,16 @@ module API
send_notifications = !(params.has_key?(:notify) && params[:notify] == 'false')
update_service = UpdateWorkPackageService.new(current_user,
@representer.represented,
@work_package,
nil,
send_notifications)
if write_request_valid? && update_service.save
@representer.represented.reload
@representer
@work_package.reload
work_package_representer
else
fail ::API::Errors::ErrorBase.create(@representer.represented.errors.dup)
fail ::API::Errors::ErrorBase.create(@work_package.errors.dup)
end
end

@ -57,17 +57,27 @@ module OpenProject::Plugins
# This looks for OpenProject::XlsExport::Patches::IssuesControllerPatch
# in openproject/xls_export/patches/issues_controller_patch.rb
base.send(:define_method, :patches) do |patched_classes|
plugin_name = engine_name
plugin_module = self.class.to_s.deconstantize
base.config.to_prepare do
patched_classes.each do |klass_name|
plugin_module = plugin_name.sub(/^openproject_/, '').camelcase
patch = "OpenProject::#{plugin_module}::Patches::#{klass_name}Patch".constantize
patch = "#{plugin_module}::Patches::#{klass_name}Patch".constantize
klass = klass_name.to_s.constantize
klass.send(:include, patch) unless klass.included_modules.include?(patch)
end
end
end
base.send(:define_method, :patch_with_namespace) do |*args|
plugin_module = self.class.to_s.deconstantize
base.config.to_prepare do
klass_name = args.last
patch = "#{plugin_module}::Patches::#{klass_name}Patch".constantize
qualified_class_name = args.map(&:to_s).join('::')
klass = qualified_class_name.to_s.constantize
klass.send(:include, patch) unless klass.included_modules.include?(patch)
end
end
# Define assets provided by the plugin
base.send(:define_method, :assets) do |assets|
base.initializer "#{engine_name}.precompile_assets" do |app|
@ -139,12 +149,12 @@ module OpenProject::Plugins
end
end
base.send(:define_method, :allow_attribute_update) do |model, attribute|
base.send(:define_method, :allow_attribute_update) do |model, attribute, &block|
config.to_prepare do
model_name = model.to_s.camelize
namespace = model_name.pluralize
contract_class = "::API::V3::#{namespace}::#{model_name}Contract".constantize
contract_class.writable_attributes << attribute.to_s
contract_class.attribute attribute, &block
end
end

@ -46,7 +46,6 @@ module Redmine
order: "#{CustomField.table_name}.position",
dependent: :delete_all,
validate: false
before_validation { |customized| customized.custom_field_values if customized.new_record? }
validate :validate_custom_values
send :include, Redmine::Acts::Customizable::InstanceMethods
# Save custom values when saving the customized object
@ -83,12 +82,19 @@ module Redmine
@custom_field_values_changed = true
values = values.stringify_keys
custom_field_values.each do |custom_value|
custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s)
if values.has_key?(custom_value.custom_field_id.to_s)
custom_value.value = values[custom_value.custom_field_id.to_s]
end
end if values.is_a?(Hash)
end
def custom_field_values
@custom_field_values ||= available_custom_fields.map { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(customized: self, custom_field: x, value: nil) }
@custom_field_values ||= available_custom_fields.map do |custom_field|
existing_cv = custom_values.detect { |v| v.custom_field == custom_field }
existing_cv || custom_values.build(customized: self,
custom_field: custom_field,
value: nil)
end
end
def visible_custom_field_values
@ -114,14 +120,13 @@ module Redmine
def reset_custom_values!
@custom_field_values = nil
@custom_field_values_changed = true
values = custom_values.inject({}) { |h, v| h[v.custom_field_id] = v.value; h }
custom_values.each { |cv| cv.destroy unless custom_field_values.include?(cv) }
end
def validate_custom_values
custom_values.reject(&:marked_for_destruction?).select(&:invalid?).each do |custom_value|
custom_value.errors.each do |_, message|
errors.add(custom_value.custom_field.accessor_name.to_sym, message)
custom_field_values.reject(&:marked_for_destruction?).select(&:invalid?).each do |cv|
cv.errors.each do |_, message|
errors.add(cv.custom_field.accessor_name.to_sym, message)
end
end
end

@ -145,6 +145,8 @@ describe VersionsController, type: :controller do
end
context 'from issue form' do
render_views
before do
allow(User).to receive(:current).and_return(user)
post :create, project_id: project.id, version: { name: 'test_add_version_from_issue_form' }, format: :js
@ -159,12 +161,9 @@ describe VersionsController, type: :controller do
it 'returns updated select box with new version' do
version = Version.find_by_name('test_add_version_from_issue_form')
select_substring = "select id=\\\"work_package_fixed_version_id\\\" name=\\\"work_package[fixed_version_id]\\\""
# selected option tag for the new version
option_substring = "option value=\\\"#{version.id}\\\" selected=\\\"selected\\\""
expect(response.body.include?(select_substring)).to be_truthy
expect(response.body.include?(option_substring)).to be_truthy
expect(response.body).to include(
"option value=\\\"#{version.id}\\\" selected=\\\"selected\\\""
)
end
it 'escapes potentially harmful html' do

@ -30,17 +30,29 @@ FactoryGirl.define do
factory :type do
sequence(:position) { |p| p }
name { |a| "Type No. #{a.position}" }
created_at { Time.now }
updated_at { Time.now }
factory :type_with_workflow, class: Type do
callback(:after_build) do |t|
t.workflows = [FactoryGirl.build(:workflow_with_default_status)]
end
end
end
factory :type_standard, class: Type do
name 'None'
is_standard true
is_default true
created_at { Time.now }
updated_at { Time.now }
end
factory :type_bug, class: Type do
name 'Bug'
position 1
created_at { Time.now }
updated_at { Time.now }
# reuse existing type with the given name
# this prevents a validation error (name has to be unique)
@ -62,12 +74,4 @@ FactoryGirl.define do
position 4
end
end
factory :type_with_workflow, class: Type do
sequence(:name) { |n| "Type #{n}" }
sequence(:position) { |n| n }
callback(:after_build) do |t|
t.workflows = [FactoryGirl.build(:workflow_with_default_status)]
end
end
end

@ -78,28 +78,59 @@ describe SettingsHelper, type: :helper do
end
end
describe '#settings_multiselect' do
describe '#settings_matrix' do
before do
expect(Setting).to receive(:field_a).at_least(:once).and_return('2')
expect(Setting).to receive(:field_b).at_least(:once).and_return('3')
end
subject(:output) {
helper.settings_multiselect [:field_a, :field_b], [
['Popsickle', '1'], ['Jello', '2'], ['Ice Cream', '3']
helper.settings_matrix [:field_a, :field_b], [
['Popsickle', '1'], ['Jello', '2'], ['Ice Cream', '3'], 'Quarkspeise'
]
}
it_behaves_like 'not wrapped in container'
it 'should have checkboxes wrapped in checkbox-container' do
expect(output).to have_selector 'span.form--check-box-container', count: 6
it 'is structured as a table' do
expect(output).to have_selector 'table.form--matrix'
end
it 'should output element' do
expect(output).to have_selector 'table'
expect(output).to have_selector 'input[type="checkbox"].form--check-box'
it 'has table headers' do
expect(output).to have_selector 'thead th.form--matrix-header-cell', count: 3
end
it 'has three table rows' do
expect(output).to have_selector 'tbody > tr.form--matrix-row', count: 4
end
it 'has cells with text labels' do
expect(output).to be_html_eql(%{
<td class="form--matrix-cell">Popsickle</td>
}).at_path('tr:first-child > td:first-child')
end
it 'has cells with styled checkboxes' do
expect(output).to be_html_eql(%{
<td class="form--matrix-checkbox-cell">
<span class="form--check-box-container">
<input class="form--check-box" id="field_a_1"
name="settings[field_a][]" type="checkbox" value="1">
</span>
</td>
}).at_path('tr.form--matrix-row:first-child > td:nth-of-type(2)')
expect(output).to be_html_eql(%{
<td class="form--matrix-checkbox-cell">
<span class="form--check-box-container">
<input class="form--check-box" id="field_a_Quarkspeise"
name="settings[field_a][]" type="checkbox" value="Quarkspeise">
</span>
</td>
}).at_path('tr.form--matrix-row:last-child > td:nth-of-type(2)')
end
it 'has the correct fields checked' do
expect(output).to have_checked_field 'field_a_2'
expect(output).to have_checked_field 'field_b_3'
end

@ -0,0 +1,106 @@
#-- 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::Types::TypeRepresenter do
let(:type) { FactoryGirl.build_stubbed(:type, color: FactoryGirl.build(:color)) }
let(:representer) { described_class.new(type) }
include API::V3::Utilities::PathHelper
context 'generation' do
subject { representer.to_json }
describe 'links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.type(type.id) }
let(:title) { type.name }
end
end
it 'indicates its id' do
is_expected.to be_json_eql(type.id.to_json).at_path('id')
end
it 'indicates its name' do
is_expected.to be_json_eql(type.name.to_json).at_path('name')
end
it 'indicates its color' do
is_expected.to be_json_eql(type.color.hexcode.to_json).at_path('color')
end
context 'no color set' do
let(:type) { FactoryGirl.build(:type, color: nil) }
it 'indicates a missing color' do
is_expected.to be_json_eql(nil.to_json).at_path('color')
end
end
it 'indicates its position' do
is_expected.to be_json_eql(type.position.to_json).at_path('position')
end
it 'indicates that it is not the default type' do
is_expected.to be_json_eql(false.to_json).at_path('isDefault')
end
context 'as default type' do
let(:type) { FactoryGirl.build(:type, is_default: true) }
it 'indicates that it is the default type' do
is_expected.to be_json_eql(true.to_json).at_path('isDefault')
end
end
it 'indicates that it is not a milestone' do
is_expected.to be_json_eql(false.to_json).at_path('isMilestone')
end
context 'as milestone' do
let(:type) { FactoryGirl.build(:type, is_milestone: true) }
it 'indicates that it is a milestone' do
is_expected.to be_json_eql(true.to_json).at_path('isMilestone')
end
end
it_behaves_like 'has UTC ISO 8601 date and time' do
let(:date) { type.created_at }
let(:json_path) { 'createdAt' }
end
it_behaves_like 'has UTC ISO 8601 date and time' do
let(:date) { type.updated_at }
let(:json_path) { 'updatedAt' }
end
end
end

@ -254,6 +254,32 @@ describe ::API::V3::Utilities::PathHelper do
end
end
describe 'types paths' do
describe '#types' do
subject { helper.types }
it_behaves_like 'api v3 path'
it { is_expected.to eql('/api/v3/types') }
end
describe '#types_by_project' do
subject { helper.types_by_project 12 }
it_behaves_like 'api v3 path'
it { is_expected.to eql('/api/v3/projects/12/types') }
end
describe '#type' do
subject { helper.type 1 }
it_behaves_like 'api v3 path'
it { is_expected.to eql('/api/v3/types/1') }
end
end
describe '#user' do
subject { helper.user 1 }

@ -62,11 +62,11 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchema do
end
describe '#assignable_statuses_for' do
let(:user) { double }
let(:status_result) { double }
let(:user) { double('current user') }
let(:status_result) { double('status result') }
before do
allow(work_package).to receive(:is_persisted?).and_return(false)
allow(work_package).to receive(:persisted?).and_return(false)
allow(work_package).to receive(:status_id_changed?).and_return(false)
end
@ -77,24 +77,60 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchema do
end
context 'changed work package' do
let(:work_package) { FactoryGirl.create(:work_package) }
let(:stored_wp) { FactoryGirl.build(:work_package, id: work_package.id) }
let(:work_package) {
double('original work package',
id: double,
clone: cloned_wp,
status: double('wrong status'),
persisted?: true).as_null_object
}
let(:cloned_wp) {
double('cloned work package',
new_statuses_allowed_to: status_result)
}
let(:stored_status) {
double('good status')
}
before do
allow(work_package).to receive(:persisted?).and_return(true)
allow(work_package).to receive(:status_id_changed?).and_return(true)
allow(WorkPackage).to receive(:find).with(work_package.id).and_return(stored_wp)
allow(Status).to receive(:find_by_id)
.with(work_package.status_id_was).and_return(stored_status)
end
it 'calls through to the stored work package' do
expect(work_package).to_not receive(:new_statuses_allowed_to)
expect(stored_wp).to receive(:new_statuses_allowed_to).with(user)
.and_return(status_result)
it 'calls through to the cloned work package' do
expect(cloned_wp).to receive(:status=).with(stored_status)
expect(cloned_wp).to receive(:new_statuses_allowed_to).with(user)
expect(subject.assignable_statuses_for(user)).to eql(status_result)
end
end
describe '#available_custom_fields' do
it_behaves_like 'WorkPackageSchema#available_custom_fields'
context 'type missing' do
let(:type) { nil }
it 'returns an empty list' do
expect(subject.available_custom_fields).to eql([])
end
end
context 'project missing' do
let(:project) { nil }
it 'returns an empty list' do
expect(subject.available_custom_fields).to eql([])
end
end
end
end
describe '#assignable_types' do
let(:result) { double }
it 'calls through to the work package' do
expect(work_package).to receive(:assignable_types).and_return(result)
expect(subject.assignable_types).to eql(result)
end
end

@ -125,7 +125,6 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
end
it { is_expected.to have_json_path('subject') }
it { is_expected.to have_json_path('type') }
describe 'lock version' do
it { is_expected.to have_json_path('lockVersion') }
@ -272,6 +271,14 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
end
end
describe 'type' do
it_behaves_like 'has a titled link' do
let(:link) { 'type' }
let(:href) { "/api/v3/types/#{work_package.type_id}" }
let(:title) { work_package.type.name }
end
end
describe 'author' do
it_behaves_like 'has a titled link' do
let(:link) { 'author' }

@ -42,6 +42,36 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
context 'generation' do
subject(:generated) { representer.to_json }
shared_examples_for 'has a collection of allowed values' do
context 'when no values are allowed' do
before { allow(schema).to receive(allowed_values_method).and_return([]) }
it_behaves_like 'links to and embeds allowed values directly' do
let(:path) { json_path }
let(:hrefs) { [] }
end
end
context 'when values are allowed' do
let(:values) { FactoryGirl.build_stubbed_list(factory, 3) }
before { allow(schema).to receive(allowed_values_method).and_return(values) }
it_behaves_like 'links to and embeds allowed values directly' do
let(:path) { json_path }
let(:hrefs) { values.map { |value| "/api/v3/#{href_path}/#{value.id}" } }
end
end
context 'when allowed values are not defined' do
include_context 'no allowed values'
it_behaves_like 'does not link to allowed values' do
let(:path) { json_path }
end
end
end
shared_context 'no allowed values' do
before do
allow(schema).to receive(:defines_assignable_values?).and_return(false)
@ -208,7 +238,14 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
let(:type) { 'Type' }
let(:name) { I18n.t('activerecord.attributes.work_package.type') }
let(:required) { true }
let(:writable) { false }
let(:writable) { true }
end
it_behaves_like 'has a collection of allowed values' do
let(:json_path) { 'type' }
let(:href_path) { 'types' }
let(:factory) { :type }
let(:allowed_values_method) { :assignable_types }
end
end
@ -221,32 +258,11 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
let(:writable) { true }
end
context 'w/o allowed statuses' do
before { allow(work_package).to receive(:new_statuses_allowed_to).and_return([]) }
it_behaves_like 'links to and embeds allowed values directly' do
let(:path) { 'status' }
let(:hrefs) { [] }
end
end
context 'with allowed statuses' do
let(:statuses) { FactoryGirl.build_list(:status, 3) }
before { allow(work_package).to receive(:new_statuses_allowed_to).and_return(statuses) }
it_behaves_like 'links to and embeds allowed values directly' do
let(:path) { 'status' }
let(:hrefs) { statuses.map { |status| "/api/v3/statuses/#{status.id}" } }
end
end
context 'when allowed values are not defined' do
include_context 'no allowed values'
it_behaves_like 'does not link to allowed values' do
let(:path) { 'status' }
end
it_behaves_like 'has a collection of allowed values' do
let(:json_path) { 'status' }
let(:href_path) { 'statuses' }
let(:factory) { :status }
let(:allowed_values_method) { :assignable_statuses_for }
end
end
@ -259,32 +275,11 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
let(:writable) { true }
end
context 'w/o allowed categories' do
before { allow(work_package).to receive(:assignable_categories).and_return([]) }
it_behaves_like 'links to allowed values directly' do
let(:path) { 'category' }
let(:hrefs) { [] }
end
end
context 'with allowed categories' do
let(:categories) { FactoryGirl.build_stubbed_list(:category, 3) }
before { allow(work_package).to receive(:assignable_categories).and_return(categories) }
it_behaves_like 'links to allowed values directly' do
let(:path) { 'category' }
let(:hrefs) { categories.map { |category| "/api/v3/categories/#{category.id}" } }
end
end
context 'when allowed values are not defined' do
include_context 'no allowed values'
it_behaves_like 'does not link to allowed values' do
let(:path) { 'category' }
end
it_behaves_like 'has a collection of allowed values' do
let(:json_path) { 'category' }
let(:href_path) { 'categories' }
let(:factory) { :category }
let(:allowed_values_method) { :assignable_categories }
end
end
@ -297,32 +292,11 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
let(:writable) { true }
end
context 'w/o allowed versions' do
before { allow(work_package).to receive(:assignable_versions).and_return([]) }
it_behaves_like 'links to and embeds allowed values directly' do
let(:path) { 'version' }
let(:hrefs) { [] }
end
end
context 'with allowed versions' do
let(:versions) { FactoryGirl.build_stubbed_list(:version, 3) }
before { allow(work_package).to receive(:assignable_versions).and_return(versions) }
it_behaves_like 'links to and embeds allowed values directly' do
let(:path) { 'version' }
let(:hrefs) { versions.map { |version| "/api/v3/versions/#{version.id}" } }
end
end
context 'when allowed values are not defined' do
include_context 'no allowed values'
it_behaves_like 'does not link to allowed values' do
let(:path) { 'version' }
end
it_behaves_like 'has a collection of allowed values' do
let(:json_path) { 'version' }
let(:href_path) { 'versions' }
let(:factory) { :version }
let(:allowed_values_method) { :assignable_versions }
end
end
@ -335,32 +309,11 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
let(:writable) { true }
end
context 'w/o allowed priorities' do
before { allow(work_package).to receive(:assignable_priorities).and_return([]) }
it_behaves_like 'links to and embeds allowed values directly' do
let(:path) { 'priority' }
let(:hrefs) { [] }
end
end
context 'with allowed priorities' do
let(:priorities) { FactoryGirl.build_stubbed_list(:priority, 3) }
before { allow(work_package).to receive(:assignable_priorities).and_return(priorities) }
it_behaves_like 'links to and embeds allowed values directly' do
let(:path) { 'priority' }
let(:hrefs) { priorities.map { |priority| "/api/v3/priorities/#{priority.id}" } }
end
end
context 'when allowed values are not defined' do
include_context 'no allowed values'
it_behaves_like 'does not link to allowed values' do
let(:path) { 'priority' }
end
it_behaves_like 'has a collection of allowed values' do
let(:json_path) { 'priority' }
let(:href_path) { 'priorities' }
let(:factory) { :priority }
let(:allowed_values_method) { :assignable_priorities }
end
end

@ -1620,9 +1620,6 @@ describe WorkPackage, type: :model do
work_package.save!
work_package.reload
# is it fine?
expect(work_package).to be_valid
# now give the work_package another required custom field, but don't assign a value
work_package.project.work_package_custom_fields << cf2
work_package.type.custom_fields << cf2

@ -37,10 +37,10 @@ describe 'API v3 Project resource' do
let(:role) { FactoryGirl.create(:role) }
describe '#get' do
let(:get_path) { "/api/v3/projects/#{project.id}" }
subject(:response) { last_response }
context 'logged in user' do
let(:get_path) { "/api/v3/projects/#{project.id}" }
before do
allow(User).to receive(:current).and_return current_user
member = FactoryGirl.build(:member, user: current_user, project: project)
@ -77,5 +77,13 @@ describe 'API v3 Project resource' do
end
end
end
context 'not logged in user' do
before do
get get_path
end
it_behaves_like 'not found'
end
end
end

@ -0,0 +1,113 @@
#-- 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 Type resource' do
include Rack::Test::Methods
let(:role) { FactoryGirl.create(:role, permissions: [:view_work_packages]) }
let(:project) { FactoryGirl.create(:project, no_types: true, is_public: false) }
let(:current_user) do
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
end
let!(:types) { FactoryGirl.create_list(:type, 4) }
describe 'types' do
describe '#get' do
let(:get_path) { '/api/v3/types' }
subject(:response) { last_response }
context 'logged in user' do
before do
allow(User).to receive(:current).and_return current_user
get get_path
end
it_behaves_like 'API V3 collection response', 4, 4, 'Type'
end
context 'not logged in user' do
before do
get get_path
end
it_behaves_like 'error response',
403,
'MissingPermission',
I18n.t('api_v3.errors.code_403')
end
end
end
describe 'types/:id' do
describe '#get' do
let(:status) { types.first }
let(:get_path) { "/api/v3/types/#{status.id}" }
subject(:response) { last_response }
context 'logged in user' do
before do
allow(User).to receive(:current).and_return(current_user)
get get_path
end
context 'valid type id' do
it { expect(response.status).to eq(200) }
end
context 'invalid type id' do
let(:get_path) { '/api/v3/types/bogus' }
it_behaves_like 'not found' do
let(:id) { 'bogus' }
let(:type) { 'Type' }
end
end
end
context 'not logged in user' do
before do
get get_path
end
it_behaves_like 'error response',
403,
'MissingPermission',
I18n.t('api_v3.errors.code_403')
end
end
end
end

@ -0,0 +1,95 @@
#-- 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/projects/:id/types' do
include Rack::Test::Methods
let(:role) { FactoryGirl.create(:role, permissions: [:view_work_packages]) }
let(:project) { FactoryGirl.create(:project, no_types: true, is_public: false) }
let(:requested_project) { project }
let(:current_user) do
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
end
let!(:irrelevant_types) { FactoryGirl.create_list(:type, 4) }
let!(:expected_types) { FactoryGirl.create_list(:type, 4) }
describe '#get' do
let(:get_path) { "/api/v3/projects/#{requested_project.id}/types" }
subject(:response) { last_response }
before do
project.types << expected_types
end
context 'logged in user' do
before do
allow(User).to receive(:current).and_return current_user
get get_path
end
it_behaves_like 'API V3 collection response', 4, 4, 'Type'
it 'only contains expected types' do
actual_types = JSON.parse(subject.body)['_embedded']['elements']
actual_type_ids = actual_types.map { |hash| hash['id'] }
expected_type_ids = expected_types.map(&:id)
expect(actual_type_ids).to match_array expected_type_ids
end
# N.B. this test depends on order, while this is not strictly neccessary
it 'only contains expected types' do
(0..3).each do |i|
expected_id = expected_types[i].id.to_json
expect(subject.body).to be_json_eql(expected_id).at_path("_embedded/elements/#{i}/id")
end
end
context 'in a foreign project' do
let(:requested_project) { FactoryGirl.create(:project, is_public: false) }
it_behaves_like 'not found'
end
end
context 'not logged in user' do
before do
get get_path
end
it_behaves_like 'not found'
end
end
end

@ -198,7 +198,6 @@ h4. things we like
end
end
# disabled the its below because the implementation was temporarily disabled
describe '#patch' do
let(:patch_path) { "/api/v3/work_packages/#{work_package.id}" }
let(:valid_params) do
@ -472,6 +471,71 @@ h4. things we like
end
end
context 'type' do
let(:target_type) { FactoryGirl.create(:type) }
let(:type_link) { "/api/v3/types/#{target_type.id}" }
let(:type_parameter) { { _links: { type: { href: type_link } } } }
let(:params) { valid_params.merge(type_parameter) }
before { allow(User).to receive(:current).and_return current_user }
context 'valid type' do
before do
project.types << target_type
end
include_context 'patch request'
it { expect(response.status).to eq(200) }
it 'should respond with updated work package type' do
expect(subject.body).to be_json_eql(target_type.name.to_json)
.at_path('_embedded/type/name')
end
it_behaves_like 'lock version updated'
end
context 'valid type changing custom fields' do
let(:custom_field) { FactoryGirl.create(:work_package_custom_field) }
before do
project.types << target_type
project.work_package_custom_fields << custom_field
target_type.custom_fields << custom_field
end
include_context 'patch request'
it 'responds with the new custom field added' do
expect(subject.body).to have_json_path("customField#{custom_field.id}")
end
end
context 'invalid type' do
include_context 'patch request'
it_behaves_like 'constraint violation' do
let(:message) { "Type #{I18n.t('activerecord.errors.messages.inclusion')}" }
end
end
context 'wrong resource' do
let(:type_link) { "/api/v3/users/#{current_user.id}" }
include_context 'patch request'
it_behaves_like 'constraint violation' do
let(:message) {
I18n.t('api_v3.errors.invalid_resource',
property: 'type',
expected: '/api/v3/types/:id',
actual: type_link)
}
end
end
end
context 'assignee and responsible' do
let(:user) { FactoryGirl.create(:user, member_in_project: project) }
let(:params) { valid_params.merge(user_parameter) }

@ -624,6 +624,48 @@ describe 'API v3 Work package form resource', type: :request do
end
end
describe 'type' do
let(:path) { '_embedded/payload/_links/type/href' }
let(:links_path) { '_embedded/schema/type/_links' }
let(:target_type) { FactoryGirl.create(:type) }
let(:other_type) { work_package.type }
let(:type_link) { "/api/v3/types/#{target_type.id}" }
let(:other_type_link) { "/api/v3/types/#{other_type.id}" }
let(:type_parameter) { { _links: { type: { href: type_link } } } }
let(:params) { valid_params.merge(type_parameter) }
before do
project.types << target_type # make sure we have a valid transition
end
describe 'allowed values' do
before do
other_type
end
include_context 'post request'
it 'should list the types' do
expect(subject.body).to be_json_eql(type_link.to_json)
.at_path("#{links_path}/allowedValues/1/href")
expect(subject.body).to be_json_eql(other_type_link.to_json)
.at_path("#{links_path}/allowedValues/0/href")
end
end
context 'valid type' do
include_context 'post request'
it_behaves_like 'valid payload'
it_behaves_like 'having no errors'
it 'should respond with updated work package type' do
expect(subject.body).to be_json_eql(type_link.to_json).at_path(path)
end
end
end
describe 'multiple errors' do
let(:user_link) { '/api/v3/users/42' }
let(:status_link) { '/api/v3/statuses/-1' }

Loading…
Cancel
Save