Skip to content
Permalink
Browse files

Fixed issues #1259 and #1598 by adding ObjectManager::Attribute valid…

…ations.
  • Loading branch information...
thorsteneckel authored and znuny-robo committed Mar 13, 2019
1 parent e579c80 commit 444e48e3772eb4abf8fb00170dca3d08ad65955f
@@ -0,0 +1,9 @@
# Copyright (C) 2018 Zammad Foundation, http://zammad-foundation.org/
module HasObjectManagerAttributesValidation
extend ActiveSupport::Concern

included do
include ActiveModel::Validations
validates_with ObjectManager::Attribute::Validation, on: %i[create update]
end
end
@@ -6,6 +6,7 @@ class Group < ApplicationModel
include ChecksClientNotification
include ChecksLatestChangeObserved
include HasHistory
include HasObjectManagerAttributesValidation

belongs_to :email_address
belongs_to :signature
@@ -0,0 +1,66 @@
class ObjectManager::Attribute::Validation < ActiveModel::Validator
include ::Mixin::HasBackends

def validate(record)
return if validation_unneeded?

@record = record
sanitize_memory_cache

return if attributes_unchanged?

model_attributes.select(&:editable).each do |attribute|
perform_validation(attribute)
end
end

private

attr_reader :record

def validation_unneeded?
return true if Setting.get('import_mode')

ApplicationHandleInfo.current != 'application_server'
end

def attributes_unchanged?
model_attributes.none? do |attribute|
record.will_save_change_to_attribute?(attribute.name)
end
end

def model_attributes
@model_attributes ||= begin
object_lookup_id = ObjectLookup.by_name(record.class.name)
@active_attributes.select { |attribute| attribute.object_lookup_id == object_lookup_id }
end
end

def perform_validation(attribute)
backends.each do |backend|
backend.validate(
record: record,
attribute: attribute
)
end
end

def validations
@validations ||= backend.map { backend.new }
end

def sanitize_memory_cache
@model_attributes = nil

latest_cache_key = active_attributes_in_db.cache_key
return if @previous_cache_key == latest_cache_key

@previous_cache_key = latest_cache_key
@active_attributes = active_attributes_in_db.to_a
end

def active_attributes_in_db
ObjectManager::Attribute.where(active: true)
end
end
@@ -0,0 +1,26 @@
class ObjectManager::Attribute::Validation::Backend
include Mixin::IsBackend

def self.inherited(subclass)
subclass.is_backend_of(::ObjectManager::Attribute::Validation)
end

def self.validate(*args)
new(*args).validate
end

attr_reader :record, :attribute, :value, :previous_value

def initialize(record:, attribute:)
@record = record
@attribute = attribute
@value = record[attribute.name]
@previous_value = record.attribute_in_database(attribute.name)
end

def invalid_because_attribute(message)
record.errors.add attribute.name.to_sym, message
end
end

Mixin::RequiredSubPaths.eager_load_recursive(__dir__)
@@ -0,0 +1,30 @@
class ObjectManager::Attribute::Validation::Date < ObjectManager::Attribute::Validation::Backend

def validate
return if value.blank?
return if irrelevant_attribute?

validate_past
validate_future
end

private

def irrelevant_attribute?
%w[date datetime].exclude?(attribute.data_type)
end

def validate_past
return if attribute.data_option[:past]
return if !value.past?

invalid_because_attribute('does not allow past dates.')
end

def validate_future
return if attribute.data_option[:future]
return if !value.future?

invalid_because_attribute('does not allow future dates.')
end
end
@@ -0,0 +1,45 @@
class ObjectManager::Attribute::Validation::Required < ObjectManager::Attribute::Validation::Backend

def validate
return if value.present?
return if optional_for_user?

invalid_because_attribute('is required but missing.')
end

private

def optional_for_user?
return true if system_user?
return true if required_for_permissions.blank?
return false if required_for_permissions.include?('-all-')

!user.permissions?(required_for_permissions)
end

def system_user?
user_id.blank? || user_id == 1
end

def user_id
user_id ||= UserInfo.current_user_id
end

def user
@user ||= User.lookup(id: user_id)
end

def required_for_permissions
@required_for_permissions ||= begin
attribute.screens[action]&.each_with_object([]) do |(permission, config), result|
result.push(permission) if config[:required].present?
end
end
end

def action
return :edit if record.persisted?

attribute.screens.keys.find { |e| e.start_with?('create') }
end
end
@@ -8,6 +8,7 @@ class Organization < ApplicationModel
include HasSearchIndexBackend
include CanCsvImport
include ChecksHtmlSanitized
include HasObjectManagerAttributesValidation

