genealogy 0.0.2.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/genealogy.rb ADDED
@@ -0,0 +1,10 @@
1
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'genealogy/constants')
2
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'genealogy/exceptions')
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'genealogy/query_methods')
4
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'genealogy/linking_methods')
5
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'genealogy/spouse_methods')
6
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'genealogy/genealogy')
7
+
8
+ ActiveRecord::Base.send :extend, Genealogy
9
+
10
+
@@ -0,0 +1,3 @@
1
+ module Genealogy
2
+ LINEAGE_NAME = { :father => :paternal, :mother => :maternal }
3
+ end
@@ -0,0 +1,13 @@
1
+ module Genealogy
2
+ class WrongArgumentException < RuntimeError; end
3
+ class WrongOptionException < RuntimeError; end
4
+ class WrongOptionValueException < RuntimeError; end
5
+ class LineageGapException < RuntimeError; end
6
+ class IncompatibleObjectException < RuntimeError; end
7
+ class WrongSexException < RuntimeError; end
8
+ class IncompatibleRelationshipException < RuntimeError
9
+ def initialize(msg = "Trying to create a relationship incopatible with the the existing ones")
10
+ super(msg)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,67 @@
1
+ module Genealogy
2
+
3
+ def check_options(options, admitted_keys)
4
+
5
+ raise WrongArgumentException, "first argument must be in a hash." unless options.is_a? Hash
6
+ raise WrongArgumentException, "seconf argument must be in an array." unless admitted_keys.is_a? Array
7
+
8
+ options.each do |key, value|
9
+ unless admitted_keys.include? key
10
+ raise WrongOptionException.new("Unknown option: #{key.inspect} => #{value.inspect}.")
11
+ end
12
+ if block_given?
13
+ yield key, value
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+
20
+ def has_parents options = {}
21
+
22
+ admitted_keys = [:sex_column, :sex_values, :father_column, :mother_column, :spouse_column, :spouse]
23
+ check_options(options, admitted_keys) do |key, value|
24
+ if key == :sex_values
25
+ raise WrongOptionException, ":sex_values option must be an array with two char: first for male sex symbol an last for female" unless value.is_a?(Array) and value.size == 2 and value.first.to_s.size == 1 and value.last.to_s.size == 1
26
+ end
27
+ end
28
+
29
+ class_attribute :spouse_enabled
30
+ self.spouse_enabled = options[:spouse].try(:==,true) || false
31
+ tracked_parents = [:father, :mother]
32
+ tracked_parents << :spouse if spouse_enabled
33
+
34
+ ## sex
35
+ # class attributes
36
+ class_attribute :sex_column, :sex_values, :sex_male_value, :sex_female_value
37
+ self.sex_column = options[:sex_column] || :sex
38
+ self.sex_values = (options[:sex_values] and options[:sex_values].to_a.map(&:to_s)) || ['M','F']
39
+ self.sex_male_value = self.sex_values.first
40
+ self.sex_female_value = self.sex_values.last
41
+ # instance attribute
42
+ alias_attribute :sex, sex_column if self.sex_column != :sex
43
+ # validation
44
+ validates_presence_of sex_column
45
+ validates_format_of sex_column, :with => /[#{sex_values.join}]/
46
+
47
+ ## relatives associations
48
+ tracked_parents.each do |key|
49
+ # class attribute where is stored the correspondig foreign_key column name
50
+ class_attribute_name = "#{key}_column"
51
+ foreign_key = "#{key}_id"
52
+ class_attribute class_attribute_name
53
+ self.send("#{class_attribute_name}=", options[class_attribute_name.to_sym] || foreign_key)
54
+
55
+ # self join association
56
+ attr_accessible foreign_key
57
+ belongs_to key, class_name: self, foreign_key: foreign_key
58
+
59
+ end
60
+
61
+ # Include instance methods and class methods
62
+ include Genealogy::QueryMethods
63
+ include Genealogy::LinkingMethods
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,192 @@
1
+ module Genealogy
2
+ module LinkingMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ # parents
6
+ [:father, :mother].each do |parent|
7
+
8
+ # add method
9
+ define_method "add_#{parent}" do |relative|
10
+ unless relative.nil?
11
+ raise IncompatibleObjectException, "Linked objects must be instances of the same class: got #{relative.class} for #{self.class}" unless relative.is_a? self.class
12
+ incompatible_parents = self.offspring | self.siblings.to_a | [self]
13
+ raise IncompatibleRelationshipException, "#{relative} can't be #{parent} of #{self}" if incompatible_parents.include? relative
14
+ raise WrongSexException, "Can't add a #{relative.sex} #{parent}" unless (parent == :father and relative.is_male?) or (parent == :mother and relative.is_female?)
15
+ end
16
+ self.send("#{parent}=",relative)
17
+ save!
18
+ end
19
+
20
+ # remove method
21
+ define_method "remove_#{parent}" do
22
+ self.send("#{parent}=",nil)
23
+ save!
24
+ end
25
+
26
+ end
27
+
28
+ # add both
29
+ def add_parents(father,mother)
30
+ transaction do
31
+ add_father(father)
32
+ add_mother(mother)
33
+ end
34
+ end
35
+
36
+ # remove both
37
+ def remove_parents
38
+ transaction do
39
+ remove_father
40
+ remove_mother
41
+ end
42
+ end
43
+
44
+ # grandparents
45
+ [:father, :mother].each do |parent|
46
+ [:father, :mother].each do |grandparent|
47
+ # add one
48
+ define_method "add_#{Genealogy::LINEAGE_NAME[parent]}_grand#{grandparent}" do |relative|
49
+ raise IncompatibleRelationshipException, "#{self} can't be grand#{grandparent} of itself" if relative == self
50
+ raise_if_gap_on(parent)
51
+ send(parent).send("add_#{grandparent}",relative)
52
+ end
53
+ # remove one
54
+ define_method "remove_#{Genealogy::LINEAGE_NAME[parent]}_grand#{grandparent}" do
55
+ raise_if_gap_on(parent)
56
+ send(parent).send("remove_#{grandparent}")
57
+ end
58
+ end
59
+ end
60
+
61
+ [:father, :mother].each do |parent|
62
+ # add two by lineage
63
+ define_method "add_#{Genealogy::LINEAGE_NAME[parent]}_grandparents" do |grandfather,grandmother|
64
+ raise_if_gap_on(parent)
65
+ send(parent).send("add_parents",grandfather,grandmother)
66
+ end
67
+ # remove two by lineage
68
+ define_method "remove_#{Genealogy::LINEAGE_NAME[parent]}_grandparents" do
69
+ raise_if_gap_on(parent)
70
+ send(parent).send("remove_parents")
71
+ end
72
+ end
73
+
74
+ # add all
75
+ def add_grandparents(pgf,pgm,mgf,mgm)
76
+ transaction do
77
+ add_paternal_grandparents(pgf,pgm)
78
+ add_maternal_grandparents(mgf,mgm)
79
+ end
80
+ end
81
+
82
+ # remove all
83
+ def remove_grandparents
84
+ transaction do
85
+ remove_paternal_grandparents
86
+ remove_maternal_grandparents
87
+ end
88
+ end
89
+
90
+
91
+ ## siblings
92
+ def add_siblings(*args)
93
+ options = args.extract_options!
94
+ raise LineageGapException, "Can't add siblings if both parents are nil" unless father and mother
95
+ raise IncompatibleRelationshipException, "Can't add an ancestor as sibling" unless (ancestors.to_a & args).empty?
96
+ transaction do
97
+ args.each do |sib|
98
+ case options[:half]
99
+ when :father
100
+ sib.add_father(self.father)
101
+ sib.add_mother(options[:spouse]) if options[:spouse]
102
+ when :mother
103
+ sib.add_father(options[:spouse]) if options[:spouse]
104
+ sib.add_mother(self.mother)
105
+ when nil
106
+ sib.add_father(self.father)
107
+ sib.add_mother(self.mother)
108
+ else
109
+ raise WrongOptionValueException, "Admitted values for :half options are: :father, :mother or nil"
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def remove_siblings(options = {})
116
+ options.delete_if{ |key,value| key == :half and ![:father,:mother].include?(value) }
117
+
118
+ resulting_indivs = siblings(options)
119
+
120
+ transaction do
121
+
122
+ resulting_indivs.each do |sib|
123
+ case options[:half]
124
+ when :father
125
+ sib.remove_father
126
+ sib.remove_mother if options[:affect_spouse] == true
127
+ when :mother
128
+ sib.remove_father if options[:affect_spouse] == true
129
+ sib.remove_mother
130
+ when nil
131
+ sib.remove_parents
132
+ end
133
+
134
+ end
135
+ end
136
+
137
+ end
138
+
139
+ # offspring
140
+ def add_offspring(*args)
141
+ options = args.extract_options!
142
+
143
+ raise_if_sex_undefined
144
+
145
+ transaction do
146
+ args.each do |child|
147
+ case sex
148
+ when sex_male_value
149
+ child.add_father(self)
150
+ child.add_mother(options[:spouse]) if options[:spouse]
151
+ when sex_female_value
152
+ child.add_father(options[:spouse]) if options[:spouse]
153
+ child.add_mother(self)
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ def remove_offspring(options = {})
160
+
161
+ raise_if_sex_undefined
162
+
163
+ resulting_indivs = offspring(options)
164
+ transaction do
165
+ resulting_indivs.each do |child|
166
+ if options[:affect_spouse] == true
167
+ child.remove_parents
168
+ else
169
+ case sex
170
+ when sex_male_value
171
+ child.remove_father
172
+ when sex_female_value
173
+ child.remove_mother
174
+ end
175
+ end
176
+ end
177
+ end
178
+ resulting_indivs.empty? ? false : true
179
+ end
180
+
181
+ private
182
+
183
+ def raise_if_gap_on(relative)
184
+ raise LineageGapException, "#{self} doesn't have #{relative}" unless send(relative)
185
+ end
186
+
187
+ def raise_if_sex_undefined
188
+ raise WrongSexException, "Can't proceed if sex undefined for #{self}" unless is_male? or is_female?
189
+ end
190
+
191
+ end
192
+ end
@@ -0,0 +1,113 @@
1
+ module Genealogy
2
+ module QueryMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ # parents
6
+ def parents
7
+ if father or mother
8
+ [father,mother]
9
+ else
10
+ []
11
+ end
12
+ end
13
+
14
+ # grandparents
15
+ [:father, :mother].each do |parent|
16
+ [:father, :mother].each do |grandparent|
17
+
18
+ # get one
19
+ define_method "#{Genealogy::LINEAGE_NAME[parent]}_grand#{grandparent}" do
20
+ send(parent) && send(parent).send(grandparent)
21
+ end
22
+
23
+ end
24
+
25
+ # get two by lineage
26
+ define_method "#{Genealogy::LINEAGE_NAME[parent]}_grandparents" do
27
+ (send(parent) && send(parent).parents) || []
28
+ end
29
+
30
+ end
31
+
32
+ # get all
33
+ def grandparents
34
+ result = []
35
+ [:father, :mother].each do |parent|
36
+ [:father, :mother].each do |grandparent|
37
+ result << send("#{Genealogy::LINEAGE_NAME[parent]}_grand#{grandparent}")
38
+ end
39
+ end
40
+ result.compact! if result.all?{|gp| gp.nil? }
41
+ result
42
+ end
43
+
44
+ def offspring(options = {})
45
+
46
+ if spouse = options[:spouse]
47
+ raise WrongSexException, "Something wrong with spouse #{spouse} gender." if spouse.sex == sex
48
+ end
49
+ case sex
50
+ when sex_male_value
51
+ self.class.find_all_by_father_id(id, :conditions => (["mother_id == ?", spouse.id] if spouse) )
52
+ when sex_female_value
53
+ self.class.find_all_by_mother_id(id, :conditions => (["father_id == ?", spouse.id] if spouse) )
54
+ end
55
+ end
56
+
57
+ def siblings(options = {})
58
+ result = case options[:half]
59
+ when :father # common father
60
+ father.try(:offspring, options).to_a
61
+ when :mother # common mother
62
+ mother.try(:offspring, options).to_a
63
+ when nil # exluding half siblings
64
+ siblings(:half => :father) & siblings(:half => :mother)
65
+ when :only # only half siblings
66
+ siblings(:half => :include) - siblings
67
+ when :include # including half siblings
68
+ siblings(:half => :father) | siblings(:half => :mother)
69
+ else
70
+ raise WrongOptionValueException, "Admitted values for :half options are: :father, :mother, false, true or nil"
71
+ end
72
+ result - [self]
73
+ end
74
+
75
+ def half_siblings(options = {})
76
+ siblings(:half => :only)
77
+ # todo: inprove with option :father and :mother
78
+ end
79
+
80
+ def ancestors
81
+ result = []
82
+ remaining = parents.to_a.compact
83
+ until remaining.empty?
84
+ result << remaining.shift
85
+ remaining += result.last.parents.to_a.compact
86
+ end
87
+ result.uniq
88
+ end
89
+
90
+ def descendants
91
+ result = []
92
+ remaining = offspring.to_a.compact
93
+ until remaining.empty?
94
+ result << remaining.shift
95
+ remaining += result.last.offspring.to_a.compact
96
+ end
97
+ result.uniq
98
+ end
99
+
100
+
101
+ def is_female?
102
+ sex == sex_female_value
103
+ end
104
+
105
+ def is_male?
106
+ sex == sex_male_value
107
+ end
108
+
109
+ module ClassMethods
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,32 @@
1
+ module Genealogy
2
+ module SpouseMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ # add method
6
+ def add_spouse(obj)
7
+ raise IncompatibleObjectException, "Linked objects must be instances of the same class" unless obj.is_a? self.class
8
+ raise WrongSexException, "Can't add spouse with same sex" if self.sex == obj.sex
9
+ self.spouse = obj
10
+ obj.spouse = self
11
+ transaction do
12
+ obj.save!
13
+ save!
14
+ end
15
+ end
16
+
17
+ # remove method
18
+ def remove_spouse
19
+ transaction do
20
+ ex_spouse = spouse
21
+ spouse.spouse = nil
22
+ self.spouse = nil
23
+ ex_spouse.save!
24
+ save!
25
+ end
26
+ end
27
+
28
+ module ClassMethods
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module Genealogy
2
+ VERSION = "0.0.2.beta1"
3
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: genealogy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2.beta1
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - masciugo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: sqlite3
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: debugger
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: a description
111
+ email:
112
+ - masciugo@gmail.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - lib/genealogy.rb
118
+ - lib/genealogy/constants.rb
119
+ - lib/genealogy/exceptions.rb
120
+ - lib/genealogy/genealogy.rb
121
+ - lib/genealogy/linking_methods.rb
122
+ - lib/genealogy/query_methods.rb
123
+ - lib/genealogy/spouse_methods.rb
124
+ - lib/genealogy/version.rb
125
+ homepage: https://github.com//genealogy
126
+ licenses:
127
+ - MIT
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ! '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ none: false
140
+ requirements:
141
+ - - ! '>'
142
+ - !ruby/object:Gem::Version
143
+ version: 1.3.1
144
+ requirements: []
145
+ rubyforge_project: ! '[none]'
146
+ rubygems_version: 1.8.24
147
+ signing_key:
148
+ specification_version: 3
149
+ summary: Organise ActiveRecord models into a genealogical tree structure
150
+ test_files: []