Fix/bump representable (#5465)

* bump reform and roar -> bumps representer

* adapt to changed validation interface

* disable initializer patch for now

* adapt to changed representable attr interface

* can no longer have private methods inside a representer

* private no longer possible for representer

* bump reform

* wip - restyle validation

* remove commented out patch

* apply injection as prescribed

* reactivate reform error symbols patch

* remove patch to Hash superfluous wit ruby 2.3

* remove outdated human_attribute_name patch

* whitespace fixes

* adapt filter name after removal of human_attribute_name patch

* adapt filter specs to no longer rely on human_attribute_name patch

* fix version filter name

* remove reliance on no longer existing human_attribute_name patch

* use correct key in journal formatter

* remove private from representer

* adapt to altered setter interface

* reenable i18n for error messages in contracts

* no private methods in representer

* defined model for contracts

* fix validaton

* instantiate correct Object

* define model for contract

* circumvent now existing render method on reform

* replace deprecated constant

* patch correct reform class - not the module - via prepend

* refactor too complex method

* replace deprations

* remove remnants of parentId

* prevent error symbols from existing twice

* adapt user representer to altered setter interface

* adapt watcher representer to altered setter interface

* remove now unnessary patch

* adapt setter to altered interface

* adapt spec

* fix custom field setters

* remove parentId from wp representer

As the parent is a wp resource, clients should use the parent link instead

* adapt spec to changed valid? interface

* remove parentId from wp schema

* replace references of parentId in frontend

* remove TODO

[ci skip]
This commit is contained in:
ulferts
2017-06-02 09:10:51 +02:00
committed by Oliver Günther
parent 634e80bee8
commit 0c0b16508b
74 changed files with 658 additions and 561 deletions
+3 -3
View File
@@ -230,15 +230,15 @@ group :development, :test do
gem 'pry-rescue', '~> 1.4.5'
gem 'pry-byebug', '~> 3.4.2', platforms: [:mri]
gem 'pry-doc', '~> 0.10'
end
# API gems
gem 'grape', '~> 0.19.2'
gem 'grape-cache_control', '~> 1.0.1'
gem 'roar', '~> 1.0.0'
gem 'reform', '~> 1.2.6', require: false
gem 'reform', '~> 2.2.0'
gem 'reform-rails', '~> 0.1.7'
gem 'roar', '~> 1.1.0'
platforms :mri, :mingw, :x64_mingw do
group :mysql2 do
+25 -16
View File
@@ -252,6 +252,10 @@ GEM
activemodel
activesupport
debug_inspector (0.0.2)
declarative (0.0.9)
declarative-builder (0.1.0)
declarative-option (< 0.2.0)
declarative-option (0.1.0)
delayed_job (4.1.2)
activesupport (>= 3.0, < 5.1)
delayed_job_active_record (4.1.1)
@@ -260,9 +264,12 @@ GEM
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
diff-lcs (1.3)
disposable (0.0.9)
representable (~> 2.0)
uber
disposable (0.4.2)
declarative (>= 0.0.9, < 1.0.0)
declarative-builder (< 0.2.0)
declarative-option (< 0.2.0)
representable (>= 2.4.0, <= 3.1.0)
uber (< 0.2.0)
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
@@ -466,15 +473,16 @@ GEM
ffi (>= 0.5.0)
rdoc (5.1.0)
redcarpet (3.3.4)
reform (1.2.6)
activemodel
disposable (~> 0.0.5)
representable (~> 2.1.0)
uber (~> 0.0.11)
representable (2.1.8)
multi_json
nokogiri
uber (~> 0.0.7)
reform (2.2.4)
disposable (>= 0.4.1)
representable (>= 2.4.0, < 3.1.0)
reform-rails (0.1.7)
activemodel (>= 3.2)
reform (>= 2.2.0)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
request_store (1.3.2)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
@@ -484,8 +492,8 @@ GEM
mime-types (>= 1.16, < 3.0)
netrc (~> 0.7)
retriable (2.1.0)
roar (1.0.4)
representable (>= 2.0.1, < 2.4.0)
roar (1.1.0)
representable (~> 3.0.0)
rspec (3.5.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
@@ -692,11 +700,12 @@ DEPENDENCIES
rails_12factor
rails_autolink (~> 1.1.6)
rdoc (>= 2.4.2)
reform (~> 1.2.6)
reform (~> 2.2.0)
reform-rails (~> 0.1.7)
request_store (~> 1.3.1)
responders (~> 2.4)
retriable (~> 2.1)
roar (~> 1.0.0)
roar (~> 1.1.0)
rspec (~> 3.5.0)
rspec-activemodel-mocks (~> 1.0.3)!
rspec-example_disabler!
+1 -1
View File
@@ -52,7 +52,7 @@
if (this.value === '') {
passwordFields.show();
passwordInputs.removeProp('disabled');
passwordInputs.prop('disabled', false);
} else {
passwordFields.hide();
passwordInputs.prop('disabled', 'disabled');
+19 -3
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -45,6 +46,9 @@ class ModelContract < Reform::Contract
end
writable_attributes.concat attributes.map(&:to_s)
# allow the _id variant as well
writable_attributes.concat(attributes.map { |a| "#{a}_id" })
if block
attribute_validations << block
end
@@ -63,10 +67,10 @@ class ModelContract < Reform::Contract
collect_ancestor_attributes(:writable_attributes)
end
validate :readonly_attributes_unchanged
validate :run_attribute_validations
def validate
readonly_attributes_unchanged
run_attribute_validations
super
model.valid?
@@ -79,6 +83,18 @@ class ModelContract < Reform::Contract
errors.empty?
end
# Methods required to get ActiveModel error messages working
extend ActiveModel::Naming
def self.model_name
ActiveModel::Name.new(model, nil)
end
def self.model
raise NotImplementedError
end
# end Methods required to get ActiveModel error messages working
private
def readonly_attributes_unchanged
+12 -3
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -45,10 +46,11 @@ module Queries
attribute :sort_criteria # => sortBy
attribute :group_by # => groupBy
attr_reader :user
def self.model
Query
end
validate :validate_project
validate :user_allowed_to_make_public
attr_reader :user
def initialize(query, user)
super query
@@ -56,6 +58,13 @@ module Queries
@user = user
end
def validate
validate_project
user_allowed_to_make_public
super
end
def validate_project
errors.add :project, :error_not_found if project_id.present? && !project_visible?
end
+17 -33
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -36,14 +37,25 @@ module Relations
attribute :delay
attribute :description
attribute :from_id
attribute :to_id
attribute :from
attribute :to
validate :user_allowed_to_access
validate :user_allowed_to_manage_relations
validate :from do
errors.add :from, :error_not_found unless visible_work_packages.exists? model.from_id
end
validate :to do
errors.add :to, :error_not_found unless visible_work_packages.exists? model.to_id
end
validate :manage_relations_permission?
attr_reader :user
def self.model
Relation
end
def initialize(relation, user)
super relation
@@ -52,35 +64,7 @@ module Relations
private
def fields
override_delay! super
end
##
# We have to redefine `#delay` in this `Reform::Contract::Fields` instance
# because it's conflicting with delayed_job's `#delay`. Without this a call
# to `fields.delay.nil?` will actually enqueue the call to `#nil?` as a delayed job
# as opposed to just checking the field for nil.
#
# This is the best I could come up with. Feel free to solve this better if you know how!
def override_delay!(fields)
@delay_overriden ||= begin
def fields.delay
@table[:delay]
end
end
fields
end
##
# Allow the user only to create/update relations between work packages they are allowed to see.
def user_allowed_to_access
errors.add :from, :error_not_found unless visible_work_packages.exists? model.from_id
errors.add :to, :error_not_found unless visible_work_packages.exists? model.to_id
end
def user_allowed_to_manage_relations
def manage_relations_permission?
if !manage_relations?
errors.add :base, :error_unauthorized
end
+6 -1
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -31,7 +32,11 @@ require 'relations/base_contract'
module Relations
class UpdateContract < BaseContract
validate :links_immutable
def validate
links_immutable
super
end
private
+10 -1
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -43,7 +44,9 @@ module Users
attribute :identity_url
attribute :password
validate :existing_auth_source
def self.model
User
end
def initialize(user, current_user)
super(user)
@@ -51,6 +54,12 @@ module Users
@current_user = current_user
end
def validate
existing_auth_source
super
end
private
attr_reader :current_user
+8 -3
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -31,9 +32,6 @@ require 'users/base_contract'
module Users
class CreateContract < BaseContract
validate :user_allowed_to_add
validate :authentication_defined
attribute :status do
unless model.active? || model.invited?
# New users may only have these two statuses
@@ -41,6 +39,13 @@ module Users
end
end
def validate
user_allowed_to_add
authentication_defined
super
end
private
def authentication_defined
+6 -1
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -31,7 +32,11 @@ require 'users/base_contract'
module Users
class UpdateContract < BaseContract
validate :user_allowed_to_update
def validate
user_allowed_to_update
super
end
private
@@ -32,6 +32,10 @@ require 'model_contract'
module WorkPackages
class BaseContract < ::ModelContract
def self.model
WorkPackage
end
attribute :subject
attribute :description
attribute :start_date, :due_date
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -35,7 +36,11 @@ module WorkPackages
errors.add :author_id, :invalid if model.author != user
end
validate :user_allowed_to_add
def validate
user_allowed_to_add
super
end
private
@@ -29,6 +29,10 @@
module WorkPackages
class CreateNoteContract < ::ModelContract
def self.model
WorkPackage
end
attr_accessor :policy,
:user
+1 -1
View File
@@ -488,7 +488,7 @@ class Project < ActiveRecord::Base
def types_used_by_work_packages
::Type.where(id: WorkPackage.where(project_id: project.id)
.select(:type_id)
.uniq)
.distinct)
end
# Returns an array of the types used by the project and its active sub projects
@@ -51,7 +51,7 @@ class Queries::WorkPackages::Filter::AssignedToFilter <
end
def human_name
WorkPackage.human_attribute_name('assigned_to_id')
WorkPackage.human_attribute_name('assigned_to')
end
def self.key
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -45,7 +46,7 @@ class Queries::WorkPackages::Filter::VersionFilter <
end
def human_name
WorkPackage.human_attribute_name('fixed_version_id')
WorkPackage.human_attribute_name('fixed_version')
end
def self.key
+46 -22
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -63,30 +64,54 @@ module Type::Attributes
OpenProject::Cache.fetch('all_work_package_form_attributes',
*WorkPackageCustomField.pluck('max(updated_at), count(id)').flatten,
merge_date) do
rattrs = API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter.representable_attrs
definitions = rattrs[:definitions]
skip = ['_type', '_dependencies', 'attribute_groups', 'links', 'parent_id', 'parent', 'description']
attributes = definitions.keys
.reject { |key| skip.include?(key) || definitions[key][:required] }
.map { |key| [key, JSON::parse(definitions[key].to_json)] }.to_h
calculate_all_work_package_form_attributes(merge_date)
end
end
# within the form date is shown as a single entry including start and due
if merge_date
attributes['date'] = { required: false, has_default: false }
attributes.delete 'due_date'
attributes.delete 'start_date'
end
private
WorkPackageCustomField.includes(:custom_options).all.each do |field|
attributes["custom_field_#{field.id}"] = {
required: field.is_required,
has_default: field.default_value.present?,
is_cf: true,
display_name: field.name
}
end
def calculate_all_work_package_form_attributes(merge_date)
attributes = calculate_default_work_package_form_attributes
attributes
# within the form date is shown as a single entry including start and due
if merge_date
merge_date_for_form_attributes(attributes)
end
add_custom_fields_to_form_attributes(attributes)
attributes
end
def calculate_default_work_package_form_attributes
representable_config = API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter
.representable_attrs
# For reasons beyond me, Representable::Config contains the definitions
# * nested in [:definitions] in some envs, e.g. development
# * directly in other envs, e.g. test
definitions = representable_config.key?(:definitions) ? representable_config[:definitions] : representable_config
skip = ['_type', '_dependencies', 'attribute_groups', 'links', 'parent_id', 'parent', 'description']
definitions.keys
.reject { |key| skip.include?(key) || definitions[key][:required] }
.map { |key| [key, JSON::parse(definitions[key].to_json)] }.to_h
end
def merge_date_for_form_attributes(attributes)
attributes['date'] = { required: false, has_default: false }
attributes.delete 'due_date'
attributes.delete 'start_date'
end
def add_custom_fields_to_form_attributes(attributes)
WorkPackageCustomField.includes(:custom_options).all.each do |field|
attributes["custom_field_#{field.id}"] = {
required: field.is_required,
has_default: field.default_value.present?,
is_cf: true,
display_name: field.name
}
end
end
end
@@ -146,7 +171,6 @@ module Type::Attributes
# If a project context is given, that context is passed
# to the constraint validator.
def passes_attribute_constraint?(attribute, project: nil)
# Check custom field constraints
if custom_field?(attribute) && !project.nil?
return custom_field_in_project?(attribute, project)
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
+5 -1
View File
@@ -68,7 +68,11 @@ See doc/COPYRIGHT.rdoc for more details.
</div>
<% else %>
<% unless @auth_sources.empty? || OpenProject::Configuration.disable_password_login? %>
<div class="form--field"><%= f.select :auth_source_id, ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }) %></div>
<div class="form--field">
<%= f.select :auth_source_id,
([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }),
label: :'activerecord.attributes.user.auth_source' %>
</div>
<% end %>
<% if !OpenProject::Configuration.disable_password_login? %>
<%
+1 -1
View File
@@ -45,7 +45,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% unless @auth_sources.empty? || OpenProject::Configuration.disable_password_login? %>
<div class="form--field">
<% sources = ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }) %>
<%= f.select :auth_source_id, sources %>
<%= f.select :auth_source_id, sources, label: :'activerecord.attributes.user.auth_source' %>
</div>
<div class="form--field" id="new_user_login" style="display: none;">
+2 -47
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -33,19 +34,9 @@ module ActiveRecord
class Base
include Redmine::I18n
# Translate attribute names for validation errors display
def self.human_attribute_name(attr, options = {})
options_with_raise = { raise: true, default: false }.merge options
attr = attr.to_s.gsub(/_id\z/, '')
super(attr, options_with_raise)
rescue I18n::MissingTranslationData => e
included_in_general_attributes = I18n.t('attributes').keys.map(&:to_s).include? attr
included_in_superclasses = ancestors.select { |a| a.ancestors.include? ActiveRecord::Base }.any? { |klass| !(I18n.t("activerecord.attributes.#{klass.name.underscore}.#{attr}").include? 'translation missing:') }
unless included_in_general_attributes or included_in_superclasses
# TODO: remove this method once no warning is displayed when running a server/console/tests/tasks etc.
warn "[DEPRECATION] Relying on Redmine::I18n addition of `field_` to your translation key \"#{attr}\" on the \"#{self}\" model is deprecated. Please use proper ActiveRecord i18n! \n Caught: #{e.message}"
end
super(attr, options)
super
end
end
end
@@ -134,24 +125,6 @@ module ActiveModel
end
end
require 'reform/contract'
class Reform::Contract::Errors
def merge_with_storing_error_symbols!(errors, prefix)
@store_new_symbols = false
merge_without_storing_error_symbols!(errors, prefix)
@store_new_symbols = true
errors.keys.each do |attribute|
errors.symbols_and_messages_for(attribute).each do |symbol, full_message, partial_message|
writable_symbols_and_messages_for(attribute) << [symbol, full_message, partial_message]
end
end
end
alias_method_chain :merge!, :storing_error_symbols
end
module ActionView
module Helpers
module Tags
@@ -267,23 +240,5 @@ ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
end
end
module ActiveRecord
class Errors
# def on_with_id_handling(attribute)
# attribute = attribute.to_s
# if attribute.ends_with? '_id'
# on_without_id_handling(attribute) || on_without_id_handling(attribute[0..-4])
# else
# on_without_id_handling(attribute)
# end
# end
# alias_method_chain :on, :id_handling
end
end
# Patch acts_as_list before any class includes the module
require 'open_project/patches/acts_as_list'
# Backports some useful ruby 2.3 methods for Hash
require 'open_project/patches/hash'
+1 -1
View File
@@ -33,7 +33,7 @@
# Mime::Type.register "text/richtext", :rtf
# Mime::Type.register_alias "text/html", :iphone
Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV)
Mime::SET << Mime[:csv] unless Mime::SET.include?(Mime[:csv])
Mime::Type.register 'application/pdf', :pdf unless Mime::Type.lookup_by_extension(:pdf)
Mime::Type.register 'image/png', :png unless Mime::Type.lookup_by_extension(:png)
@@ -1,3 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -26,24 +28,20 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject
module Patches
module Hash
##
# Becomes obsolete with ruby 2.3's Hash#dig but until then this will do.
def dig(*keys)
keys.inject(self) { |hash, key| hash && (hash.is_a?(Hash) || nil) && hash[key] }
end
require "reform/form/active_model/validations"
def map_values(&_block)
entries = map { |key, value| [key, (yield value)] }
::Hash[entries]
end
end
end
Reform::Form.class_eval do
include Reform::Form::ActiveModel::Validations
end
if !Hash.instance_methods.include? :dig
Hash.prepend OpenProject::Patches::Hash
Reform::Contract.class_eval do
include Reform::Form::ActiveModel::Validations
end
require 'reform/contract'
require 'open_project/patches/reform'
class Reform::Form::ActiveModel::Errors
prepend OpenProject::Patches::Reform
end
+3 -3
View File
@@ -253,13 +253,13 @@ en:
activemodel:
errors:
models:
"queries/base_contract":
query:
attributes:
project:
error_not_found: "not found"
public:
error_unauthorized: "- The user has no permission to create public queries."
"relations/base_contract":
relation:
attributes:
to:
error_not_found: "work package in `to` position not found or not visible"
@@ -267,7 +267,7 @@ en:
from:
error_not_found: "work package in `from` position not found or not visible"
error_readonly: "an existing relation's `from` link is immutable"
"users/base_contract":
user:
attributes:
status:
invalid_on_create: "is not a valid status for new users."
+22 -22
View File
@@ -17,27 +17,28 @@
## Linked Properties
| Link | Description | Type | Constraints | Supported operations | Condition |
| :----------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------ | --------------------- | ----------------------------------------- |
| self | This work package | WorkPackage | not null | READ | |
| schema | The schema of this work package | Schema | not null | READ | |
| ancestors | Array of all visible ancestors of the work package, with the root node being the first element | Collection | not null | READ | **Permission** view work packages |
| attachments | The files attached to this work package | Collection | not null | READ | |
| author | The person that created the work package | User | not null | READ | |
| assignee | The person that is intended to work on the work package | User | | READ / WRITE | |
| availableWatchers | All users that can be added to the work package as watchers. | User | | READ | **Permission** add work package watchers |
| category | The category of the work package | Category | | READ / WRITE | |
| children | Array of all visible children of the work package | Collection | not null | READ | **Permission** view work packages |
| priority | The priority of the work package | Priority | not null | READ / WRITE | |
| project | The project to which the work package belongs | Project | not null | READ / WRITE | |
| responsible | The person that is responsible for the overall outcome | User | | READ / WRITE | |
| relations | Relations this work package is involved in | Relation | | READ | **Permission** view work packages |
| revisions | Revisions that are referencing the work package | Revision | | READ | **Permission** view changesets |
| status | The current status of the work package | Status | not null | READ / WRITE | |
| timeEntries | All time entries logged on the work package. Please note that this is a link to an HTML resource for now and as such, the link is subject to change. | N/A | | READ | **Permission** view time entries |
| type | The type of the work package | Type | not null | READ / WRITE | |
| version | The version associated to the work package | Version | | READ / WRITE | |
| watchers | All users that are currently watching this work package | Collection | | READ | **Permission** view work package watchers |
| Link | Description | Type | Constraints | Supported operations | Condition |
| :----------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------ | --------------------- | ----------------------------------------- |
| self | This work package | WorkPackage | not null | READ | |
| schema | The schema of this work package | Schema | not null | READ | |
| ancestors | Array of all visible ancestors of the work package, with the root node being the first element | Collection | not null | READ | **Permission** view work packages |
| attachments | The files attached to this work package | Collection | not null | READ | |
| author | The person that created the work package | User | not null | READ | |
| assignee | The person that is intended to work on the work package | User | | READ / WRITE | |
| availableWatchers | All users that can be added to the work package as watchers. | User | | READ | **Permission** add work package watchers |
| category | The category of the work package | Category | | READ / WRITE | |
| children | Array of all visible children of the work package | Collection | not null | READ | **Permission** view work packages |
| parent | Parent work package | WorkPackage | Needs to be visible (to the current user) | READ / WRITE | |
| priority | The priority of the work package | Priority | not null | READ / WRITE | |
| project | The project to which the work package belongs | Project | not null | READ / WRITE | |
| responsible | The person that is responsible for the overall outcome | User | | READ / WRITE | |
| relations | Relations this work package is involved in | Relation | | READ | **Permission** view work packages |
| revisions | Revisions that are referencing the work package | Revision | | READ | **Permission** view changesets |
| status | The current status of the work package | Status | not null | READ / WRITE | |
| timeEntries | All time entries logged on the work package. Please note that this is a link to an HTML resource for now and as such, the link is subject to change. | N/A | | READ | **Permission** view time entries |
| type | The type of the work package | Type | not null | READ / WRITE | |
| version | The version associated to the work package | Version | | READ / WRITE | |
| watchers | All users that are currently watching this work package | Collection | | READ | **Permission** view work package watchers |
## Local Properties
@@ -48,7 +49,6 @@
| subject | Work package subject | String | not null; 1 <= length <= 255 | READ / WRITE | |
| type | Name of the work package's type | String | not null | READ | |
| description | The work package description | Formattable | | READ / WRITE | |
| parentId | Parent work package id | Integer | Must be an id of an existing and visible (for the current user) work package | READ / WRITE | |
| startDate | Scheduled beginning of a work package | Date | Cannot be set for parent work packages; must be equal or greater than the earliest possible start date; Exists only on work packages of a non milestone type | READ / WRITE | |
| dueDate | Scheduled end of a work package | Date | Cannot be set for parent work packages; must be greater than or equal to the start date; Exists only on work packages of a non milestone type | READ / WRITE | |
| date | Date on which a milestone is achieved | Date | Exists only on work packages of a milestone type
@@ -103,6 +103,7 @@ var schemaCacheService: SchemaCacheService;
var NotificationsService: any;
var wpNotificationsService: any;
var AttachmentCollectionResource:any;
var v3Path:any;
export class WorkPackageResource extends HalResource {
// Add index signature for getter this[attr]
@@ -134,7 +135,6 @@ export class WorkPackageResource extends HalResource {
public $embedded: WorkPackageResourceEmbedded;
public $links: WorkPackageLinksObject;
public $pristine: { [attribute: string]: any } = {};
public parentId: number;
public subject: string;
public updatedAt: Date;
public lockVersion: number;
@@ -585,7 +585,15 @@ export class WorkPackageResource extends HalResource {
return apiWorkPackages.createWorkPackage(payload);
};
this.parentId = this.parentId || $stateParams.parent_id;
if (this.parent) {
this.$source._links['parent'] = {
href: this.parent.href
};
} else if ($stateParams.parent_id) {
this.$source._links['parent'] = {
href: v3Path.wp({ wp: $stateParams.parent_id })
};
}
}
/**
@@ -636,7 +644,8 @@ function wpResource(...args:any[]) {
schemaCacheService,
NotificationsService,
wpNotificationsService,
AttachmentCollectionResource] = args;
AttachmentCollectionResource,
v3Path] = args;
return WorkPackageResource;
}
@@ -651,7 +660,8 @@ wpResource.$inject = [
'schemaCacheService',
'NotificationsService',
'wpNotificationsService',
'AttachmentCollectionResource'
'AttachmentCollectionResource',
'v3Path'
];
opApiModule.factory('WorkPackageResource', wpResource);
@@ -81,22 +81,22 @@ export class HierarchyRenderPass extends TableRenderPass {
* @returns {boolean}
*/
public deferInsertion(workPackage:WorkPackageResourceInterface):boolean {
const parentId = workPackage.parentId;
const parent = workPackage.parent;
// Will only defer if parent exists
if (!parentId) {
if (!parent) {
return false;
}
// Will only defer is parent is
// 1. existent in the table results
// 1. yet to be rendered
if (this.workPackageTable.rowIndex[parentId] === undefined || this.rendered[parentId]) {
if (this.workPackageTable.rowIndex[parent.id] === undefined || this.rendered[parent.id]) {
return false;
}
const elements = this.deferred[parentId] || [];
this.deferred[parentId] = elements.concat([workPackage]);
const elements = this.deferred[parent.id] || [];
this.deferred[parent.id] = elements.concat([workPackage]);
return true;
}
@@ -115,7 +115,7 @@ export class HierarchyRenderPass extends TableRenderPass {
deferredChildren.forEach((child:WorkPackageResourceInterface) => {
// Callback on the child itself
const row:WorkPackageTableRow = this.workPackageTable.rowIndex[child.id];
this.insertUnderParent(row, child.parentId.toString());
this.insertUnderParent(row, child.parent);
// Descend into any children the child WP might have and callback
this.renderAllDeferredChildren(child);
@@ -145,7 +145,7 @@ export class HierarchyRenderPass extends TableRenderPass {
} else {
// This ancestor must be inserted in the last position of its root
const parent = ancestors[index - 1];
this.insertAtExistingHierarchy(ancestor, ancestorRow, parent.id, hidden, true);
this.insertAtExistingHierarchy(ancestor, ancestorRow, parent, hidden, true);
}
// Remember we just added this extra ancestor row
@@ -161,7 +161,7 @@ export class HierarchyRenderPass extends TableRenderPass {
// Insert this row to parent
const parent = _.last(ancestors);
this.insertUnderParent(row, parent.id);
this.insertUnderParent(row, parent);
}
/**
@@ -169,10 +169,10 @@ export class HierarchyRenderPass extends TableRenderPass {
* @param row
* @param parentId
*/
private insertUnderParent(row:WorkPackageTableRow, parentId:string) {
private insertUnderParent(row:WorkPackageTableRow, parent:WorkPackageResourceInterface) {
const [tr, hidden] = this.rowBuilder.buildEmpty(row.object);
row.element = tr;
this.insertAtExistingHierarchy(row.object, tr, parentId, hidden, false);
this.insertAtExistingHierarchy(row.object, tr, parent, hidden, false);
}
/**
@@ -210,11 +210,15 @@ export class HierarchyRenderPass extends TableRenderPass {
/**
* Append a row to the given parent hierarchy group.
*/
private insertAtExistingHierarchy(workPackage:WorkPackageResourceInterface, el:HTMLElement, parentId:string, hidden:boolean, isAncestor:boolean) {
private insertAtExistingHierarchy(workPackage:WorkPackageResourceInterface,
el:HTMLElement,
parent:WorkPackageResourceInterface,
hidden:boolean,
isAncestor:boolean) {
// Either append to the hierarchy group root (= the parentID row itself)
const hierarchyRoot = `.__hierarchy-root-${parentId}`;
const hierarchyRoot = `.__hierarchy-root-${parent.id}`;
// Or, if it has descendants, append to the LATEST of that set
const hierarchyGroup = `.__hierarchy-group-${parentId}`;
const hierarchyGroup = `.__hierarchy-group-${parent.id}`;
// Insert into table
const target = jQuery(this.tableBody).find(`${hierarchyRoot},${hierarchyGroup}`).last();
@@ -32,24 +32,24 @@ import {WorkPackageResourceInterface} from "../../api/api-v3/hal-resources/work-
import {WorkPackageCacheService} from "../../work-packages/work-package-cache.service";
export class WorkPackageRelationsHierarchyController {
public workPackage: WorkPackageResourceInterface;
public showEditForm: boolean = false;
public workPackage:WorkPackageResourceInterface;
public showEditForm:boolean = false;
public workPackagePath = this.PathHelper.workPackagePath;
public canHaveChildren = !this.workPackage.isMilestone;
public canModifyHierarchy = !!this.workPackage.changeParent;
public canAddRelation = !!this.workPackage.addRelation;
constructor(protected $scope: ng.IScope,
protected $rootScope: ng.IRootScopeService,
protected $q: ng.IQService,
protected wpCacheService: WorkPackageCacheService,
protected PathHelper: op.PathHelper,
protected I18n: op.I18n) {
constructor(protected $scope:ng.IScope,
protected $rootScope:ng.IRootScopeService,
protected $q:ng.IQService,
protected wpCacheService:WorkPackageCacheService,
protected PathHelper:op.PathHelper,
protected I18n:op.I18n) {
scopedObservable(
this.$scope,
this.wpCacheService.loadWorkPackage(this.workPackage.id).values$())
.subscribe((wp: WorkPackageResourceInterface) => {
.subscribe((wp:WorkPackageResourceInterface) => {
this.workPackage = wp;
this.loadParent();
this.loadChildren();
@@ -67,15 +67,15 @@ export class WorkPackageRelationsHierarchyController {
}
protected loadParent() {
if (!angular.isNumber(this.workPackage.parentId)) {
if (!this.workPackage.parent) {
return;
}
scopedObservable(
this.$scope,
this.wpCacheService.loadWorkPackage(this.workPackage.parentId.toString()).values$())
this.wpCacheService.loadWorkPackage(this.workPackage.parent.id).values$())
.take(1)
.subscribe((parent: WorkPackageResourceInterface) => {
.subscribe((parent:WorkPackageResourceInterface) => {
this.workPackage.parent = parent;
});
}
@@ -40,17 +40,33 @@ export class WorkPackageRelationsHierarchyService {
protected wpTableRefresh: WorkPackageTableRefreshService,
protected $rootScope: ng.IRootScopeService,
protected wpNotificationsService: WorkPackageNotificationService,
protected wpCacheService: WorkPackageCacheService) {
protected wpCacheService: WorkPackageCacheService,
protected v3Path:any) {
}
public changeParent(workPackage: WorkPackageResourceInterface, parentId: string | null) {
public changeParent(workPackage:WorkPackageResourceInterface, parentId:string | null) {
let payload:any = {
lockVersion: workPackage.lockVersion
};
if (parentId) {
payload['_links'] = {
parent: {
href: this.v3Path.wp({wp: parentId})
}
};
} else {
payload['_links'] = {
parent: {
href: null
}
};
}
return workPackage
.changeParent({
parentId: parentId,
lockVersion: workPackage.lockVersion
})
.then((wp: WorkPackageResourceInterface) => {
.changeParent(payload)
.then((wp:WorkPackageResourceInterface) => {
this.wpCacheService.updateWorkPackage(wp);
this.wpNotificationsService.showSave(wp);
this.wpTableRefresh.request(true, `Changed parent of ${workPackage.id} to ${parentId}`);
@@ -96,10 +112,14 @@ export class WorkPackageRelationsHierarchyService {
});
}
public removeChild(childWorkPackage: WorkPackageResourceInterface) {
public removeChild(childWorkPackage:WorkPackageResourceInterface) {
return childWorkPackage.$load().then(() => {
return childWorkPackage.changeParent({
parentId: null,
_links: {
parent: {
href: null
}
},
lockVersion: childWorkPackage.lockVersion
}).then(wp => {
this.wpCacheService.updateWorkPackage(wp);
-2
View File
@@ -88,8 +88,6 @@ module API
false
end
private
attr_reader :sums,
:count,
:query
+6 -7
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -53,20 +54,18 @@ module API
{ href: @self_link }
end
property :total, getter: -> (*) { @total }, exec_context: :decorator
property :count, getter: -> (*) { count }
property :total, getter: ->(*) { @total }, exec_context: :decorator
property :count, getter: ->(*) { count }
collection :elements,
getter: -> (*) {
represented.map { |model|
getter: ->(*) {
represented.map do |model|
element_decorator.create(model, current_user: current_user)
}
end
},
exec_context: :decorator,
embedded: true
private
def _type
'Collection'
end
+30 -25
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -35,7 +36,7 @@ module API
path: :"#{property_name}",
namespace: path.to_s.pluralize,
getter: :"#{property_name}_id",
title_getter: -> (*) { model.send(property_name).name },
title_getter: ->(*) { model.send(property_name).name },
setter: :"#{getter}=")
@property_name = property_name
@path = path
@@ -49,36 +50,40 @@ module API
property :href,
exec_context: :decorator,
getter: -> (*) {
id = represented.send(@getter) if represented
return nil if id.nil? || id == 0
api_v3_paths.send(@path, id)
},
setter: -> (value, *) {
if value
id = ::API::Utilities::ResourceLinkParser.parse_id value,
property: @property_name,
expected_version: '3',
expected_namespace: @namespace
end
represented.send(@setter, id)
},
render_nil: true
property :title,
exec_context: :decorator,
getter: -> (*) {
attribute = ::API::Utilities::PropertyNameConverter.to_ar_name(
@property_name,
context: represented
)
represented.try(attribute).try(:name)
},
writeable: false,
render_nil: false
def href
id = represented.send(@getter) if represented
return nil if id.nil? || id.zero?
api_v3_paths.send(@path, id)
end
def href=(value)
if value
id = ::API::Utilities::ResourceLinkParser.parse_id value,
property: @property_name,
expected_version: '3',
expected_namespace: @namespace
end
represented.send(@setter, id)
end
def title
attribute = ::API::Utilities::PropertyNameConverter.to_ar_name(
@property_name,
context: represented
)
represented.try(attribute).try(:name)
end
end
end
end
-2
View File
@@ -216,8 +216,6 @@ module API
property :_dependencies,
exec_context: :decorator
private
attr_accessor :form_embedded,
:self_link
+3 -5
View File
@@ -111,7 +111,7 @@ module API
::API::Utilities::DecoratorFactory.new(decorator: decorator,
current_user: current_user)
},
if: -> (*) { embed_links && call_or_use(show_if) }
if: ->(*) { embed_links && call_or_use(show_if) }
end
class_attribute :to_eager_load
@@ -121,7 +121,8 @@ module API
current_user.allowed_to?(permission, context)
end
private
# Override in subclasses to specify the JSON indicated "_type" of this representer
def _type; end
def call_or_send_to_represented(callable_or_name)
if callable_or_name.respond_to? :call
@@ -143,9 +144,6 @@ module API
::API::V3::Utilities::DateTimeFormatter
end
# Override in subclasses to specify the JSON indicated "_type" of this representer
def _type; end
# If a subclass does not depend on a model being passed to this class, it can override
# this method and return false. Otherwise it will be enforced that the model of each
# representer is non-nil.
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -40,10 +41,10 @@ module API
property :file_name
property :description,
getter: -> (*) {
getter: ->(*) {
::API::Decorators::Formattable.new(description, format: 'plain')
},
setter: -> (value, *) { self.description = value['raw'] },
setter: ->(fragment:, **) { self.description = fragment['raw'] },
render_nil: true
end
end
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -40,12 +41,10 @@ module API
end
property :maximum_attachment_file_size,
getter: -> (*) { attachment_max_size.to_i.kilobyte }
getter: ->(*) { attachment_max_size.to_i.kilobyte }
property :per_page_options,
getter: -> (*) { per_page_options.split(',').map(&:to_i) }
private
getter: ->(*) { per_page_options.split(',').map(&:to_i) }
def _type
'Configuration'
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -45,8 +46,6 @@ module API
property :caption,
as: :name
private
def converted_name
convert_attribute(represented.name)
end
@@ -76,7 +76,9 @@ module API
exec_context: :decorator,
show_nil: true
private
def _type
"#{converted_name.camelize}QueryFilter"
end
def name
represented.human_name
@@ -97,10 +99,6 @@ module API
end
end
def _type
"#{converted_name.camelize}QueryFilter"
end
def converted_name
convert_attribute(represented.name)
end
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -43,8 +44,6 @@ module API
property :id,
exec_context: :decorator
private
def converted_key
convert_attribute(represented.name)
end
@@ -45,8 +45,6 @@ module API
property :caption,
as: :name
private
def converted_name
convert_attribute(represented.name)
end
@@ -46,8 +46,6 @@ module API
property :name,
exec_context: :decorator
private
def name
represented.human_name
end
+2 -2
View File
@@ -217,12 +217,12 @@ module API
:user,
project: :work_package_custom_fields]
private
def _type
'Query'
end
private
def allowed_to?(action)
@policy ||= QueryPolicy.new(current_user)
@@ -101,8 +101,6 @@ module API
WorkPackage
end
private
alias :filter :represented
def _type
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -196,8 +197,6 @@ module API
Query
end
private
def convert_attribute(attribute)
::API::Utilities::PropertyNameConverter.from_ar_name(attribute)
end
@@ -57,8 +57,6 @@ module API
property :name
private
def self_link_params
[represented.converted_name, represented.direction_name]
end
+1 -1
View File
@@ -66,7 +66,7 @@ module API
service = ::UpdateRelationService.new relation: Relation.find_by_id!(params[:id]),
user: current_user
call = service.call attributes: attributes,
send_notifications: !(params[:notify] == 'false')
send_notifications: (params[:notify] != 'false')
if call.success?
representer.new call.result, current_user: current_user, embed_links: true
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -44,8 +45,6 @@ module API
current_user: current_user)
end
private
attr_accessor :on
alias :dependencies :represented
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -56,8 +57,6 @@ module API
# (nil values are not supported by a string_objects URL anyway)
getter: ->(*) { values.last || '' }
private
def _type
'StringObject'
end
+37 -30
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -54,66 +55,70 @@ module API
end
link :updateImmediately do
next unless current_user_is_admin
{
href: api_v3_paths.user(represented.id),
title: "Update #{represented.login}",
method: :patch
} if current_user_is_admin
}
end
link :lock do
next unless current_user_is_admin && represented.lockable?
{
href: api_v3_paths.user_lock(represented.id),
title: "Set lock on #{represented.login}",
method: :post
} if current_user_is_admin && represented.lockable?
}
end
link :unlock do
next unless current_user_is_admin && represented.activatable?
{
href: api_v3_paths.user_lock(represented.id),
title: "Remove lock on #{represented.login}",
method: :delete
} if current_user_is_admin && represented.activatable?
}
end
link :delete do
next unless current_user_can_delete_represented?
{
href: api_v3_paths.user(represented.id),
title: "Delete #{represented.login}",
method: :delete
} if current_user_can_delete_represented?
}
end
property :id,
render_nil: true
property :login,
exec_context: :decorator,
render_nil: false,
getter: ->(*) { represented.login },
setter: ->(value, *) { represented.login = value },
exec_context: :decorator,
setter: ->(fragment:, represented:, **) { represented.login = fragment },
if: ->(*) { current_user_is_admin_or_self }
property :admin,
render_nil: false,
exec_context: :decorator,
render_nil: false,
getter: ->(*) {
represented.admin?
},
setter: ->(value, *) { represented.admin = value },
setter: ->(fragment:, represented:, **) { represented.admin = fragment },
if: ->(*) { current_user_is_admin }
property :subtype,
getter: -> (*) { type },
getter: ->(*) { type },
render_nil: true
property :firstName,
getter: ->(*) { represented.firstname },
setter: ->(value, *) { represented.firstname = value },
exec_context: :decorator,
getter: ->(*) { represented.firstname },
setter: ->(fragment:, represented:, **) { represented.firstname = fragment },
render_nil: false,
if: ->(*) { current_user_is_admin_or_self }
property :lastName,
getter: ->(*) { represented.lastname },
setter: ->(value, *) { represented.lastname = value },
exec_context: :decorator,
getter: ->(*) { represented.lastname },
setter: ->(fragment:, represented:, **) { represented.lastname = fragment },
render_nil: false,
if: ->(*) { current_user_is_admin_or_self }
property :name,
@@ -128,48 +133,50 @@ module API
end
}
property :avatar,
getter: -> (*) { avatar_url(represented) },
render_nil: true,
exec_context: :decorator
property :created_on,
as: 'createdAt',
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.created_on) },
getter: ->(*) { avatar_url(represented) },
render_nil: true
property :created_on,
exec_context: :decorator,
as: 'createdAt',
getter: ->(*) { datetime_formatter.format_datetime(represented.created_on) },
render_nil: false,
if: ->(*) { current_user_is_admin_or_self }
property :updated_on,
as: 'updatedAt',
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.updated_on) },
as: 'updatedAt',
getter: ->(*) { datetime_formatter.format_datetime(represented.updated_on) },
render_nil: false,
if: ->(*) { current_user_is_admin_or_self }
property :status,
getter: -> (*) { status_name },
setter: -> (value, *) { self.status = User::STATUSES[value.to_sym] },
getter: ->(*) { status_name },
setter: ->(fragment:, represented:, **) { represented.status = User::STATUSES[fragment.to_sym] },
render_nil: true
link :auth_source do
next unless represented.is_a?(User) && represented.auth_source && current_user.admin?
{
href: "/api/v3/auth_sources/#{represented.auth_source_id}",
title: represented.auth_source.name
} if represented.is_a?(User) && represented.auth_source && current_user.admin?
}
end
property :identity_url,
as: 'identityUrl',
exec_context: :decorator,
getter: -> (*) { represented.identity_url },
setter: -> (value, *) { represented.identity_url = value },
as: 'identityUrl',
getter: ->(*) { represented.identity_url },
setter: ->(fragment:, represented:, **) { represented.identity_url = fragment },
render_nil: true,
if: ->(*) { represented.is_a?(User) && current_user_is_admin_or_self }
# Write-only properties
property :password,
getter: -> (*) { nil },
getter: ->(*) { nil },
render_nil: false,
setter: -> (value, *) {
self.password = self.password_confirmation = value
setter: ->(fragment:, represented:, **) {
represented.password = represented.password_confirmation = fragment
}
##
+10 -5
View File
@@ -271,8 +271,8 @@ module API
end
def link_value_setter_for(custom_field, property, expected_namespace)
->(link_object, *) {
values = Array([link_object].flatten).flat_map do |link|
->(fragment:, represented:, **) {
values = Array([fragment].flatten).flat_map do |link|
href = link['href']
value =
if href
@@ -317,7 +317,8 @@ module API
end
def inject_property_value(custom_field)
@class.property property_name(custom_field.id),
@class.property "custom_field_#{custom_field.id}".to_sym,
as: property_name(custom_field.id),
getter: property_value_getter_for(custom_field),
setter: property_value_setter_for(custom_field),
render_nil: true
@@ -336,8 +337,12 @@ module API
end
def property_value_setter_for(custom_field)
->(value, *) {
value = value['raw'] if custom_field.field_format == 'text'
->(fragment:, **) {
value = if custom_field.field_format == 'text'
fragment['raw']
else
fragment
end
self.custom_field_values = { custom_field.id => value }
}
end
+3 -3
View File
@@ -40,12 +40,12 @@ module API
property :user,
exec_context: :decorator,
getter: -> (*) {
getter: ->(*) {
create_link_representer
},
setter: -> (value, *) {
setter: ->(fragment:, **) {
link = create_link_representer
link.from_hash(value)
link.from_hash(fragment)
}
private
@@ -41,7 +41,6 @@ module API
work_package = write_work_package_attributes(work_package, request_body || {})
result = create_work_package(current_user,
work_package,
notify_according_to_params)
@@ -122,8 +122,8 @@ module API
schema :lock_version,
type: 'Integer',
name_source: -> (*) { I18n.t('api_v3.attributes.lock_version') },
show_if: -> (*) { @show_lock_version }
name_source: ->(*) { I18n.t('api_v3.attributes.lock_version') },
show_if: ->(*) { @show_lock_version }
schema :id,
type: 'Integer'
@@ -140,17 +140,17 @@ module API
schema :start_date,
type: 'Date',
required: false,
show_if: -> (*) { !represented.milestone? }
show_if: ->(*) { !represented.milestone? }
schema :due_date,
type: 'Date',
required: false,
show_if: -> (*) { !represented.milestone? }
show_if: ->(*) { !represented.milestone? }
schema :date,
type: 'Date',
required: false,
show_if: -> (*) { represented.milestone? }
show_if: ->(*) { represented.milestone? }
schema :estimated_time,
type: 'Duration',
@@ -163,7 +163,7 @@ module API
schema :percentage_done,
type: 'Integer',
name_source: :done_ratio,
show_if: -> (*) { Setting.work_package_done_ratio != 'disabled' },
show_if: ->(*) { Setting.work_package_done_ratio != 'disabled' },
required: false
schema :created_at,
@@ -178,7 +178,7 @@ module API
schema_with_allowed_link :project,
type: 'Project',
required: true,
href_callback: -> (*) {
href_callback: ->(*) {
if @action == :create
api_v3_paths.available_projects_on_create
else
@@ -186,13 +186,7 @@ module API
end
}
schema :parent_id,
type: 'Integer',
required: false,
writable: true
# TODO:
# * remove parent_id above in favor of only having :parent
# * create an available_work_package_parent resource
# * turn :parent into a schema_with_allowed_link
@@ -204,7 +198,7 @@ module API
schema_with_allowed_link :assignee,
type: 'User',
required: false,
href_callback: -> (*) {
href_callback: ->(*) {
if represented.project
api_v3_paths.available_assignees(represented.project_id)
end
@@ -213,7 +207,7 @@ module API
schema_with_allowed_link :responsible,
type: 'User',
required: false,
href_callback: -> (*) {
href_callback: ->(*) {
if represented.project
api_v3_paths.available_responsibles(represented.project_id)
end
@@ -221,7 +215,7 @@ module API
schema_with_allowed_collection :type,
value_representer: Types::TypeRepresenter,
link_factory: -> (type) {
link_factory: ->(type) {
{
href: api_v3_paths.type(type.id),
title: type.name
@@ -231,7 +225,7 @@ module API
schema_with_allowed_collection :status,
value_representer: Statuses::StatusRepresenter,
link_factory: -> (status) {
link_factory: ->(status) {
{
href: api_v3_paths.status(status.id),
title: status.name
@@ -241,7 +235,7 @@ module API
schema_with_allowed_collection :category,
value_representer: Categories::CategoryRepresenter,
link_factory: -> (category) {
link_factory: ->(category) {
{
href: api_v3_paths.category(category.id),
title: category.name
@@ -251,7 +245,7 @@ module API
schema_with_allowed_collection :version,
value_representer: Versions::VersionRepresenter,
link_factory: -> (version) {
link_factory: ->(version) {
{
href: api_v3_paths.version(version.id),
title: version.name
@@ -261,7 +255,7 @@ module API
schema_with_allowed_collection :priority,
value_representer: Priorities::PriorityRepresenter,
link_factory: -> (priority) {
link_factory: ->(priority) {
{
href: api_v3_paths.priority(priority.id),
title: priority.name
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -41,9 +42,9 @@ module API
class << self
def create_class(work_package)
injector_class = ::API::V3::Utilities::CustomFieldInjector
injector_class.create_value_representer_for_link_patching(
work_package,
WorkPackageAttributeLinksRepresenter)
injector_class
.create_value_representer_for_link_patching(work_package,
WorkPackageAttributeLinksRepresenter)
end
def create(work_package)
@@ -60,21 +61,21 @@ module API
show_if: true)
property property,
exec_context: :decorator,
getter: -> (*) {
getter: ->(represented:, **) {
::API::Decorators::LinkObject.new(represented,
property_name: property,
path: path,
namespace: namespace,
getter: association)
},
setter: -> (value, *) {
setter: ->(fragment:, represented:, **) {
link = ::API::Decorators::LinkObject.new(represented,
property_name: property,
path: path,
namespace: namespace,
getter: association)
link.from_hash(value)
link.from_hash(fragment)
},
if: show_if
end
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -147,15 +148,13 @@ module API
property :total_sums,
exec_context: :decorator,
getter: -> (*) {
getter: ->(*) {
if total_sums
::API::V3::WorkPackages::WorkPackageSumsRepresenter.create(total_sums)
end
},
render_nil: false
private
def current_user_allowed_to_add_work_packages?
current_user.allowed_to?(:add_work_packages, project, global: project.nil?)
end
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -40,9 +41,9 @@ module API
class << self
def create_class(work_package)
injector_class = ::API::V3::Utilities::CustomFieldInjector
injector_class.create_value_representer_for_property_patching(
work_package,
WorkPackagePayloadRepresenter)
injector_class
.create_value_representer_for_property_patching(work_package,
WorkPackagePayloadRepresenter)
end
def create(work_package)
@@ -58,101 +59,111 @@ module API
property :linked_resources,
as: :_links,
exec_context: :decorator,
getter: -> (*) {
work_package_attribute_links_representer represented
},
setter: -> (value, *) {
representer = work_package_attribute_links_representer represented
representer.from_json(value.to_json)
}
exec_context: :decorator
property :lock_version,
render_nil: true,
getter: -> (*) {
getter: ->(*) {
lock_version.to_i
}
property :subject, render_nil: true
property :subject,
render_nil: true
property :done_ratio,
as: :percentageDone,
render_nil: true,
if: -> (*) { Setting.work_package_done_ratio == 'field' }
if: ->(*) { Setting.work_package_done_ratio == 'field' }
property :estimated_hours,
as: :estimatedTime,
exec_context: :decorator,
getter: -> (*) {
datetime_formatter.format_duration_from_hours(represented.estimated_hours,
allow_nil: true)
},
setter: -> (value, *) {
represented.estimated_hours = datetime_formatter.parse_duration_to_hours(
value,
'estimated_hours',
allow_nil: true)
},
render_nil: true
property :description,
exec_context: :decorator,
getter: -> (*) {
API::Decorators::Formattable.new(represented.description, object: represented)
},
setter: -> (value, *) { represented.description = value['raw'] },
render_nil: true
property :parent_id,
writeable: true,
render_nil: true
property :start_date,
exec_context: :decorator,
getter: -> (*) {
datetime_formatter.format_date(represented.start_date, allow_nil: true)
},
setter: -> (value, *) {
represented.start_date = datetime_formatter.parse_date(value,
'startDate',
allow_nil: true)
},
render_nil: true,
if: -> (*) { !represented.milestone? }
if: ->(*) { !represented.milestone? }
property :due_date,
exec_context: :decorator,
getter: -> (*) {
datetime_formatter.format_date(represented.due_date, allow_nil: true)
},
setter: -> (value, *) {
represented.due_date = datetime_formatter.parse_date(value,
'dueDate',
allow_nil: true)
},
render_nil: true,
if: -> (*) { !represented.milestone? }
if: ->(*) { !represented.milestone? }
property :date,
exec_context: :decorator,
getter: -> (*) {
datetime_formatter.format_date(represented.due_date, allow_nil: true)
},
setter: -> (value, *) {
new_date = datetime_formatter.parse_date(value,
'date',
allow_nil: true)
represented.due_date = represented.start_date = new_date
},
render_nil: true,
if: -> (*) { represented.milestone? }
property :version_id,
getter: -> (*) { nil },
setter: -> (value, *) { self.fixed_version_id = value },
render_nil: false
property :created_at,
getter: -> (*) { nil }, render_nil: false
property :updated_at,
getter: -> (*) { nil }, render_nil: false
if: ->(represented:, **) { represented.milestone? }
private
property :created_at,
getter: ->(*) { nil }, render_nil: false
property :updated_at,
getter: ->(*) { nil }, render_nil: false
def linked_resources
work_package_attribute_links_representer represented
end
def linked_resources=(value)
representer = work_package_attribute_links_representer represented
representer.from_json(value.to_json)
end
def estimated_hours
datetime_formatter.format_duration_from_hours(represented.estimated_hours,
allow_nil: true)
end
def estimated_hours=(value)
represented.estimated_hours = datetime_formatter
.parse_duration_to_hours(value,
'estimated_hours',
allow_nil: true)
end
def description
API::Decorators::Formattable.new(represented.description, object: represented)
end
def description=(value)
represented.description = value['raw']
end
def start_date
datetime_formatter.format_date(represented.start_date, allow_nil: true)
end
def start_date=(value)
represented.start_date = datetime_formatter.parse_date(value,
'startDate',
allow_nil: true)
end
def due_date
datetime_formatter.format_date(represented.due_date, allow_nil: true)
end
def due_date=(value)
represented.due_date = datetime_formatter.parse_date(value,
'dueDate',
allow_nil: true)
end
def date=(value)
new_date = datetime_formatter.parse_date(value,
'date',
allow_nil: true)
represented.due_date = represented.start_date = new_date
end
def date
datetime_formatter.format_date(represented.due_date, allow_nil: true)
end
def datetime_formatter
API::V3::Utilities::DateTimeFormatter
@@ -51,7 +51,7 @@ module API
rep = representer.new Relation.new, current_user: current_user
relation = rep.from_json request.body.read
service = ::CreateRelationService.new user: current_user
call = service.call relation, send_notifications: !(params[:notify] == 'false')
call = service.call relation, send_notifications: (params[:notify] != 'false')
if call.success?
representer.new call.result, current_user: current_user, embed_links: true
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -60,7 +61,7 @@ module API
super
end
self_link title_getter: -> (*) { represented.subject }
self_link title_getter: ->(*) { represented.subject }
link :update do
{
@@ -278,8 +279,8 @@ module API
linked_property :parent,
path: :work_package,
title_getter: -> (*) { represented.parent.subject },
show_if: -> (*) { represented.parent.nil? || represented.parent.visible? }
title_getter: ->(*) { represented.parent.subject },
show_if: ->(*) { represented.parent.nil? || represented.parent.visible? }
link :timeEntries do
{
@@ -295,7 +296,7 @@ module API
linked_property :version,
getter: :fixed_version,
title_getter: -> (*) {
title_getter: ->(*) {
represented.fixed_version.to_s
},
embed_as: ::API::V3::Versions::VersionRepresenter
@@ -323,42 +324,42 @@ module API
property :subject, render_nil: true
property :description,
exec_context: :decorator,
getter: -> (*) {
getter: ->(*) {
::API::Decorators::Formattable.new(represented.description, object: represented)
},
setter: -> (value, *) { represented.description = value['raw'] },
setter: ->(value, *) { represented.description = value['raw'] },
render_nil: true
property :start_date,
exec_context: :decorator,
getter: -> (*) do
getter: ->(*) do
datetime_formatter.format_date(represented.start_date, allow_nil: true)
end,
render_nil: true,
if: -> (_) {
if: ->(_) {
!represented.is_milestone?
}
property :due_date,
exec_context: :decorator,
getter: -> (*) do
getter: ->(*) do
datetime_formatter.format_date(represented.due_date, allow_nil: true)
end,
render_nil: true,
if: -> (_) {
if: ->(_) {
!represented.is_milestone?
}
property :date,
exec_context: :decorator,
getter: -> (*) do
getter: ->(*) do
datetime_formatter.format_date(represented.due_date, allow_nil: true)
end,
render_nil: true,
if: -> (_) {
if: ->(_) {
represented.is_milestone?
}
property :estimated_time,
exec_context: :decorator,
getter: -> (*) do
getter: ->(*) do
datetime_formatter.format_duration_from_hours(represented.estimated_hours,
allow_nil: true)
end,
@@ -366,30 +367,29 @@ module API
writeable: false
property :spent_time,
exec_context: :decorator,
getter: -> (*) do
getter: ->(*) do
datetime_formatter.format_duration_from_hours(represented.spent_hours)
end,
writeable: false,
if: -> (_) {
if: ->(_) {
current_user_allowed_to(:view_time_entries, context: represented.project)
}
property :done_ratio,
as: :percentageDone,
render_nil: true,
writeable: false,
if: -> (*) { Setting.work_package_done_ratio != 'disabled' }
property :parent_id, writeable: true
if: ->(*) { Setting.work_package_done_ratio != 'disabled' }
property :created_at,
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.created_at) }
getter: ->(*) { datetime_formatter.format_datetime(represented.created_at) }
property :updated_at,
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.updated_at) }
getter: ->(*) { datetime_formatter.format_datetime(represented.updated_at) }
property :watchers,
embedded: true,
exec_context: :decorator,
if: -> (*) {
if: ->(*) {
current_user_allowed_to(:view_work_package_watchers,
context: represented.project) &&
embed_links
@@ -398,12 +398,12 @@ module API
property :attachments,
embedded: true,
exec_context: :decorator,
if: -> (*) { embed_links }
if: ->(*) { embed_links }
property :relations,
embedded: true,
exec_context: :decorator,
if: -> (*) { embed_links }
if: ->(*) { embed_links }
def _type
'WorkPackage'
@@ -430,9 +430,9 @@ module API
def relations
self_path = api_v3_paths.work_package_relations(represented.id)
relations = represented.relations
visible_relations = relations.select { |relation|
visible_relations = relations.select do |relation|
relation.other_work_package(represented).visible?
}
end
::API::V3::Relations::RelationCollectionRepresenter.new(visible_relations,
self_path,
@@ -445,7 +445,7 @@ module API
self.to_eager_load = [{ children: { project: :enabled_modules } },
{ parent: { project: :enabled_modules } },
{ project: [:enabled_modules, :work_package_custom_fields] },
{ project: %i(enabled_modules work_package_custom_fields) },
:status,
:priority,
{ type: :custom_fields },
@@ -89,7 +89,7 @@ module API
end
def notify_according_to_params
!(params[:notify] == 'false')
params[:notify] != 'false'
end
end
end
+1 -1
View File
@@ -47,6 +47,6 @@ module ExtendedHTTP
#
# This is especially useful for successful update actions.
def no_content
render text: '', status: :no_content
render html: '', status: :no_content
end
end
+54
View File
@@ -0,0 +1,54 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 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 OpenProject
module Patches
module Reform
def merge!(errors, prefix)
@store_new_symbols = false
super(errors, prefix)
@store_new_symbols = true
errors.keys.each do |attribute|
errors.symbols_and_messages_for(attribute).each do |symbol, full_message, partial_message|
symbols_and_messages = writable_symbols_and_messages_for(attribute)
next if symbols_and_messages && symbols_and_messages.any? do |sam|
sam[0] === symbol &&
sam[1] === full_message &&
sam[2] === partial_message
end
symbols_and_messages << [symbol, full_message, partial_message]
end
end
end
end
end
end
@@ -38,7 +38,7 @@ module OpenProject
view = options[:view]
if view.respond_to?(:render)
if view.respond_to?(:javascript_include_tag)
view.render partial: '/timelines/timeline',
locals: { timeline: timeline }
else
@@ -77,6 +77,10 @@ class JournalFormatter::NamedAssociation < JournalFormatter::Attribute
end
end
def label(key)
@journal.journable.class.human_attribute_name(key.to_s.gsub(/_id$/, ''))
end
def class_from_field(field)
association = @journal.journable.class.reflect_on_association(field)
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -30,29 +31,48 @@
require 'spec_helper'
describe Relations::CreateContract do
let(:from) { FactoryGirl.create :work_package }
let(:to) { FactoryGirl.create :work_package }
let(:user) { FactoryGirl.create :admin }
let(:from) { FactoryGirl.build_stubbed :work_package }
let(:to) { FactoryGirl.build_stubbed :work_package }
let(:user) { FactoryGirl.build_stubbed :admin }
let(:relation) do
Relation.new from_id: from.id, to_id: to.id, relation_type: "follows", delay: 42
Relation.new from: from, to: to, relation_type: "follows", delay: 42
end
subject(:contract) { described_class.new relation, user }
describe "validating the delay" do
class ::Delayed::DelayProxy
def to_i
99
before do
allow(WorkPackage)
.to receive_message_chain(:visible, :exists?)
.and_return(true)
end
describe 'to' do
context 'not visible' do
before do
allow(WorkPackage)
.to receive_message_chain(:visible, :exists?)
.with(to.id)
.and_return(false)
end
it 'is invalid' do
is_expected.not_to be_valid
end
end
end
it "does not trigger delayed_job and checks the correct delay" do
begin
expect(contract).to be_valid
expect(contract.send(:fields).delay.to_i).to eq 42
ensure
::Delayed::DelayProxy.send :remove_method, :to_i
describe 'from' do
context 'not visible' do
before do
allow(WorkPackage)
.to receive_message_chain(:visible, :exists?)
.with(from.id)
.and_return(false)
end
it 'is invalid' do
is_expected.not_to be_valid
end
end
end
@@ -140,12 +140,8 @@ describe WorkPackages::BaseContract do
work_package.start_date = Date.today + 2.days
end
it 'is invalid' do
expect(contract).not_to be_valid
end
it 'notes the error' do
contract.valid?
contract.validate
message = I18n.t('activerecord.errors.models.work_package.attributes.start_date.violates_parent_relationships',
soonest_start: Date.today + 4.days)
@@ -39,7 +39,7 @@ describe 'Query selection', type: :feature do
let(:filter_1_name) { 'assignee' }
let(:filter_2_name) { 'percentageDone' }
let(:i18n_filter_1_name) { WorkPackage.human_attribute_name(:assigned_to_id) }
let(:i18n_filter_1_name) { WorkPackage.human_attribute_name(:assigned_to) }
let(:i18n_filter_2_name) { WorkPackage.human_attribute_name(:done_ratio) }
let(:default_status) { FactoryGirl.create(:default_status) }
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
@@ -29,7 +30,7 @@
require 'spec_helper'
describe ModelContract do
let(:model) {
let(:model) do
double('The model',
child_attribute: nil,
grand_child_attribute: nil,
@@ -37,7 +38,7 @@ describe ModelContract do
changed: [],
valid?: true,
errors: ActiveModel::Errors.new(nil))
}
end
let(:child_contract) { ChildContract.new(model) }
let(:grand_child_contract) { GrandChildContract.new(model) }
@@ -83,8 +84,8 @@ describe ModelContract do
'grand_child_attribute')
end
it 'should not contain the same attribute twice' do
expect(grand_child_contract.writable_attributes.count).to eq(3)
it 'should not contain the same attribute twice, but also has the _id variant' do
expect(grand_child_contract.writable_attributes.count).to eq(6)
end
it 'should execute all the validations' do
@@ -487,16 +487,6 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
end
end
describe 'parentId' do
it_behaves_like 'has basic schema properties' do
let(:path) { 'parentId' }
let(:type) { 'Integer' }
let(:name) { I18n.t('activerecord.attributes.work_package.parent') }
let(:required) { false }
let(:writable) { true }
end
end
describe 'parent' do
it_behaves_like 'has basic schema properties' do
let(:path) { 'parent' }
+4 -3
View File
@@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@@ -64,15 +65,15 @@ describe Representable do
describe 'as_strategy with class not responding to #call?' do
it 'raises error' do
expect {
expect do
class FailRepresenter < Representable::Decorator
include Representable::JSON
self.as_strategy = Object.new
self.as_strategy = ::Object.new
property :title
end
}.to raise_error(RuntimeError)
end.to raise_error(RuntimeError)
end
end
end
@@ -36,7 +36,7 @@ describe Queries::WorkPackages::Filter::VersionFilter, type: :model do
let(:type) { :list_optional }
let(:class_key) { :fixed_version_id }
let(:values) { [version.id.to_s] }
let(:name) { WorkPackage.human_attribute_name('fixed_version_id') }
let(:name) { WorkPackage.human_attribute_name('fixed_version') }
before do
if project
@@ -182,7 +182,7 @@ describe ::API::V3::Relations::RelationRepresenter, type: :request do
describe "permissions" do
let(:user) { FactoryGirl.create :user }
let(:permissions) { [:view_work_packages, :manage_work_package_relations] }
let(:permissions) { %i(view_work_packages manage_work_package_relations) }
let(:role) do
FactoryGirl.create :existing_role, permissions: permissions
@@ -307,55 +307,6 @@ describe 'API v3 Work package resource', type: :request do
end
end
context 'parent id' do
let(:parent) { FactoryGirl.create(:work_package, project: work_package.project) }
let(:params) { valid_params.merge(parentId: parent.id) }
before do
allow(Setting).to receive(:cross_project_work_package_relations?).and_return(true)
end
context 'w/o permission' do
include_context 'patch request'
it { expect(response.status).to eq(403) }
end
context 'with permission' do
before do role.add_permission!(:manage_subtasks) end
include_context 'patch request'
context 'invalid parent' do
let(:params) { valid_params.merge(parentId: '-123') }
it { expect(WorkPackage.visible(current_user).exists?('-123')).to be_falsey }
it { expect(response.status).to eq(422) }
end
context 'empty id' do
let(:params) { valid_params.merge(parentId: nil) }
it { expect(response.status).to eq(200) }
it { expect(subject.body).not_to have_json_path('parentId') }
it_behaves_like 'lock version updated'
end
context 'valid id' do
let(:params) { valid_params.merge(parentId: parent.id) }
it { expect(response.status).to eq(200) }
it { expect(subject.body).to be_json_eql(parent.id.to_json).at_path('parentId') }
it_behaves_like 'lock version updated'
end
end
end
context 'subject' do
let(:params) { valid_params.merge(subject: 'Updated subject') }
@@ -918,11 +869,20 @@ describe 'API v3 Work package resource', type: :request do
context 'multiple invalid attributes' do
let(:params) do
valid_params.tap { |h| h[:subject] = '' }
.merge(parentId: '-123')
valid_params
.tap { |h| h[:subject] = '' }
.merge(
_links: {
parent: {
href: api_v3_paths.work_package("-123")
}
}
)
end
before do role.add_permission!(:manage_subtasks) end
before do
role.add_permission!(:manage_subtasks)
end
include_context 'patch request'
@@ -979,11 +939,11 @@ describe 'API v3 Work package resource', type: :request do
it_behaves_like 'multiple errors of the same type', 2, 'PropertyConstraintViolation'
it_behaves_like 'multiple errors of the same type with messages' do
let(:message) {
[child_1.id, child_2.id].map { |id|
let(:message) do
[child_1.id, child_2.id].map do |id|
"Child element ##{id}: Parent cannot be in another project."
}
}
end
end
end
end
end
@@ -37,7 +37,7 @@ shared_context 'filter tests' do
filter.values = values
filter
end
let(:name) { model.human_attribute_name(instance_key || class_key) }
let(:name) { model.human_attribute_name((instance_key || class_key).to_s.gsub('_id', '')) }
let(:model) { WorkPackage }
end
+3 -3
View File
@@ -32,10 +32,10 @@ describe 'users/edit', type: :view do
let(:admin) { FactoryGirl.build :admin }
context 'authentication provider' do
let(:user) {
FactoryGirl.build :user, id: 1, # id is required to create route to edit
let(:user) do
FactoryGirl.build :user, id: 1, # id is required to create route to edit
identity_url: 'test_provider:veryuniqueid'
}
end
before do
assign(:user, user)