seven1m-onebody-updateagent 0.1.0
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/README.markdown +77 -0
- data/bin/update_onebody +68 -0
- data/example.yml +20 -0
- data/example_using_converter.yml +20 -0
- data/lib/onebody-updateagent.rb +19 -0
- data/lib/updateagent/converters/acs_converter.rb +84 -0
- data/lib/updateagent/hash_extensions.rb +21 -0
- data/lib/updateagent/resources.rb +8 -0
- data/lib/updateagent/schema.rb +15 -0
- data/lib/updateagent/updateagent.rb +160 -0
- data/lib/updateagent/updaters/family_updater.rb +6 -0
- data/lib/updateagent/updaters/people_updater.rb +72 -0
- metadata +90 -0
data/README.markdown
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
OneBody UpdateAgent
|
2
|
+
===================
|
3
|
+
|
4
|
+
Ruby gem that pushes data from a membership data source to a remote OneBody instance via the REST API.
|
5
|
+
|
6
|
+
Download and Install
|
7
|
+
--------------------
|
8
|
+
|
9
|
+
Run the following command ("sudo" may be required in some environments):
|
10
|
+
|
11
|
+
gem install seven1m-onebody-updateagent -s http://gems.github.com
|
12
|
+
|
13
|
+
Configuration
|
14
|
+
-------------
|
15
|
+
|
16
|
+
1. Run `update_onebody` at a terminal, then take note of where the example.yml config file resides.
|
17
|
+
2. Copy the example config file to a convenient location and edit appropriately.
|
18
|
+
|
19
|
+
Your "site" address will probably be something like "http://example.com" or
|
20
|
+
"http://yoursite.beonebody.com".
|
21
|
+
|
22
|
+
You can get your user api key from OneBody (you must be a super user) by running the following
|
23
|
+
command (on the server):
|
24
|
+
|
25
|
+
cd /path/to/onebody
|
26
|
+
rake onebody:api:key EMAIL=admin@example.com
|
27
|
+
|
28
|
+
If your instance is hosted at <http://beonebody.com>, you may email <support@beonebody.com>.
|
29
|
+
|
30
|
+
Preparation
|
31
|
+
-----------
|
32
|
+
|
33
|
+
Using your membership management software, reporting solution, database utility, custom script, etc.,
|
34
|
+
export your people and family data to a single comma separated values (CSV) file, e.g. people.csv.
|
35
|
+
|
36
|
+
Duplicate family data should be present for each member of the same family.
|
37
|
+
|
38
|
+
The first row of the file is the attribute headings, and must match the attributes available:
|
39
|
+
* http://github.com/seven1m/onebody/tree/master/app/models/person.rb
|
40
|
+
* http://github.com/seven1m/onebody/tree/master/app/models/family.rb (prefix each with "family_")
|
41
|
+
|
42
|
+
Not all attributes are required.
|
43
|
+
|
44
|
+
Optionally, if you have shell access to your hosted instance of OneBody, you can run
|
45
|
+
`rake onebody:export:people:csv` to export the current OneBody data (if you have any records in
|
46
|
+
the OneBody database) as a starting point.
|
47
|
+
|
48
|
+
Use the following attributes to track the identity/foreign keys from your existing membership
|
49
|
+
management database. Do *not* include "id" and "family_id" columns.
|
50
|
+
* legacy\_id
|
51
|
+
* legacy\_family\_id
|
52
|
+
|
53
|
+
Converters
|
54
|
+
----------
|
55
|
+
|
56
|
+
As of this writing, one Church Management System (ChMS) is supported via a Converter. The
|
57
|
+
Converter translates field names and data into the formats and locations expected by OneBody.
|
58
|
+
|
59
|
+
To use a converter, you must specify it in your config file. The example config file has these
|
60
|
+
settings disabled; simply remove the pound sign at the beginning of each line to enable the
|
61
|
+
use of the converter. Specify the name and any additional settings.
|
62
|
+
|
63
|
+
Usage
|
64
|
+
-----
|
65
|
+
|
66
|
+
To run UpdateAgent:
|
67
|
+
|
68
|
+
update_onebody -c path/to/config.yml path/to/people.yml
|
69
|
+
|
70
|
+
If you plan to scheduled UpdateAgent to run periodically without human intervention,
|
71
|
+
you'll want to at least use the `-y` switch, which assumes *yes* to any questions:
|
72
|
+
|
73
|
+
update_onebody -c path/to/config.yml -y path/to/people.yml
|
74
|
+
|
75
|
+
You may also log all output:
|
76
|
+
|
77
|
+
update_onebody -c path/to/config.yml -y -l path/to/updateagent.log path/to/people.yml
|
data/bin/update_onebody
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
options = {:confirm => true, :force => false}
|
7
|
+
opt_parser = OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: ruby updateagent.rb -c path/to/config.yml [options] path/to/people.csv"
|
9
|
+
opts.on("-y", "--no-confirm", "Assume 'yes' to any questions") do |v|
|
10
|
+
options[:confirm] = false
|
11
|
+
end
|
12
|
+
opts.on("-l", "--log LOGFILE", "Output to log rather than stdout") do |log|
|
13
|
+
$stdout = $stderr = File.open(log, 'a')
|
14
|
+
end
|
15
|
+
opts.on("-f", "--force", "Force update all records") do |f|
|
16
|
+
options[:force] = true
|
17
|
+
end
|
18
|
+
opts.on("-c", "--config-file PATH", "Path to configuration file") do |c|
|
19
|
+
options[:config_file] = c
|
20
|
+
config = YAML::load(File.read(c))
|
21
|
+
SITE = config['site']
|
22
|
+
USER_EMAIL = config['user_email']
|
23
|
+
USER_KEY = config['user_key']
|
24
|
+
if c = config['converter']
|
25
|
+
USE_CONVERTER = c['name']
|
26
|
+
CONVERTER_CONFIG = c
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
opt_parser.parse!
|
31
|
+
|
32
|
+
unless options[:config_file]
|
33
|
+
puts opt_parser.help
|
34
|
+
puts
|
35
|
+
puts 'You must specify a config file containing site and login info.'
|
36
|
+
puts "See #{File.expand_path(File.dirname(File.dirname(__FILE__)))}/example.yml"
|
37
|
+
puts
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
|
41
|
+
if ARGV[0] # path/to/people.csv
|
42
|
+
require 'onebody-updateagent'
|
43
|
+
puts "Update Agent running at #{Time.now.strftime('%m/%d/%Y %I:%M %p')}"
|
44
|
+
agent = PeopleUpdater.new(ARGV[0])
|
45
|
+
puts "comparing records..."
|
46
|
+
agent.compare(options[:force])
|
47
|
+
if agent.has_work?
|
48
|
+
if options[:confirm]
|
49
|
+
case ask("#{agent.create.length + agent.update.length} record(s) to push. Continue? (Yes, No, Review) ") { |q| q.in = %w(yes no review y n r) }
|
50
|
+
when 'review', 'r'
|
51
|
+
agent.present
|
52
|
+
unless agent.confirm
|
53
|
+
puts "Canceled by user\n"
|
54
|
+
exit
|
55
|
+
end
|
56
|
+
when 'no', 'n'
|
57
|
+
puts "Canceled by user\n"
|
58
|
+
exit
|
59
|
+
end
|
60
|
+
end
|
61
|
+
agent.push
|
62
|
+
puts "Completed at #{Time.now.strftime('%m/%d/%Y %I:%M %p')}\n\n"
|
63
|
+
else
|
64
|
+
puts "Nothing to push\n\n"
|
65
|
+
end
|
66
|
+
else
|
67
|
+
puts opt_parser.help
|
68
|
+
end
|
data/example.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
site: http://localhost:3000
|
2
|
+
user_email: admin@example.com
|
3
|
+
user_key: dafH2KIiAcnLEr5JxjmX2oveuczq0R6u7Ijd329DtjatgdYcKp
|
4
|
+
#converter:
|
5
|
+
# name: ACS
|
6
|
+
# visible:
|
7
|
+
# - Member
|
8
|
+
# - Visitor
|
9
|
+
# - Prospect
|
10
|
+
# - Attender
|
11
|
+
# - College
|
12
|
+
# - Other Child
|
13
|
+
# - Other Youth
|
14
|
+
# visible_on_printed_directory:
|
15
|
+
# - Member
|
16
|
+
# - Attender
|
17
|
+
# full_access:
|
18
|
+
# - Member
|
19
|
+
# - Attender
|
20
|
+
# - College
|
@@ -0,0 +1,20 @@
|
|
1
|
+
site: http://localhost:3000
|
2
|
+
user_email: admin@example.com
|
3
|
+
user_key: dafH2KIiAcnLEr5JxjmX2oveuczq0R6u7Ijd329DtjatgdYcKp
|
4
|
+
converter:
|
5
|
+
name: ACS
|
6
|
+
visible:
|
7
|
+
- Member
|
8
|
+
- Visitor
|
9
|
+
- Prospect
|
10
|
+
- Attender
|
11
|
+
- College
|
12
|
+
- Other Child
|
13
|
+
- Other Youth
|
14
|
+
visible_on_printed_directory:
|
15
|
+
- Member
|
16
|
+
- Attender
|
17
|
+
full_access:
|
18
|
+
- Member
|
19
|
+
- Attender
|
20
|
+
- College
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'fastercsv'
|
4
|
+
require 'highline/import'
|
5
|
+
require 'activeresource'
|
6
|
+
require 'digest/sha1'
|
7
|
+
|
8
|
+
HighLine.track_eof = false
|
9
|
+
|
10
|
+
require 'updateagent/resources'
|
11
|
+
require 'updateagent/schema'
|
12
|
+
require 'updateagent/hash_extensions'
|
13
|
+
require 'updateagent/updateagent'
|
14
|
+
require 'updateagent/updaters/people_updater'
|
15
|
+
require 'updateagent/updaters/family_updater'
|
16
|
+
|
17
|
+
if defined?(USE_CONVERTER)
|
18
|
+
require "updateagent/converters/#{USE_CONVERTER.downcase}_converter"
|
19
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class ACSConverter
|
2
|
+
|
3
|
+
def straight
|
4
|
+
{
|
5
|
+
"FamilyNumber" => "legacy_family_id",
|
6
|
+
"IndividualNumber" => "sequence",
|
7
|
+
"LastName" => "last_name",
|
8
|
+
"Suffix" => "suffix",
|
9
|
+
"Address1" => "family_address1",
|
10
|
+
"Address2" => "family_address2",
|
11
|
+
"City" => "family_city",
|
12
|
+
"State" => "family_state",
|
13
|
+
"ZIPCode" => "family_zip",
|
14
|
+
"DateOfBirth" => "birthday",
|
15
|
+
"HomeEmailAddr" => "email",
|
16
|
+
"FamilyName" => "family_name",
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def convert(records)
|
21
|
+
create_family_names(records)
|
22
|
+
records.map do |record|
|
23
|
+
convert_record(record)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def convert_record(record)
|
28
|
+
returning({}) do |new_record|
|
29
|
+
record.each do |key, value|
|
30
|
+
if new_key = straight[key]
|
31
|
+
new_record[new_key] = value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
new_record['legacy_id'] = new_record['legacy_family_id'] + new_record['sequence']
|
35
|
+
new_record['first_name'] = get_first_name(record)
|
36
|
+
new_record['gender'] = record['FamilyPosition'] == 'Child' ? {'Male' => 'Boy', 'Female' => 'Girl'}[record['Gender']] : record['Gender']
|
37
|
+
new_record['family_home_phone'] = get_phone(record, 'Home')
|
38
|
+
new_record['work_phone'] = get_phone(record, 'Business')
|
39
|
+
new_record['mobile_phone'] = get_phone(record, 'Cell')
|
40
|
+
new_record['fax'] = get_phone(record, 'FAX')
|
41
|
+
new_record['family_last_name'] = new_record['last_name']
|
42
|
+
new_record['family_name'] = @family_names[new_record['legacy_family_id']]
|
43
|
+
new_record['visible_to_everyone'] = CONVERTER_CONFIG['visible'].include?(record['MemberStatus'])
|
44
|
+
new_record['visible_on_printed_directory'] = CONVERTER_CONFIG['visible_on_printed_directory'].include?(record['MemberStatus'])
|
45
|
+
new_record['full_access'] = CONVERTER_CONFIG['full_access'].include?(record['MemberStatus'])
|
46
|
+
new_record['email'] = record['HomeEmailAddr'].to_s.any? ? record['HomeEmailAddr'] : record['BusinessEmailAddr']
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_family_names(records)
|
51
|
+
# this could probably be less messy
|
52
|
+
@families = {}
|
53
|
+
records.each do |record|
|
54
|
+
@families[record['FamilyNumber']] ||= {}
|
55
|
+
@families[record['FamilyNumber']][record['FamilyPosition']] = record
|
56
|
+
end
|
57
|
+
@family_names = {}
|
58
|
+
@families.each do |family_number, family|
|
59
|
+
family['Head'] ||= family.delete('Spouse') || family.delete('Child')
|
60
|
+
if family['Head']
|
61
|
+
if family['Spouse']
|
62
|
+
@family_names[family_number] = "#{get_first_name(family['Head'])} & #{get_first_name(family['Spouse'])} #{family['Head']['LastName']}"
|
63
|
+
else
|
64
|
+
@family_names[family_number] = "#{get_first_name(family['Head'])} #{family['Head']['LastName']}"
|
65
|
+
end
|
66
|
+
else
|
67
|
+
@family_names[family_number] = "#{get_first_name(family['Other'])} #{family['Other']['LastName']}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_first_name(record)
|
73
|
+
record['GoesByName'].to_s.any? ? record['GoesByName'] : record['FirstName']
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_phone(record, type)
|
77
|
+
unless record[type + 'Unlisted'].to_s.downcase == 'true'
|
78
|
+
phone = record[type + 'Phone'].to_s.scan(/\d/).join
|
79
|
+
phone << ' ' + record[type + 'Extension'] if record[type + 'Extension'].to_s.any?
|
80
|
+
phone
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Hash
|
2
|
+
# creates a uniq sha1 digest of the hash's values
|
3
|
+
# should mirror similar code in OneBody's lib/db_tools.rb
|
4
|
+
def values_hash(*attrs)
|
5
|
+
attrs = keys.sort unless attrs.any?
|
6
|
+
attrs = attrs.first if attrs.first.is_a?(Array)
|
7
|
+
values = attrs.map do |attr|
|
8
|
+
value = self[attr.to_s]
|
9
|
+
if value.respond_to?(:strftime)
|
10
|
+
value.strftime('%Y-%m-%d %H:%M:%S')
|
11
|
+
elsif value == true
|
12
|
+
1
|
13
|
+
elsif value == false
|
14
|
+
0
|
15
|
+
else
|
16
|
+
value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
DEBUG ? values.join : Digest::SHA1.hexdigest(values.join)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Schema
|
2
|
+
def initialize(resource)
|
3
|
+
@schema = resource.get(:schema)
|
4
|
+
end
|
5
|
+
def type(t)
|
6
|
+
@schema.select { |c| c['type'] == t }.map { |c| c['name'] }.uniq
|
7
|
+
end
|
8
|
+
end
|
9
|
+
person_schema = Schema.new(Person)
|
10
|
+
family_schema = Schema.new(Family)
|
11
|
+
|
12
|
+
DATETIME_ATTRIBUTES = person_schema.type(:datetime) + family_schema.type(:datetime).map { |c| 'family_' + c }
|
13
|
+
BOOLEAN_ATTRIBUTES = person_schema.type(:boolean) + family_schema.type(:boolean).map { |c| 'family_' + c }
|
14
|
+
INTEGER_ATTRIBUTES = person_schema.type(:integer) + family_schema.type(:integer).map { |c| 'family_' + c }
|
15
|
+
IGNORE_ATTRIBUTES = %w(updated_at created_at family_updated_at family_latitude family_longitude)
|
@@ -0,0 +1,160 @@
|
|
1
|
+
MAX_HASHES_AT_A_TIME = 1000
|
2
|
+
MAX_TO_BATCH_AT_A_TIME = 25
|
3
|
+
|
4
|
+
DEBUG = false
|
5
|
+
|
6
|
+
# general class to handle comparing and pushing data to the remote end
|
7
|
+
class UpdateAgent
|
8
|
+
def initialize(data=nil)
|
9
|
+
@attributes = []
|
10
|
+
@data = []
|
11
|
+
@create = []
|
12
|
+
@update = []
|
13
|
+
if data
|
14
|
+
if data.is_a?(Array)
|
15
|
+
@data = data
|
16
|
+
@attributes = data.first.keys.sort
|
17
|
+
else
|
18
|
+
read_from_file(data)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
if invalid = @data.detect { |row| row['id'] }
|
22
|
+
puts "Error: one or more records contain an 'id' column."
|
23
|
+
puts "You must utilize 'legacy_id' rather than 'id' so that"
|
24
|
+
puts "identity and foreign keys are maintained from your"
|
25
|
+
puts "existing membership management database."
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# load data from csv file and do some type conversion for bools and dates
|
31
|
+
# first row must be attribute names
|
32
|
+
def read_from_file(filename)
|
33
|
+
csv = FasterCSV.open(filename, 'r')
|
34
|
+
@attributes = csv.shift
|
35
|
+
record_count = 0
|
36
|
+
@data = csv.map do |row|
|
37
|
+
hash = {}
|
38
|
+
row.each_with_index do |value, index|
|
39
|
+
key = @attributes[index]
|
40
|
+
next if IGNORE_ATTRIBUTES.include?(key)
|
41
|
+
if DATETIME_ATTRIBUTES.include?(key)
|
42
|
+
if value.blank?
|
43
|
+
value = nil
|
44
|
+
else
|
45
|
+
begin
|
46
|
+
value = DateTime.parse(value)
|
47
|
+
rescue ArgumentError
|
48
|
+
puts "Invalid date in #{filename} record #{index} (#{key}) - #{value}"
|
49
|
+
exit(1)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
elsif BOOLEAN_ATTRIBUTES.include?(key)
|
53
|
+
if value == '' or value == nil
|
54
|
+
value = nil
|
55
|
+
elsif %w(no false 0).include?(value.downcase)
|
56
|
+
value = false
|
57
|
+
else
|
58
|
+
value = true
|
59
|
+
end
|
60
|
+
elsif INTEGER_ATTRIBUTES.include?(key)
|
61
|
+
value = value.to_s != '' ? value.scan(/\d/).join.to_i : nil
|
62
|
+
end
|
63
|
+
hash[key] = value
|
64
|
+
end
|
65
|
+
record_count += 1
|
66
|
+
print "reading record #{record_count}\r"
|
67
|
+
hash
|
68
|
+
end
|
69
|
+
puts
|
70
|
+
@attributes.reject! { |a| IGNORE_ATTRIBUTES.include?(a) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def ids
|
74
|
+
@data.map { |r| r['id'] }.compact
|
75
|
+
end
|
76
|
+
|
77
|
+
def legacy_ids
|
78
|
+
@data.map { |r| r['legacy_id'] }.compact
|
79
|
+
end
|
80
|
+
|
81
|
+
def compare(force=false)
|
82
|
+
compare_hashes(legacy_ids, force)
|
83
|
+
end
|
84
|
+
|
85
|
+
def has_work?
|
86
|
+
(@create + @update).any?
|
87
|
+
end
|
88
|
+
|
89
|
+
def present
|
90
|
+
puts "The following #{resource.name.downcase} records will be pushed..."
|
91
|
+
puts 'legacy id name'
|
92
|
+
puts '---------- -------------------------------------'
|
93
|
+
@create.each { |r| present_record(r, true) }
|
94
|
+
@update.each { |r| present_record(r) }
|
95
|
+
puts
|
96
|
+
end
|
97
|
+
|
98
|
+
def present_record(row, new=false)
|
99
|
+
puts "#{row['legacy_id'].to_s.ljust(10)} #{name_for(row).to_s.ljust(40)} #{new ? '(new)' : ' '}"
|
100
|
+
if DEBUG
|
101
|
+
puts row.values_hash(@attributes)
|
102
|
+
puts row['remote_hash']
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def confirm
|
107
|
+
agree('Do you want to continue, pushing these records to OneBody? ')
|
108
|
+
end
|
109
|
+
|
110
|
+
# use ActiveResource to create/update records on remote end
|
111
|
+
def push
|
112
|
+
puts 'Updating remote end...'
|
113
|
+
index = 0
|
114
|
+
print "#{resource.name} 0/0\r"; STDOUT.flush
|
115
|
+
(@create + @update).each_slice(MAX_TO_BATCH_AT_A_TIME) do |records|
|
116
|
+
response = resource.post(:batch, {}, records.to_xml)
|
117
|
+
statuses = Hash.from_xml(response.body)['records']
|
118
|
+
statuses.select { |s| s['status'] == 'error' }.each do |status|
|
119
|
+
puts "#{status['legacy_id']}: #{status['error']}"
|
120
|
+
end
|
121
|
+
index += records.length
|
122
|
+
print "#{resource.name} #{index}/#{@create.length + @update.length}\r"; STDOUT.flush
|
123
|
+
end
|
124
|
+
puts
|
125
|
+
end
|
126
|
+
|
127
|
+
def data_by_id
|
128
|
+
@data_by_id ||= begin
|
129
|
+
by_id = {}
|
130
|
+
@data.each { |r| by_id[r['legacy_id'].to_i] = r }
|
131
|
+
by_id
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
attr_accessor :attributes, :data
|
136
|
+
attr_reader :update, :create
|
137
|
+
|
138
|
+
class << self; attr_accessor :resource; end
|
139
|
+
def resource; self.class.resource; end
|
140
|
+
|
141
|
+
protected
|
142
|
+
|
143
|
+
# ask remote end for value hashe for each record (50 at a time)
|
144
|
+
# mark records to create or update based on response
|
145
|
+
def compare_hashes(ids, force=false)
|
146
|
+
ids.each_slice(MAX_HASHES_AT_A_TIME) do |some_ids|
|
147
|
+
print '.'; STDOUT.flush
|
148
|
+
options = {:attrs => @attributes.join(','), :legacy_id => some_ids.join(',')}
|
149
|
+
options.merge!(:debug => true) if DEBUG
|
150
|
+
hashes = resource.get(:hashify, options)
|
151
|
+
hashes.each do |record|
|
152
|
+
row = data_by_id[record['legacy_id'].to_i]
|
153
|
+
row['remote_hash'] = record['hash'] if DEBUG
|
154
|
+
@update << row if force or row.values_hash(@attributes) != record['hash']
|
155
|
+
end
|
156
|
+
@create += some_ids.reject { |id| hashes.map { |h| h['legacy_id'].to_i }.include?(id.to_i) }.map { |id| data_by_id[id.to_i] }
|
157
|
+
end
|
158
|
+
puts
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# handles people.csv and splits out family data for FamilyUpdater
|
2
|
+
class PeopleUpdater < UpdateAgent
|
3
|
+
self.resource = Person
|
4
|
+
|
5
|
+
# split out family data and create a new FamilyUpdater
|
6
|
+
def initialize(filename)
|
7
|
+
super(filename)
|
8
|
+
if defined?(USE_CONVERTER)
|
9
|
+
converter = Kernel.const_get(USE_CONVERTER + 'Converter').new
|
10
|
+
@data = converter.convert(@data.clone)
|
11
|
+
@attributes = @data.first.keys
|
12
|
+
end
|
13
|
+
person_data = []
|
14
|
+
family_data = {}
|
15
|
+
@data.each_with_index do |row, index|
|
16
|
+
person, family = split_change_hash(row)
|
17
|
+
if existing_family = family_data[family['legacy_id']]
|
18
|
+
person['family'] = existing_family
|
19
|
+
person_data << person
|
20
|
+
else
|
21
|
+
person['family'] = family
|
22
|
+
person_data << person
|
23
|
+
family_data[family['legacy_id']] = family
|
24
|
+
end
|
25
|
+
print "splitting family record #{index+1}\r"
|
26
|
+
end
|
27
|
+
puts
|
28
|
+
@data = person_data
|
29
|
+
@attributes.reject! { |a| a =~ /^family_/ and a != 'family_id' }
|
30
|
+
@family_agent = FamilyUpdater.new(family_data.values)
|
31
|
+
end
|
32
|
+
|
33
|
+
def name_for(row)
|
34
|
+
"#{row['first_name']} #{row['last_name']}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def compare(force=false)
|
38
|
+
@family_agent.compare(force)
|
39
|
+
super(force)
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_work?
|
43
|
+
@family_agent.has_work? or super
|
44
|
+
end
|
45
|
+
|
46
|
+
def present
|
47
|
+
@family_agent.present if @family_agent.has_work?
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
def push
|
52
|
+
@family_agent.push
|
53
|
+
super
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
# split hash of values into person and family values based on keys
|
59
|
+
def split_change_hash(vals)
|
60
|
+
person_vals = {}
|
61
|
+
family_vals = {}
|
62
|
+
vals.each do |key, val|
|
63
|
+
if key =~ /^family_/
|
64
|
+
family_vals[key.sub(/^family_/, '')] = val
|
65
|
+
else
|
66
|
+
person_vals[key] = val
|
67
|
+
end
|
68
|
+
end
|
69
|
+
family_vals['legacy_id'] ||= person_vals['legacy_family_id']
|
70
|
+
[person_vals, family_vals]
|
71
|
+
end
|
72
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: seven1m-onebody-updateagent
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tim Morgan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-08-13 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: fastercsv
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: "0"
|
23
|
+
version:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: highline
|
26
|
+
version_requirement:
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: "0"
|
32
|
+
version:
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activeresource
|
35
|
+
version_requirement:
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: "0"
|
41
|
+
version:
|
42
|
+
description:
|
43
|
+
email: tim@timmorgan.org
|
44
|
+
executables:
|
45
|
+
- update_onebody
|
46
|
+
extensions: []
|
47
|
+
|
48
|
+
extra_rdoc_files: []
|
49
|
+
|
50
|
+
files:
|
51
|
+
- example.yml
|
52
|
+
- example_using_converter.yml
|
53
|
+
- README.markdown
|
54
|
+
- bin/update_onebody
|
55
|
+
- lib/onebody-updateagent.rb
|
56
|
+
- lib/updateagent/hash_extensions.rb
|
57
|
+
- lib/updateagent/resources.rb
|
58
|
+
- lib/updateagent/schema.rb
|
59
|
+
- lib/updateagent/updateagent.rb
|
60
|
+
- lib/updateagent/converters/acs_converter.rb
|
61
|
+
- lib/updateagent/updaters/family_updater.rb
|
62
|
+
- lib/updateagent/updaters/people_updater.rb
|
63
|
+
has_rdoc: false
|
64
|
+
homepage: http://github.com/seven1m/onebody-updateagent
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: "0"
|
75
|
+
version:
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: "0"
|
81
|
+
version:
|
82
|
+
requirements: []
|
83
|
+
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 1.2.0
|
86
|
+
signing_key:
|
87
|
+
specification_version: 2
|
88
|
+
summary: Companion to OneBody that handles sync with external data source.
|
89
|
+
test_files: []
|
90
|
+
|