OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
openproject/app/contracts/model_contract.rb

195 lines
5.6 KiB

#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'reform'
require 'reform/form/active_model/model_validations'
class ModelContract < Reform::Contract
class << self
def writable_attributes
@writable_attributes ||= []
end
def writable_conditions
@writable_conditions ||= []
end
def attribute_validations
@attribute_validations ||= []
end
def attribute_aliases
@attribute_aliases ||= {}
end
def attribute_alias(db, outside)
attribute_aliases[db] = outside
end
def attribute(attribute, options = {}, &block)
property attribute
add_writable(attribute, options[:writeable])
if block
attribute_validations << block
end
end
private
def add_writable(attribute, writeable)
attribute_name = attribute.to_s.gsub /_id\z/, ''
unless writeable == false
writable_attributes << attribute_name
# allow the _id variant as well
writable_attributes << "#{attribute_name}_id"
end
if writeable.respond_to?(:call)
writable_conditions << [attribute_name, writeable]
end
end
end
attr_reader :user
def initialize(model, user)
super(model)
@user = user
end
# we want to add a validation error whenever someone sets a property that we don't know.
# However AR will cleverly try to resolve the value for errorneous properties. Thus we need
# to hook into this method and return nil for unknown properties to avoid NoMethod errors...
def read_attribute_for_validation(attribute)
if respond_to? attribute
send attribute
end
end
def writable_attributes
@writable_attributes ||= begin
writable = collect_ancestor_attributes(:writable_attributes)
collect_ancestor_attributes(:writable_conditions).each do |attribute, condition|
writable -= [attribute, "#{attribute}_id"] unless instance_exec(&condition)
end
writable
end
end
def writable?(attribute)
writable_attributes.include?(attribute.to_s)
end
def validate
readonly_attributes_unchanged
run_attribute_validations
super
model.valid?
# We need to merge the contract errors with the model errors in
# order to have them available at one place.
# This is something we need as long as we have validations split
# among the model and its contract.
errors.merge!(model.errors, [])
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
# use activerecord as the base scope instead of 'activemodel' to be compatible
# to the messages we have already stored
def self.i18n_scope
:activerecord
end
# end Methods required to get ActiveModel error messages working
private
def readonly_attributes_unchanged
invalid_changes = model.changed - writable_attributes
invalid_changes.each do |attribute|
outside_attribute = collect_ancestor_attributes(:attribute_aliases)[attribute] || attribute
errors.add outside_attribute, :error_readonly
end
end
def run_attribute_validations
attribute_validations.each { |validation| instance_exec(&validation) }
end
def attribute_validations
collect_ancestor_attributes(:attribute_validations)
end
# Traverse ancestor hierarchy to collect contract information.
# This allows to define attributes on a common base class of two or more contracts.
def collect_ancestor_attributes(attribute_to_collect)
combination_method, cleanup_method = if self.class.send(attribute_to_collect).is_a?(Hash)
%i[merge with_indifferent_access]
else
%i[concat uniq]
end
collect_ancestor_attributes_by(attribute_to_collect, combination_method, cleanup_method)
end
def collect_ancestor_attributes_by(attribute_to_collect, combination_method, cleanup_method)
klass = self.class
attributes = klass.send(attribute_to_collect)
while klass.superclass != ModelContract
# Collect all the attribute_to_collect from ancestors
klass = klass.superclass
attributes = attributes.send(combination_method, klass.send(attribute_to_collect))
end
attributes.send(cleanup_method)
end
end