mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
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:
@@ -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
@@ -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!
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
if (this.value === '') {
|
||||
passwordFields.show();
|
||||
passwordInputs.removeProp('disabled');
|
||||
passwordInputs.prop('disabled', false);
|
||||
} else {
|
||||
passwordFields.hide();
|
||||
passwordInputs.prop('disabled', 'disabled');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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? %>
|
||||
<%
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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."
|
||||
|
||||
@@ -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);
|
||||
|
||||
+17
-13
@@ -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();
|
||||
|
||||
+12
-12
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
+29
-9
@@ -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);
|
||||
|
||||
@@ -88,8 +88,6 @@ module API
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :sums,
|
||||
:count,
|
||||
:query
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -216,8 +216,6 @@ module API
|
||||
property :_dependencies,
|
||||
exec_context: :decorator
|
||||
|
||||
private
|
||||
|
||||
attr_accessor :form_embedded,
|
||||
:self_link
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
##
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user