require 'ruby-fs-stack/fs_communicator' require 'ruby-fs-stack/fs_utils' module FamilytreeV2 # This method gets mixed into the FsCommunicator so that # you can make calls on the familytree_v2 module def familytree_v2 @familytree_v2_com ||= Communicator.new self # self at this point refers to the FsCommunicator instance end class Communicator Base = '/familytree/v2/' # ===params # fs_communicator: FsCommunicator instance def initialize(fs_communicator) @fs_communicator = fs_communicator end # ===params # id_or_ids should be a string of the persons identifier. For the 'me' person, use :me or 'me'. Can also accept an array of ID strings. # options accepts a hash of parameters as documented by the API. # For full parameter documentation, see DevNet[https://devnet.familysearch.org/docs/api-manual-reference-system/familytree-v2/r_api_family_tree_person_read_v2.html] # # ===Example # # communicator is an authenticated FsCommunicator object # # Request a person with no assertions, only the version. # p = communicator.familytree_v2.person :me, :names => 'none', :genders => 'none', :events => 'none' # # p.version # => '90194378772' # p.id # => 'KW3B-NNM' # # ===Blocks # A block is available for this method, so that you can register a callback of sorts # for when a read has been completed. # # For example, if I were to send 500 person IDs to # this method and the current person.max.ids was 10, 50 person reads would be performed # to gather all of the records. This could take some time, so you may want to present a # progress of sorts to the end-user. Using a block enables this to be done. # # ids = [] #array of 500 ids # running_total = 0 # persons = communicator.familytree_v2.person ids, :parents => 'summary' do |people| # running_total += ps.size # puts running_total # end # # # If you are only requesting a single individual, the block will be passed a single person record # person = communicator.familytree_v2.person :me do |p| # puts p.id # end # # ===500 Errors # Occasionally, the FamilySearch API returns 500 errors when reading a person record. # This is problematic when you are requesting 100+ person records from the person read # because it may happen towards the end of your entire batch and it causes the entire # read to fail. Rather than fail, it does the following. # # If you are requesting multiple IDs and a 500 is thrown when requesting 10 records, it is # possible that only 1 of the 10 person records actually caused the problem, so this will # re-request the records individually. # # If a single record throws a 500, then the response will be an empty person record with only # an ID. # def person(id_or_ids, options = {}, &block) if id_or_ids.kind_of? Array return multi_person_read(id_or_ids,options,&block) else return single_person_read(id_or_ids.to_s,options,&block) end end def save_person(person) if person.id.nil? url = Base + 'person' else url = Base + 'person/' + person.id end familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.new familytree.persons = [person] response = @fs_communicator.post(url,familytree.to_json) res_familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.from_json JSON.parse(response.body) person = res_familytree.persons.first return person end # ====Params # search_params - A hash of search parameters matching API doc def search(search_params) url = Base + 'search' url += add_querystring(search_params) response = @fs_communicator.get(url) familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.from_json JSON.parse(response.body) # require 'pp' # pp familytree familytree.searches[0] end # ====Params # id_or_hash - Either an ID or a hash of match parameters matching API doc # hash - if the first parameter is an ID, then this will contain the hash # of match parameters. def match(id_or_hash, hash={}) url = Base + 'match' if id_or_hash.kind_of? String id = id_or_hash url += "/#{id}" params_hash = hash elsif id_or_hash.kind_of? Hash id = nil params_hash = id_or_hash else raise ArgumentError, "first parameter must be a kind of String or Hash" end url += add_querystring(params_hash) #"?" + FsUtils.querystring_from_hash(params_hash) unless params_hash.empty? response = @fs_communicator.get(url) familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.from_json JSON.parse(response.body) # require 'pp' # pp familytree familytree.matches[0] end # ====Params # * base_id - The root person for creating the relationship # * options - Should include either :parent, :spouse, or :child. :lineage and :event is optional # # :lineage can be set to the following values: # * 'Biological' # * 'Adoptive' # * 'Foster' # * 'Guardianship' # * 'Step' # * 'Other' # # :event should be a hash with the following values # ** :type - "Marriage", etc. (REQUIRED) # ** :place - "Utah, United States" (optional) # ** :date - "Nov 2009" # # :ordinance should be a hash with the following values # ** :type - "Sealing_to_Spouse", etc. (REQUIRED) # ** :place - "Utah, United States" (optional) # ** :date - "Nov 2009" # ** :temple - 'SLAKE' # # If the :lineage is set, the parent-child relationships will be written via a characteristic. # Otherwise, an exists assertion will be created to just establish the relationship. # ====Example # # communicator.familytree_v2.write_relationship 'KWQS-BBQ', :parent => 'KWQS-BBT', :lineage => 'Biological' # communicator.familytree_v2.write_relationship 'KWQS-BBQ', :parent => 'KWQS-BBT', :lineage => 'Adoptive' # communicator.familytree_v2.write_relationship 'KWQS-BBQ', :spouse => 'KWRT-BBZ', :event => {:type => 'Marriage', :date => '15 Aug 1987', :place => 'Utah, United States'} def write_relationship(base_id,options) relationship_type = get_relationship_type(options) with_id = options[relationship_type.to_sym] # Get the existing person/relationship or create a new person unless person = relationship(base_id,options.merge({:events => 'none'})) person = Org::Familysearch::Ws::Familytree::V2::Schema::Person.new person.id = base_id end # Add the relationship to the person with all of the correct options r_options = {:type => relationship_type, :with => with_id} r_options[:event] = options[:event] if options[:event] r_options[:ordinance] = options[:ordinance] if options[:ordinance] r_options[:lineage] = options[:lineage] if options[:lineage] person.create_relationship r_options # Create the payload familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.new familytree.persons = [person] # Get the most current related ID for the URI rels = person.relationships.get_relationships_of_type(r_options[:type]) rel = rels.find{|r|r.id == r_options[:with] || r.requestedId == r_options[:with]} related_id = rel.id url = "#{Base}person/#{base_id}/#{relationship_type}/#{related_id}" # Post the response and return the resulting person/relationship record from response response = @fs_communicator.post(url,familytree.to_json) res_familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.from_json JSON.parse(response.body) person = res_familytree.persons.first return person end # ====Params # * base_id - The root person for creating the relationship # * options - Should include either :parent, :spouse, or :child. :lineage and :event is optional. # Other Relationship Read parameters may be included in options such as :events => 'all', # :characteristics => 'all', etc. # # If the :lineage is set, the parent-child relationships will be written via a characteristic. # Otherwise, an exists assertion will be created to just establish the relationship. # ====Example # # communicator.familytree_v2.relationship 'KWQS-BBQ', :parent => 'KWQS-BBT' # communicator.familytree_v2.relationship 'KWQS-BBQ', :parent => 'KWQS-BBT' def relationship(base_id,options) begin r_type = get_relationship_type(options) with_id = options[r_type.to_sym] url = "#{Base}person/#{base_id}/#{r_type}/#{with_id}" options.reject!{|k,v| k.to_s == 'spouse'} url += add_querystring(options) res = @fs_communicator.get(url) familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.from_json JSON.parse(res.body) person = familytree.persons.find{|p|p.requestedId == base_id} return person rescue RubyFsStack::NotFound return nil end end # Writes a note attached to the value ID of the specific person or relationship. # # ====Params # * options - Options for the note including the following: # * :personId - the person ID if attaching to a person assertion. # * :spouseIds - an Array of spouse IDs if creating a note attached to a spouse # relationship assertion. # * :parentIds - an Array of parent IDs if creating a note attached to a parent # relationship assertion. If creating a note for a child-parent or parent-child # relationship, you will need only one parent ID in the array along with a :childId option. # * :childId - a child ID. # * :text - the text of the note (required). # * :assertionId - the valueId of the assertion you are attaching this note to. # def write_note(options) url = "#{Base}note" note = Org::Familysearch::Ws::Familytree::V2::Schema::Note.new note.build(options) familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.new familytree.notes = [note] res = @fs_communicator.post(url,familytree.to_json) familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.from_json JSON.parse(res.body) return familytree.notes.first end # Combines person into a new person # # ====Params # * person_array - an array of person IDs. def combine(person_array) url = Base + 'person' version_persons = self.person person_array, :genders => 'none', :events => 'none', :names => 'none' combine_person = Org::Familysearch::Ws::Familytree::V2::Schema::Person.new combine_person.create_combine(version_persons) familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.new familytree.persons = [combine_person] res = @fs_communicator.post(url,familytree.to_json) familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.from_json JSON.parse(res.body) return familytree.persons[0] end def pedigree(id_or_ids, options = {}) if id_or_ids.kind_of? Array multiple_ids = true url = Base + 'pedigree/' + id_or_ids.join(',') else multiple_ids = false id = id_or_ids.to_s if id == 'me' url = Base + 'pedigree' else url = Base + 'pedigree/' + id end end url += add_querystring(options) # url += add_querystring(options) response = @fs_communicator.get(url) familytree = parse_response(response) if multiple_ids return familytree.pedigrees else pedigree = familytree.pedigrees.find{|p| p.requestedId == id } pedigree ||= familytree.pedigrees.first if id == 'me' return pedigree end end # ===params # id_or_ids should be a string of the persons identifier. For the 'me' person, use :me or 'me'. Can also accept an array of ID strings. # options accepts a hash of parameters as documented by the API. # For full parameter documentation, see DevNet[https://devnet.familysearch.org/docs/api-manual-reference-system/familytree-v2/r_api_family_tree_person_read_v2.html] # # ===Example # # communicator is an authenticated FsCommunicator object # # Request a person with no assertions, only the version. # p = communicator.familytree_v2.person :me, :names => 'none', :genders => 'none', :events => 'none' # # p.version # => '90194378772' # p.id # => 'KW3B-NNM' def contributor(id_or_ids) if id_or_ids.kind_of? Array multiple_ids = true url = Base + 'contributor/' + id_or_ids.join(',') props = properties() if id_or_ids.size > props['contributor.max.ids'] contributors = [] id_or_ids.each_slice(props['contributor.max.ids']) do |ids_slice| contributors = contributors + contributor(ids_slice) end return contributors end else multiple_ids = false id = id_or_ids.to_s if id == 'me' url = Base + 'contributor' else url = Base + 'contributor/' + id end end response = @fs_communicator.get(url) familytree = parse_response(response) if multiple_ids return familytree.contributors else return familytree.contributors.first end end def properties if @properties_hash return @properties_hash else url = Base + 'properties' response = @fs_communicator.get(url) familytree = parse_response(response) @properties_hash = {} familytree.properties.each do |prop| @properties_hash[prop.name] = prop.value.to_i end return @properties_hash end end private def multi_person_read(ids,options,&block) url = Base + 'person/' + ids.join(',') props = properties() if ids.size > props['person.max.ids'] persons = [] ids.each_slice(props['person.max.ids']) do |ids_slice| persons = persons + person(ids_slice,options,&block) end return persons end url += add_querystring(options) begin response = @fs_communicator.get(url) familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.from_json JSON.parse(response.body) rescue RubyFsStack::ServerError => e persons = [] ids.each do |id| persons << person(id,options) end return persons end yield(familytree.persons) if block return familytree.persons end def single_person_read(id,options,&block) if id == 'me' url = Base + 'person' else url = Base + 'person/' + id end url += add_querystring(options) begin response = @fs_communicator.get(url) familytree = Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.from_json JSON.parse(response.body) rescue RubyFsStack::ServerError => e person = Org::Familysearch::Ws::Familytree::V2::Schema::Person.new person.id = id person.requestedId = id return person end person = familytree.persons.find{|p| p.requestedId == id } person ||= familytree.persons.first if id == 'me' yield(person) if block return person end def parse_response(response) Org::Familysearch::Ws::Familytree::V2::Schema::FamilyTree.from_json JSON.parse(response.body) end #options will either have a :parent, :child, or :spouse key. We need to find which one def get_relationship_type(options) keys = options.keys.collect{|k|k.to_s} key = keys.find{|k| ['parent','child','spouse'].include? k} key end def add_querystring(options) params = options.reject{|k,v| ['parent','child','lineage','event'].include? k.to_s } (params.empty?) ? '' : "?" + FsUtils.querystring_from_hash(params) end end end # Mix in the module so that the fs_familytree_v1 can be called class FsCommunicator include FamilytreeV2 end