genealogy 0.0.2.beta1

Sign up to get free protection for your applications and to get access to all the features.
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: []