1
0
Fork 0

Validate hierarchies

This commit is contained in:
Alex Kotov 2019-10-02 06:31:12 +05:00
parent 277fad827d
commit d4540f8498
Signed by: kotovalexarian
GPG Key ID: 553C0EBBEB5D5F08
13 changed files with 611 additions and 33 deletions

View File

@ -9,15 +9,15 @@ class OrgUnit < ApplicationRecord
class_name: 'OrgUnitKind',
inverse_of: :instances
belongs_to :parent,
belongs_to :parent_unit,
class_name: 'OrgUnit',
inverse_of: :children,
inverse_of: :children_units,
optional: true
has_many :children,
has_many :children_units,
class_name: 'OrgUnit',
inverse_of: :parent,
foreign_key: :parent_id
inverse_of: :parent_unit,
foreign_key: :parent_unit_id
has_many :all_relationships,
class_name: 'Relationship',
@ -31,7 +31,7 @@ class OrgUnit < ApplicationRecord
validates :name, good_small_text: true, uniqueness: true
validates :parent,
validates :parent_unit,
presence: {
if: ->(record) { record.kind&.parent_kind },
message: :required,
@ -39,9 +39,19 @@ class OrgUnit < ApplicationRecord
validate :parent_matches_kind
#############
# Callbacks #
#############
before_validation :set_level
private
def parent_matches_kind
errors.add :parent unless parent&.kind == kind&.parent_kind
errors.add :parent_unit unless parent_unit&.kind == kind&.parent_kind
end
def set_level
self.level = parent_unit.nil? ? 0 : parent_unit.level + 1
end
end

View File

@ -30,6 +30,12 @@ class OrgUnitKind < ApplicationRecord
validates :name, good_small_text: true, uniqueness: true
#############
# Callbacks #
#############
before_validation :set_level
###########
# Methods #
###########
@ -37,4 +43,10 @@ class OrgUnitKind < ApplicationRecord
def to_param
codename
end
private
def set_level
self.level = parent_kind.nil? ? 0 : parent_kind.level + 1
end
end

View File

@ -7,13 +7,35 @@ class Relationship < ApplicationRecord
belongs_to :org_unit, inverse_of: :all_relationships
belongs_to :parent_rel,
class_name: 'Relationship',
inverse_of: :children_rels,
optional: true
belongs_to :status, class_name: 'RelationStatus'
belongs_to :person, inverse_of: :all_relationships
has_many :children_rels,
class_name: 'Relationship',
inverse_of: :parent_rel,
foreign_key: :parent_rel_id
###############
# Validations #
###############
validates :from_date, presence: true, uniqueness: { scope: :person_id }
#############
# Callbacks #
#############
before_validation :set_level
private
def set_level
self.level = parent_rel.nil? ? 0 : parent_rel.level + 1
end
end

View File

@ -24,15 +24,15 @@
<dt><%= OrgUnit.human_attribute_name :name %></dt>
<dd><%= @org_unit.name %></dd>
<dt><%= OrgUnit.human_attribute_name :parent %></dt>
<dt><%= OrgUnit.human_attribute_name :parent_unit %></dt>
<dd>
<% if @org_unit.parent.nil? %>
<% if @org_unit.parent_unit.nil? %>
<%= none %>
<% elsif policy([:staff, @org_unit.parent]).show? %>
<%= link_to @org_unit.parent.name,
[:staff, @org_unit.parent] %>
<% elsif policy([:staff, @org_unit.parent_unit]).show? %>
<%= link_to @org_unit.parent_unit.name,
[:staff, @org_unit.parent_unit] %>
<% else %>
<%= @org_unit.parent.name %>
<%= @org_unit.parent_unit.name %>
<% end %>
</dd>
</dl>

View File

@ -77,7 +77,7 @@ en:
kind: Type
short_name: Short name
name: Name
parent: Parent
parent_unit: Parent
org_unit_kind:
id: ID
codename: Codename

View File

@ -77,7 +77,7 @@ ru:
kind: Тип
short_name: Короткое развание
name: Название
parent: Родитель
parent_unit: Родитель
org_unit_kind:
id: ID
codename: Кодовое имя

View File

@ -0,0 +1,229 @@
# frozen_string_literal: true
class AddLevelToTables < ActiveRecord::Migration[6.0]
include Partynest::Migration
def change
rename_column :org_units, :parent_id, :parent_unit_id
add_reference :relationships,
:parent_rel,
null: true,
index: true,
foreign_key: { to_table: :relationships }
# rubocop:disable Rails/NotNullColumn
add_column :org_unit_kinds, :level, :integer, null: false
add_column :org_units, :level, :integer, null: false
add_column :relationships, :level, :integer, null: false
# rubocop:enable Rails/NotNullColumn
add_constraint :org_unit_kinds, :level, 'level >= 0'
add_constraint :org_units, :level, 'level >= 0'
add_constraint :relationships, :level, 'level >= 0'
add_constraint :org_unit_kinds, :parent_kind, 'parent_kind_id != id'
add_constraint :org_units, :parent_unit, 'parent_unit_id != id'
add_constraint :relationships, :parent_rel, 'parent_rel_id != id'
add_func_validate_org_unit_kind_hierarchy
add_func_validate_org_unit_hierarchy
add_func_validate_relationship_hierarchy
add_trigger :org_unit_kinds,
:validate_hierarchy,
'BEFORE INSERT OR UPDATE',
'validate_org_unit_kind_hierarchy()'
add_trigger :org_units,
:validate_hierarchy,
'BEFORE INSERT OR UPDATE',
'validate_org_unit_hierarchy()'
add_trigger :relationships,
:validate_hierarchy,
'BEFORE INSERT OR UPDATE',
'validate_relationship_hierarchy()'
end
def add_func_validate_org_unit_kind_hierarchy
add_func :validate_org_unit_kind_hierarchy, <<~SQL
() RETURNS trigger LANGUAGE plpgsql AS
$$
DECLARE
parent_kind record;
BEGIN
IF NEW.parent_kind_id IS NULL THEN
IF NEW.level != 0 THEN
RAISE EXCEPTION 'level is invalid';
END IF;
RETURN NEW;
END IF;
SELECT *
FROM org_unit_kinds
INTO parent_kind
WHERE id = NEW.parent_kind_id;
IF parent_kind IS NULL THEN
RAISE EXCEPTION 'can not find parent';
END IF;
IF NEW.level != parent_kind.level + 1 THEN
RAISE EXCEPTION 'level is invalid';
END IF;
RETURN NEW;
END;
$$;
SQL
end
def add_func_validate_org_unit_hierarchy
add_func :validate_org_unit_hierarchy, <<~SQL
() RETURNS trigger LANGUAGE plpgsql AS
$$
DECLARE
kind record;
parent_kind record;
parent_unit record;
BEGIN
IF NEW.kind_id IS NULL THEN
RAISE EXCEPTION 'does not have type';
END IF;
SELECT *
FROM org_unit_kinds
INTO kind
WHERE id = NEW.kind_id;
IF kind IS NULL THEN
RAISE EXCEPTION 'can not find type';
END IF;
SELECT *
FROM org_unit_kinds
INTO parent_kind
WHERE id = kind.parent_kind_id;
IF (kind.parent_kind_id IS NULL) != (parent_kind IS NULL) THEN
RAISE EXCEPTION 'can not find parent type';
END IF;
IF parent_kind IS NULL THEN
IF NEW.parent_unit_id IS NOT NULL THEN
RAISE EXCEPTION 'parent is invalid (expected NULL)';
END IF;
IF NEW.level != 0 THEN
RAISE EXCEPTION 'level is invalid';
END IF;
RETURN NEW;
END IF;
IF NEW.parent_unit_id IS NULL THEN
RAISE EXCEPTION 'parent is invalid (expected NOT NULL)';
END IF;
SELECT *
FROM org_units
INTO parent_unit
WHERE id = NEW.parent_unit_id;
IF parent_unit IS NULL THEN
RAISE EXCEPTION 'can not find parent';
END IF;
IF parent_unit.kind_id != parent_kind.id THEN
RAISE EXCEPTION 'parent is invalid';
END IF;
IF (
NEW.level != kind.level OR
NEW.level != parent_kind.level + 1 OR
NEW.level != parent_unit.level + 1
) THEN
RAISE EXCEPTION 'level is invalid';
END IF;
RETURN NEW;
END;
$$;
SQL
end
def add_func_validate_relationship_hierarchy
add_func :validate_relationship_hierarchy, <<~SQL
() RETURNS trigger LANGUAGE plpgsql AS
$$
DECLARE
org_unit record;
parent_unit record;
parent_rel record;
BEGIN
IF NEW.org_unit_id IS NULL THEN
RAISE EXCEPTION 'does not have org unit';
END IF;
SELECT *
FROM org_units
INTO org_unit
WHERE id = NEW.org_unit_id;
IF org_unit IS NULL THEN
RAISE EXCEPTION 'can not find org unit';
END IF;
SELECT *
FROM org_units
INTO parent_unit
WHERE id = org_unit.parent_unit_id;
IF (org_unit.parent_unit_id IS NULL) != (parent_unit IS NULL) THEN
RAISE EXCEPTION 'can not find parent org unit';
END IF;
IF parent_unit IS NULL THEN
IF NEW.parent_rel_id IS NOT NULL THEN
RAISE EXCEPTION 'parent rel is invalid (expected NULL)';
END IF;
IF NEW.level != 0 THEN
RAISE EXCEPTION 'level is invalid (expected 0)';
END IF;
RETURN NEW;
END IF;
IF NEW.parent_rel_id IS NULL THEN
RAISE EXCEPTION 'parent rel is invalid (expected NOT NULL)';
END IF;
SELECT *
FROM relationships
INTO parent_rel
WHERE id = NEW.parent_rel_id;
IF parent_rel IS NULL THEN
RAISE EXCEPTION 'can not find parent rel';
END IF;
IF parent_rel.org_unit_id != parent_unit.id THEN
RAISE EXCEPTION 'parent rel is invalid';
END IF;
IF (
NEW.level != org_unit.level OR
NEW.level != parent_unit.level + 1 OR
NEW.level != parent_rel.level + 1
) THEN
END IF;
RETURN NEW;
END;
$$;
SQL
end
end

View File

@ -199,6 +199,193 @@ END;
$_$;
--
-- Name: validate_org_unit_hierarchy(); Type: FUNCTION; Schema: public; Owner: -
--
CREATE FUNCTION public.validate_org_unit_hierarchy() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
kind record;
parent_kind record;
parent_unit record;
BEGIN
IF NEW.kind_id IS NULL THEN
RAISE EXCEPTION 'does not have type';
END IF;
SELECT *
FROM org_unit_kinds
INTO kind
WHERE id = NEW.kind_id;
IF kind IS NULL THEN
RAISE EXCEPTION 'can not find type';
END IF;
SELECT *
FROM org_unit_kinds
INTO parent_kind
WHERE id = kind.parent_kind_id;
IF (kind.parent_kind_id IS NULL) != (parent_kind IS NULL) THEN
RAISE EXCEPTION 'can not find parent type';
END IF;
IF parent_kind IS NULL THEN
IF NEW.parent_unit_id IS NOT NULL THEN
RAISE EXCEPTION 'parent is invalid (expected NULL)';
END IF;
IF NEW.level != 0 THEN
RAISE EXCEPTION 'level is invalid';
END IF;
RETURN NEW;
END IF;
IF NEW.parent_unit_id IS NULL THEN
RAISE EXCEPTION 'parent is invalid (expected NOT NULL)';
END IF;
SELECT *
FROM org_units
INTO parent_unit
WHERE id = NEW.parent_unit_id;
IF parent_unit IS NULL THEN
RAISE EXCEPTION 'can not find parent';
END IF;
IF parent_unit.kind_id != parent_kind.id THEN
RAISE EXCEPTION 'parent is invalid';
END IF;
IF (
NEW.level != kind.level OR
NEW.level != parent_kind.level + 1 OR
NEW.level != parent_unit.level + 1
) THEN
RAISE EXCEPTION 'level is invalid';
END IF;
RETURN NEW;
END;
$$;
--
-- Name: validate_org_unit_kind_hierarchy(); Type: FUNCTION; Schema: public; Owner: -
--
CREATE FUNCTION public.validate_org_unit_kind_hierarchy() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
parent_kind record;
BEGIN
IF NEW.parent_kind_id IS NULL THEN
IF NEW.level != 0 THEN
RAISE EXCEPTION 'level is invalid';
END IF;
RETURN NEW;
END IF;
SELECT *
FROM org_unit_kinds
INTO parent_kind
WHERE id = NEW.parent_kind_id;
IF parent_kind IS NULL THEN
RAISE EXCEPTION 'can not find parent';
END IF;
IF NEW.level != parent_kind.level + 1 THEN
RAISE EXCEPTION 'level is invalid';
END IF;
RETURN NEW;
END;
$$;
--
-- Name: validate_relationship_hierarchy(); Type: FUNCTION; Schema: public; Owner: -
--
CREATE FUNCTION public.validate_relationship_hierarchy() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
org_unit record;
parent_unit record;
parent_rel record;
BEGIN
IF NEW.org_unit_id IS NULL THEN
RAISE EXCEPTION 'does not have org unit';
END IF;
SELECT *
FROM org_units
INTO org_unit
WHERE id = NEW.org_unit_id;
IF org_unit IS NULL THEN
RAISE EXCEPTION 'can not find org unit';
END IF;
SELECT *
FROM org_units
INTO parent_unit
WHERE id = org_unit.parent_unit_id;
IF (org_unit.parent_unit_id IS NULL) != (parent_unit IS NULL) THEN
RAISE EXCEPTION 'can not find parent org unit';
END IF;
IF parent_unit IS NULL THEN
IF NEW.parent_rel_id IS NOT NULL THEN
RAISE EXCEPTION 'parent rel is invalid (expected NULL)';
END IF;
IF NEW.level != 0 THEN
RAISE EXCEPTION 'level is invalid (expected 0)';
END IF;
RETURN NEW;
END IF;
IF NEW.parent_rel_id IS NULL THEN
RAISE EXCEPTION 'parent rel is invalid (expected NOT NULL)';
END IF;
SELECT *
FROM relationships
INTO parent_rel
WHERE id = NEW.parent_rel_id;
IF parent_rel IS NULL THEN
RAISE EXCEPTION 'can not find parent rel';
END IF;
IF parent_rel.org_unit_id != parent_unit.id THEN
RAISE EXCEPTION 'parent rel is invalid';
END IF;
IF (
NEW.level != org_unit.level OR
NEW.level != parent_unit.level + 1 OR
NEW.level != parent_rel.level + 1
) THEN
END IF;
RETURN NEW;
END;
$$;
SET default_tablespace = '';
SET default_with_oids = false;
@ -473,8 +660,11 @@ CREATE TABLE public.org_unit_kinds (
short_name character varying NOT NULL,
name character varying NOT NULL,
parent_kind_id bigint,
level integer NOT NULL,
CONSTRAINT codename CHECK (public.is_codename((codename)::text)),
CONSTRAINT level CHECK ((level >= 0)),
CONSTRAINT name CHECK (public.is_good_small_text((name)::text)),
CONSTRAINT parent_kind CHECK ((parent_kind_id <> id)),
CONSTRAINT short_name CHECK (public.is_good_small_text((short_name)::text))
);
@ -509,8 +699,11 @@ CREATE TABLE public.org_units (
short_name character varying NOT NULL,
name character varying NOT NULL,
kind_id bigint NOT NULL,
parent_id bigint,
parent_unit_id bigint,
level integer NOT NULL,
CONSTRAINT level CHECK ((level >= 0)),
CONSTRAINT name CHECK (public.is_good_small_text((name)::text)),
CONSTRAINT parent_unit CHECK ((parent_unit_id <> id)),
CONSTRAINT short_name CHECK (public.is_good_small_text((short_name)::text))
);
@ -759,7 +952,11 @@ CREATE TABLE public.relationships (
person_id bigint NOT NULL,
from_date date NOT NULL,
status_id bigint NOT NULL,
org_unit_id bigint NOT NULL
org_unit_id bigint NOT NULL,
parent_rel_id bigint,
level integer NOT NULL,
CONSTRAINT level CHECK ((level >= 0)),
CONSTRAINT parent_rel CHECK ((parent_rel_id <> id))
);
@ -1343,10 +1540,10 @@ CREATE UNIQUE INDEX index_org_units_on_name ON public.org_units USING btree (nam
--
-- Name: index_org_units_on_parent_id; Type: INDEX; Schema: public; Owner: -
-- Name: index_org_units_on_parent_unit_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_org_units_on_parent_id ON public.org_units USING btree (parent_id);
CREATE INDEX index_org_units_on_parent_unit_id ON public.org_units USING btree (parent_unit_id);
--
@ -1454,6 +1651,13 @@ CREATE INDEX index_relationships_on_from_date ON public.relationships USING btre
CREATE INDEX index_relationships_on_org_unit_id ON public.relationships USING btree (org_unit_id);
--
-- Name: index_relationships_on_parent_rel_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_relationships_on_parent_rel_id ON public.relationships USING btree (parent_rel_id);
--
-- Name: index_relationships_on_person_id_and_from_date; Type: INDEX; Schema: public; Owner: -
--
@ -1545,6 +1749,27 @@ CREATE TRIGGER ensure_contact_list_id_remains_unchanged BEFORE UPDATE OF contact
CREATE TRIGGER ensure_superuser_has_related_user BEFORE INSERT OR UPDATE ON public.accounts FOR EACH ROW EXECUTE PROCEDURE public.ensure_superuser_has_related_user();
--
-- Name: org_unit_kinds validate_hierarchy; Type: TRIGGER; Schema: public; Owner: -
--
CREATE TRIGGER validate_hierarchy BEFORE INSERT OR UPDATE ON public.org_unit_kinds FOR EACH ROW EXECUTE PROCEDURE public.validate_org_unit_kind_hierarchy();
--
-- Name: org_units validate_hierarchy; Type: TRIGGER; Schema: public; Owner: -
--
CREATE TRIGGER validate_hierarchy BEFORE INSERT OR UPDATE ON public.org_units FOR EACH ROW EXECUTE PROCEDURE public.validate_org_unit_hierarchy();
--
-- Name: relationships validate_hierarchy; Type: TRIGGER; Schema: public; Owner: -
--
CREATE TRIGGER validate_hierarchy BEFORE INSERT OR UPDATE ON public.relationships FOR EACH ROW EXECUTE PROCEDURE public.validate_relationship_hierarchy();
--
-- Name: relationships fk_rails_0ea63a126c; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -1566,7 +1791,7 @@ ALTER TABLE ONLY public.people
--
ALTER TABLE ONLY public.org_units
ADD CONSTRAINT fk_rails_54c0512b74 FOREIGN KEY (parent_id) REFERENCES public.org_units(id);
ADD CONSTRAINT fk_rails_54c0512b74 FOREIGN KEY (parent_unit_id) REFERENCES public.org_units(id);
--
@ -1657,6 +1882,14 @@ ALTER TABLE ONLY public.relation_transitions
ADD CONSTRAINT fk_rails_b61956945e FOREIGN KEY (from_status_id) REFERENCES public.relation_statuses(id);
--
-- Name: relationships fk_rails_b943fd3c34; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.relationships
ADD CONSTRAINT fk_rails_b943fd3c34 FOREIGN KEY (parent_rel_id) REFERENCES public.relationships(id);
--
-- Name: org_units fk_rails_ccc56f184e; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -1725,6 +1958,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20190930210852'),
('20190930215223'),
('20191001022049'),
('20191001211809');
('20191001211809'),
('20191002002101');

View File

@ -10,7 +10,7 @@ FactoryBot.define do
trait :with_parent do
association :kind, factory: :some_children_org_unit_kind
parent { create :some_root_org_unit, kind: kind.parent_kind }
parent_unit { create :some_root_org_unit, kind: kind.parent_kind }
end
end

View File

@ -1,16 +1,30 @@
# frozen_string_literal: true
FactoryBot.define do
factory :supporter_relationship, class: Relationship do
association :org_unit, factory: :some_children_org_unit
factory :some_root_relationship, class: Relationship do
association :org_unit, factory: :some_root_org_unit
association :status, factory: :some_relation_status
association :person, factory: :initial_person
sequence :from_date do |n|
Date.new rand((10 * n)...(11 * n)), rand(1..12), rand(1..28)
end
trait :with_parent do
association :org_unit, factory: :some_children_org_unit
parent_rel do
create :some_root_relationship, org_unit: org_unit&.parent_unit
end
end
end
factory :some_children_relationship,
parent: :some_root_relationship,
traits: %i[with_parent]
factory :supporter_relationship, parent: :some_children_relationship
factory :excluded_relationship, parent: :supporter_relationship
factory :member_relationship, parent: :supporter_relationship

View File

@ -44,6 +44,20 @@ module Partynest
end
end
def add_trigger(table, name, events, call)
reversible do |dir|
dir.up { trigger_creation(table, name, events, call).call }
dir.down { trigger_deletion(table, name).call }
end
end
def remove_trigger(table, name, events, call)
reversible do |dir|
dir.up { trigger_deletion(table, name).call }
dir.down { trigger_creation(table, name, events, call).call }
end
end
private
def func_creation(name, sql)
@ -69,6 +83,18 @@ module Partynest
end
end
def trigger_creation(table, name, events, call)
lambda do
execute <<~SQL
CREATE TRIGGER #{name}
#{events}
ON #{table}
FOR EACH ROW
EXECUTE PROCEDURE #{call};
SQL
end
end
def func_deletion(name)
lambda do
execute "DROP FUNCTION #{name}"
@ -86,5 +112,11 @@ module Partynest
execute "ALTER TABLE #{table} DROP CONSTRAINT #{name}"
end
end
def trigger_deletion(table, name)
lambda do
execute "DROP TRIGGER #{name} ON #{table}"
end
end
end
end

View File

@ -17,19 +17,21 @@ RSpec.describe OrgUnit do
it { is_expected.to validate_presence_of(:kind).with_message(:required) }
end
describe '#parent' do
describe '#parent_unit' do
it do
is_expected.to \
belong_to(:parent)
belong_to(:parent_unit)
.class_name('OrgUnit')
.inverse_of(:children)
.inverse_of(:children_units)
end
context 'when organizational unit type does not require parent' do
subject { create :some_root_org_unit }
it do
is_expected.not_to validate_presence_of(:parent).with_message(:required)
is_expected.not_to \
validate_presence_of(:parent_unit)
.with_message(:required)
end
end
@ -37,18 +39,20 @@ RSpec.describe OrgUnit do
subject { create :some_children_org_unit }
it do
is_expected.to validate_presence_of(:parent).with_message(:required)
is_expected.to \
validate_presence_of(:parent_unit)
.with_message(:required)
end
end
end
describe '#children' do
describe '#children_units' do
it do
is_expected.to \
have_many(:children)
have_many(:children_units)
.class_name('OrgUnit')
.inverse_of(:parent)
.with_foreign_key(:parent_id)
.inverse_of(:parent_unit)
.with_foreign_key(:parent_unit_id)
.dependent(:restrict_with_exception)
end
end

View File

@ -14,6 +14,27 @@ RSpec.describe Relationship do
end
end
describe '#parent_rel' do
it do
is_expected.to \
belong_to(:parent_rel)
.class_name('Relationship')
.inverse_of(:children_rels)
.optional
end
end
describe '#children_rels' do
it do
is_expected.to \
have_many(:children_rels)
.class_name('Relationship')
.inverse_of(:parent_rel)
.with_foreign_key(:parent_rel_id)
.dependent(:restrict_with_exception)
end
end
describe '#status' do
it do
is_expected.to belong_to(:status).class_name('RelationStatus').required