include Organization::ChecksAccess
include Organization::Assets
@@ -14,6 +14,7 @@ class Ticket < ApplicationModel
include HasKarmaActivityLog
include HasLinks
include Ticket::ChecksAccess
include HasObjectManagerAttributesValidation

include Ticket::Escalation
include Ticket::Subject
@@ -6,6 +6,7 @@ class Ticket::Article < ApplicationModel
include HasHistory
include ChecksHtmlSanitized
include CanCsvImport
include HasObjectManagerAttributesValidation

include Ticket::Article::ChecksAccess
include Ticket::Article::Assets
@@ -9,6 +9,7 @@ class User < ApplicationModel
include ChecksHtmlSanitized
include HasGroups
include HasRoles
include HasObjectManagerAttributesValidation

include User::ChecksAccess
include User::Assets
@@ -0,0 +1,7 @@
class ObjectManagerAttributeIndexes < ActiveRecord::Migration[5.1]
def change

add_index :object_manager_attributes, :active
add_index :object_manager_attributes, :updated_at
end
end
@@ -0,0 +1,13 @@
module Mixin
module HasBackends
extend ActiveSupport::Concern

included do
cattr_accessor :backends do
Set.new
end

require_dependency "#{name}::Backend".underscore
end
end
end
@@ -0,0 +1,12 @@
module Mixin
module IsBackend
extend ActiveSupport::Concern

class_methods do

def is_backend_of(klass) # rubocop:disable Naming/PredicateName
klass.backends.add(self)
end
end
end
end
@@ -0,0 +1,12 @@
RSpec.shared_examples 'Mixin::HasBackends' do

describe '.backends' do
it 'is a Set' do
expect(described_class.backends).to be_a(Set)
end
end

it "auto requires #{described_class}::Backend" do
expect(described_class).to be_const_defined(:Backend)
end
end
@@ -0,0 +1,5 @@
RSpec.shared_examples 'HasObjectManagerAttributesValidation' do
it 'validates ObjectManager::Attributes' do
expect(described_class.validators.map(&:class)).to include(ObjectManager::Attribute::Validation)
end
end
@@ -1,8 +1,10 @@
require 'rails_helper'
require 'models/application_model_examples'
require 'models/concerns/can_be_imported_examples'
require 'models/concerns/has_object_manager_attributes_validation_examples'

RSpec.describe Group, type: :model do
it_behaves_like 'ApplicationModel'
it_behaves_like 'CanBeImported'
it_behaves_like 'HasObjectManagerAttributesValidation'
end
@@ -0,0 +1,15 @@
require 'rails_helper'

RSpec.shared_examples 'a validation without errors' do
it 'validatates without errors' do
subject.validate
expect(record.errors).to be_blank
end
end

RSpec.shared_examples 'a validation with errors' do
it 'validates with errors' do
subject.validate
expect(record.errors).to be_present
end
end
@@ -0,0 +1,58 @@
require 'rails_helper'

RSpec.describe ObjectManager::Attribute::Validation::Backend do

it 'registers inheriting classes as ObjectManager::Attribute::Validation backends' do
backends = spy
expect(ObjectManager::Attribute::Validation).to receive(:backends).and_return(backends)
backend = Class.new(described_class)
expect(backends).to have_received(:add).with(backend)
end

describe 'backend interface' do

let(:record) { build(:user) }
let(:attribute) { ::ObjectManager::Attribute.find_by(name: 'firstname') }

subject do
described_class.new(
record: record,
attribute: attribute
)
end

it 'has attr_accessor for record' do
expect(subject.record).to eq(record)
end

it 'has attr_accessor for attribute' do
expect(subject.attribute).to eq(attribute)
end

it 'has attr_accessor for value' do
expect(subject.value).to eq(record[attribute.name])
end

it 'has attr_accessor for previous_value' do
record.save!
previous_value = record[attribute.name]
record[attribute.name] = 'changed'
expect(subject.previous_value).to eq(previous_value)
end

describe '.invalid_because_attribute' do

before do
subject.invalid_because_attribute('has value that is ... .')
end

it 'adds Rails validation error' do
expect(record.errors.count).to be(1)
end

it 'uses ObjectManager::Attribute#name as ActiveModel::Errors identifier' do
expect(record.errors.to_h).to have_key(attribute.name.to_sym)
end
end
end
end
Oops, something went wrong.

0 comments on commit 444e48e

Please sign in to comment.
You can’t perform that action at this time.