genealogy 1.5.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/lib/genealogy.rb +4 -2
- data/lib/genealogy/alter_methods.rb +173 -112
- data/lib/genealogy/constants.rb +52 -21
- data/lib/genealogy/current_spouse_methods.rb +66 -0
- data/lib/genealogy/exceptions.rb +5 -8
- data/lib/genealogy/genealogy.rb +100 -63
- data/lib/genealogy/ineligible_methods.rb +116 -0
- data/lib/genealogy/query_methods.rb +194 -236
- data/lib/genealogy/util_methods.rb +180 -0
- data/lib/genealogy/version.rb +1 -1
- metadata +103 -30
- data/lib/genealogy/spouse_methods.rb +0 -53
data/lib/genealogy/constants.rb
CHANGED
@@ -1,24 +1,55 @@
|
|
1
1
|
module Genealogy
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
module Constants
|
3
|
+
# deafault options for has_parent method
|
4
|
+
DEFAULTS = {
|
5
|
+
column_names: {
|
6
|
+
sex: 'sex',
|
7
|
+
father_id: 'father_id',
|
8
|
+
mother_id: 'mother_id',
|
9
|
+
current_spouse_id: 'current_spouse_id',
|
10
|
+
birth_date: 'birth_date',
|
11
|
+
death_date: 'death_date'
|
12
|
+
},
|
13
|
+
perform_validation: true,
|
14
|
+
ineligibility: :pedigree,
|
15
|
+
current_spouse: false,
|
16
|
+
sex_values: ['M','F'],
|
17
|
+
limit_ages: {
|
18
|
+
min_male_procreation_age: 12,
|
19
|
+
max_male_procreation_age: 75,
|
20
|
+
min_female_procreation_age: 9,
|
21
|
+
max_female_procreation_age: 50,
|
22
|
+
max_male_life_expectancy: 110,
|
23
|
+
max_female_life_expectancy: 110
|
24
|
+
}
|
25
|
+
}
|
7
26
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
27
|
+
# ineligibility levels
|
28
|
+
PEDIGREE = 1
|
29
|
+
PEDIGREE_AND_DATES = 2
|
30
|
+
OFF = 0
|
31
|
+
|
32
|
+
PARENT2LINEAGE = ActiveSupport::HashWithIndifferentAccess.new({ father: :paternal, mother: :maternal })
|
33
|
+
LINEAGE2PARENT = ActiveSupport::HashWithIndifferentAccess.new({ paternal: :father, maternal: :mother })
|
34
|
+
PARENT2SEX = ActiveSupport::HashWithIndifferentAccess.new({ father: :male, mother: :female })
|
35
|
+
SEX2PARENT = ActiveSupport::HashWithIndifferentAccess.new({ male: :father, female: :mother })
|
36
|
+
OPPOSITESEX = ActiveSupport::HashWithIndifferentAccess.new({male: :female, female: :male})
|
37
|
+
|
38
|
+
AKA = ActiveSupport::HashWithIndifferentAccess.new({
|
39
|
+
father: "F",
|
40
|
+
mother: "M",
|
41
|
+
paternal_grandfather: "PGF",
|
42
|
+
paternal_grandmother: "PGM",
|
43
|
+
maternal_grandfather: "MGF",
|
44
|
+
maternal_grandmother: "MGM",
|
45
|
+
children: "C",
|
46
|
+
siblings: "S",
|
47
|
+
half_siblings: "HS",
|
48
|
+
paternal_half_siblings: "PHS",
|
49
|
+
maternal_half_siblings: "MHS",
|
50
|
+
grandchildren: "GC",
|
51
|
+
uncles_and_aunts: "U&A",
|
52
|
+
nieces_and_nephews: "N&N"
|
53
|
+
})
|
54
|
+
end
|
24
55
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Genealogy
|
2
|
+
# Module CurrentSpouseMethods provides methods to manage and query current spouse. It's included by the genealogy enabled AR model
|
3
|
+
module CurrentSpouseMethods
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
include Constants
|
7
|
+
|
8
|
+
# add current spouse updating receiver and argument individuals foreign_key in a transaction
|
9
|
+
# @param [Object] spouse
|
10
|
+
# @return [Boolean]
|
11
|
+
def add_current_spouse(spouse)
|
12
|
+
|
13
|
+
raise_unless_current_spouse_enabled
|
14
|
+
check_incompatible_relationship(:current_spouse,spouse)
|
15
|
+
|
16
|
+
if gclass.perform_validation_enabled
|
17
|
+
self.current_spouse = spouse
|
18
|
+
spouse.current_spouse = self
|
19
|
+
transaction do
|
20
|
+
spouse.save!
|
21
|
+
save!
|
22
|
+
end
|
23
|
+
else
|
24
|
+
transaction do
|
25
|
+
self.update_attribute(:current_spouse,spouse)
|
26
|
+
spouse.update_attribute(:current_spouse,self)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
# remove current spouse resetting receiver and argument individuals foreign_key in a transaction
|
33
|
+
# @return [Boolean]
|
34
|
+
def remove_current_spouse
|
35
|
+
raise_unless_current_spouse_enabled
|
36
|
+
if gclass.perform_validation_enabled
|
37
|
+
ex_current_spouse = current_spouse
|
38
|
+
current_spouse.current_spouse = nil
|
39
|
+
self.current_spouse = nil
|
40
|
+
transaction do
|
41
|
+
ex_current_spouse.save!
|
42
|
+
save!
|
43
|
+
end
|
44
|
+
else
|
45
|
+
transaction do
|
46
|
+
current_spouse.update_attribute(:current_spouse,nil)
|
47
|
+
self.update_attribute(:current_spouse,nil)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# list of individual who cannot be current spouse
|
53
|
+
# @return [Array]
|
54
|
+
def ineligible_current_spouses
|
55
|
+
raise_unless_current_spouse_enabled
|
56
|
+
self.gclass.send(ssex.to_s.pluralize)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def raise_unless_current_spouse_enabled
|
62
|
+
raise FeatureNotEnabled, "Spouse tracking not enabled. Enable it with option 'current_spouse_enabled: true' for has_parents method}" unless self.class.current_spouse_enabled
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
data/lib/genealogy/exceptions.rb
CHANGED
@@ -1,12 +1,9 @@
|
|
1
1
|
module Genealogy
|
2
|
-
class
|
3
|
-
class
|
4
|
-
class
|
5
|
-
class
|
6
|
-
|
7
|
-
class WrongSexException < RuntimeError; end
|
8
|
-
class IncompatibleRelationshipException < RuntimeError
|
9
|
-
def initialize(msg = "Trying to create a relationship incopatible with the the existing ones")
|
2
|
+
class LineageGapException < StandardError; end
|
3
|
+
class FeatureNotEnabled < StandardError; end
|
4
|
+
class SexError < StandardError; end
|
5
|
+
class IncompatibleRelationshipException < StandardError
|
6
|
+
def initialize(msg = "Trying to create an incompatible relationship")
|
10
7
|
super(msg)
|
11
8
|
end
|
12
9
|
end
|
data/lib/genealogy/genealogy.rb
CHANGED
@@ -1,82 +1,119 @@
|
|
1
1
|
module Genealogy
|
2
2
|
|
3
|
-
|
3
|
+
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
include Genealogy::Constants
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
# gives to ActiveRecord model geneaogy capabilities. Modules UtilMethods QueryMethods IneligibleMethods AlterMethods and SpouseMethods are included
|
10
|
+
# @param [Hash] options
|
11
|
+
# @option options [Boolean] current_spouse (false) specifies whether to track or not individual's current spouse
|
12
|
+
# @option options [Boolean] perform_validation (true) specifies whether to perform validation or not while altering pedigree that is before updating relatives external keys
|
13
|
+
# @option options [Boolean, Symbol] ineligibility (:pedigree) specifies ineligibility setting. If `false` ineligibility checks will be disabled and you can assign, as a relative, any individuals you want.
|
14
|
+
# This can be dangerous because you can build nosense loop (in terms of pedigree). If pass one of symbols `:pedigree` or `:pedigree_and_dates` ineligibility checks will be enabled.
|
15
|
+
# More specifically with `:pedigree` (or `true`) checks will be based on pedigree topography, i.e., ineligible children will include ancestors. With `:pedigree_and_dates` check will also be based on
|
16
|
+
# procreation ages (min and max, male and female) and life expectancy (male and female), i.e. an individual born 200 years before is an ineligible mother
|
17
|
+
# @option options [Hash] limit_ages (min_male_procreation_age:12, max_male_procreation_age:75, min_female_procreation_age:9, max_female_procreation_age:50, max_male_life_expectancy:110, max_female_life_expectancy:110)
|
18
|
+
# specifies one or more limit ages different than defaults
|
19
|
+
# @option options [Hash] column_names (sex:'sex', father_id:'father_id', mother_id:'mother_id', current_spouse_id:'current_spouse_id', birth_date:'birth_date', death_date:'death_date') specifies column names to map database individual table
|
20
|
+
# @option options [Array] sex_values (['M','F']) specifies values used in database sex column
|
21
|
+
# @return [void]
|
22
|
+
def has_parents options = {}
|
23
|
+
|
24
|
+
check_options(options)
|
25
|
+
|
26
|
+
# keep track of the original extend class to prevent wrong scopes in query method in case of STI
|
27
|
+
class_attribute :gclass, instance_writer: false
|
28
|
+
self.gclass = self
|
29
|
+
|
30
|
+
class_attribute :ineligibility_level, instance_accessor: false
|
31
|
+
self.ineligibility_level = case options[:ineligibility]
|
32
|
+
when :pedigree
|
33
|
+
PEDIGREE
|
34
|
+
when true
|
35
|
+
PEDIGREE
|
36
|
+
when :pedigree_and_dates
|
37
|
+
PEDIGREE_AND_DATES
|
38
|
+
when false
|
39
|
+
OFF
|
40
|
+
when nil
|
41
|
+
PEDIGREE
|
42
|
+
else
|
43
|
+
raise ArgumentError, "ineligibility option must be one among :pedigree, :pedigree_and_dates or false"
|
11
44
|
end
|
12
|
-
|
13
|
-
|
45
|
+
|
46
|
+
## limit_ages
|
47
|
+
if ineligibility_level >= PEDIGREE_AND_DATES
|
48
|
+
DEFAULTS[:limit_ages].each do |age,v|
|
49
|
+
class_attribute age, instance_accessor: false
|
50
|
+
self.send("#{age}=", options[:limit_ages].try(:[],age) || v)
|
51
|
+
end
|
14
52
|
end
|
15
|
-
end
|
16
53
|
|
17
|
-
|
54
|
+
[:current_spouse, :perform_validation].each do |opt|
|
55
|
+
ca = "#{opt}_enabled"
|
56
|
+
class_attribute ca, instance_accessor: false
|
57
|
+
self.send "#{ca}=", options.key?(opt) ? options[opt] : DEFAULTS[opt]
|
58
|
+
end
|
18
59
|
|
19
|
-
def has_parents options = {}
|
20
60
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
61
|
+
# column names class attributes
|
62
|
+
DEFAULTS[:column_names].merge(options[:column_names]).each do |k,v|
|
63
|
+
class_attribute_name = "#{k}_column"
|
64
|
+
class_attribute class_attribute_name, instance_accessor: false
|
65
|
+
self.send("#{class_attribute_name}=", v)
|
66
|
+
alias_attribute k, v unless k == v.to_sym
|
25
67
|
end
|
26
|
-
end
|
27
68
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
## sex
|
38
|
-
# class attributes
|
39
|
-
class_attribute :sex_column, :sex_values, :sex_male_value, :sex_female_value, :birth_date_column, :death_date_column
|
40
|
-
self.sex_column = options[:sex_column] || 'sex'
|
41
|
-
self.sex_values = options[:sex_values] || ['M','F']
|
42
|
-
self.sex_male_value = self.sex_values.first
|
43
|
-
self.sex_female_value = self.sex_values.last
|
44
|
-
self.birth_date_column = options[:birth_date_column] || 'birth_date'
|
45
|
-
self.death_date_column = options[:death_date_column] || 'death_date'
|
46
|
-
# instance attribute
|
47
|
-
alias_attribute :sex, sex_column if self.sex_column != 'sex'
|
48
|
-
# validation
|
49
|
-
validates_presence_of sex_column
|
50
|
-
validates_format_of sex_column, :with => /[#{sex_values.join}]/
|
51
|
-
|
52
|
-
tracked_relatives.each do |key|
|
53
|
-
# class attribute where is stored the correspondig foreign_key column name
|
54
|
-
class_attribute_name = "#{key}_column"
|
55
|
-
foreign_key = "#{key}_id"
|
56
|
-
class_attribute class_attribute_name
|
57
|
-
self.send("#{class_attribute_name}=", options[class_attribute_name.to_sym] || foreign_key)
|
58
|
-
# self join associations
|
59
|
-
belongs_to key, class_name: self, foreign_key: foreign_key
|
60
|
-
end
|
69
|
+
## sex
|
70
|
+
class_attribute :sex_values, :sex_male_value, :sex_female_value, instance_accessor: false
|
71
|
+
self.sex_values = options[:sex_values] || DEFAULTS[:sex_values]
|
72
|
+
self.sex_male_value = self.sex_values.first
|
73
|
+
self.sex_female_value = self.sex_values.last
|
74
|
+
|
75
|
+
# validation
|
76
|
+
validates_presence_of :sex
|
77
|
+
validates_format_of :sex, with: /[#{sex_values.join}]/
|
61
78
|
|
62
|
-
|
63
|
-
|
79
|
+
tracked_relatives = [:father, :mother]
|
80
|
+
tracked_relatives << :current_spouse if current_spouse_enabled
|
81
|
+
tracked_relatives.each do |k|
|
82
|
+
belongs_to k, class_name: self, foreign_key: self.send("#{k}_id_column")
|
83
|
+
end
|
64
84
|
|
65
|
-
|
66
|
-
|
67
|
-
include Genealogy::AlterMethods
|
68
|
-
include Genealogy::SpouseMethods if current_spouse_enabled
|
85
|
+
has_many :children_as_father, class_name: self, foreign_key: self.father_id_column, dependent: :nullify
|
86
|
+
has_many :children_as_mother, class_name: self, foreign_key: self.mother_id_column, dependent: :nullify
|
69
87
|
|
70
|
-
|
88
|
+
include Genealogy::UtilMethods
|
89
|
+
include Genealogy::QueryMethods
|
90
|
+
include Genealogy::IneligibleMethods
|
91
|
+
include Genealogy::AlterMethods
|
92
|
+
include Genealogy::CurrentSpouseMethods
|
71
93
|
|
72
|
-
module MotherAssociationExtension
|
73
|
-
def with(father_id)
|
74
|
-
where(father_id: father_id)
|
75
94
|
end
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def check_options(options)
|
99
|
+
|
100
|
+
raise ArgumentError, "Hash expected, #{options.class} given." unless options.is_a? Hash
|
101
|
+
|
102
|
+
# column names
|
103
|
+
options[:column_names] ||= {}
|
104
|
+
raise ArgumentError, "Hash expected for :column_names option, #{options[:column_names].class} given." unless options[:column_names].is_a? Hash
|
105
|
+
|
106
|
+
# sex
|
107
|
+
if array = options[:sex_values]
|
108
|
+
raise ArgumentError, ":sex_values option must be an array of length 2: [:male_value, :female_value]" unless array.is_a?(Array) and array.size == 2
|
109
|
+
end
|
110
|
+
|
111
|
+
# booleans
|
112
|
+
options.slice(:perform_validation, :current_spouse).each do |k,v|
|
113
|
+
raise ArgumentError, "Boolean expected for #{k} option, #{v.class} given." unless !!v == v
|
114
|
+
end
|
80
115
|
end
|
116
|
+
|
81
117
|
end
|
118
|
+
|
82
119
|
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Genealogy
|
2
|
+
# Module IneligibleMethods provides methods to run genealogy queries to retrive groups of individuals who cannot be relatives according to provided role.
|
3
|
+
# It's included by the genealogy enabled AR model
|
4
|
+
module IneligibleMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
include Constants
|
8
|
+
|
9
|
+
# @!macro [attach] generate
|
10
|
+
# @method ineligible_$1s
|
11
|
+
# list of individual who cannot be $1
|
12
|
+
# @return [Array,NilClass] Return nil if $1 already assigned
|
13
|
+
def self.generate_method_ineligibles_parent(parent)
|
14
|
+
define_method "ineligible_#{parent}s" do
|
15
|
+
unless self.send(parent)
|
16
|
+
ineligibles = []
|
17
|
+
ineligibles |= descendants | [self] | gclass.send("#{OPPOSITESEX[PARENT2SEX[parent]]}s") if gclass.ineligibility_level >= PEDIGREE
|
18
|
+
if gclass.ineligibility_level >= PEDIGREE_AND_DATES and life_range
|
19
|
+
ineligibles |= (gclass.all - ineligibles).find_all do |indiv|
|
20
|
+
if indiv.life_range
|
21
|
+
if birth
|
22
|
+
!indiv.can_procreate_on?(birth)
|
23
|
+
else
|
24
|
+
!indiv.can_procreate_during?(life_range)
|
25
|
+
end
|
26
|
+
else
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
ineligibles
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
generate_method_ineligibles_parent(:father)
|
36
|
+
generate_method_ineligibles_parent(:mother)
|
37
|
+
|
38
|
+
|
39
|
+
# @!macro [attach] generate
|
40
|
+
# @method ineligible_$1_grand$2(grandparent)
|
41
|
+
# list of individual who cannot be $1 grand$2
|
42
|
+
# @return [Array,NilClass] Return nil if $$1 grand$2 already assigned
|
43
|
+
def self.generate_method_ineligible_grandparent(lineage,grandparent)
|
44
|
+
relationship = "#{lineage}_grand#{grandparent}"
|
45
|
+
define_method "ineligible_#{relationship}s" do
|
46
|
+
unless send(relationship)
|
47
|
+
parent = send(LINEAGE2PARENT[lineage])
|
48
|
+
ineligibles = []
|
49
|
+
ineligibles |= [parent].compact | descendants | siblings | send("#{lineage}_half_siblings") | [self] | gclass.send("#{OPPOSITESEX[PARENT2SEX[grandparent]]}s") if gclass.ineligibility_level >= PEDIGREE
|
50
|
+
if gclass.ineligibility_level >= PEDIGREE_AND_DATES
|
51
|
+
ineligibles |= if parent
|
52
|
+
parent.send("ineligible_#{grandparent}s")
|
53
|
+
else
|
54
|
+
(gclass.all - ineligibles).find_all do |indiv|
|
55
|
+
if indiv.life_range
|
56
|
+
!indiv.can_procreate_during?(send("#{LINEAGE2PARENT[lineage]}_birth_range"))
|
57
|
+
else
|
58
|
+
false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
ineligibles
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
generate_method_ineligible_grandparent(:paternal,:father)
|
69
|
+
generate_method_ineligible_grandparent(:paternal,:mother)
|
70
|
+
generate_method_ineligible_grandparent(:maternal,:father)
|
71
|
+
generate_method_ineligible_grandparent(:maternal,:mother)
|
72
|
+
|
73
|
+
# list of individual who cannot be children: ancestors, children, full siblings, theirself and all individuals with father or mother according to self's sex
|
74
|
+
# @return [Array]
|
75
|
+
def ineligible_children
|
76
|
+
ineligibles = []
|
77
|
+
ineligibles |= ancestors | children | siblings | [self] | gclass.all_with(SEX2PARENT[ssex]) if gclass.ineligibility_level >= PEDIGREE
|
78
|
+
if gclass.ineligibility_level >= PEDIGREE_AND_DATES and fertility_range
|
79
|
+
ineligibles |= (gclass.all - ineligibles).find_all{ |indiv| !can_procreate_during?(indiv.birth_range)}
|
80
|
+
end
|
81
|
+
ineligibles
|
82
|
+
end
|
83
|
+
|
84
|
+
# list of individual who cannot be full siblings: ancestors, descendants, siblings, theirself and all individuals with different father or mother
|
85
|
+
# @return [Array]
|
86
|
+
def ineligible_siblings
|
87
|
+
ineligibles = []
|
88
|
+
if gclass.ineligibility_level >= PEDIGREE
|
89
|
+
ineligibles |= ancestors | descendants | siblings | [self]
|
90
|
+
ineligibles |= (father ? gclass.all_with(:father).where.not(father_id: father) : [])
|
91
|
+
ineligibles |= (mother ? gclass.all_with(:mother).where.not(mother_id: mother) : [])
|
92
|
+
end
|
93
|
+
if gclass.ineligibility_level >= PEDIGREE_AND_DATES
|
94
|
+
# if a parent is present ineligible siblings are parent's ineligible children, otherwise try to estimate parent birth range.
|
95
|
+
# If it's possible ineligible siblings are all individuals whose life range overlaps parent birth range
|
96
|
+
[:father,:mother].each do |parent|
|
97
|
+
if p = send(parent)
|
98
|
+
ineligibles |= p.ineligible_children
|
99
|
+
elsif parent_fertility_range = send("#{parent}_fertility_range")
|
100
|
+
remainings = gclass.all - ineligibles
|
101
|
+
ineligibles |= remainings.find_all do |indiv|
|
102
|
+
if ibr = indiv.birth_range
|
103
|
+
!parent_fertility_range.overlaps? ibr
|
104
|
+
else
|
105
|
+
false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
else
|
111
|
+
end
|
112
|
+
ineligibles
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|