allplayers_imports 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ allplayers_imports.rb
2
+ =====================
3
+
4
+ Ruby tool that parses data and imports into AllPlayers.com via AllPlayers public API.
5
+
6
+ Currently works with gdata (Google Spreadsheet API) but can also accept a parsed csv as input.
7
+
8
+ To install, run <code>gem install allplayers_imports</code>
9
+
10
+ Extend your allplayers object to include AllPlayersImports
11
+
12
+ Example:
13
+ ```
14
+ include 'allplayers'
15
+ include 'allplayers_imports'
16
+
17
+ allplayers_session = AllPlayers::Client.new(nil, 'www.allplayers.com')
18
+ allplayers_session.add_headers({:Authorization => 'Basic ' + Base64.encode64(user + ':' + pass)})
19
+ allplayers_session.extend AllPlayersImports
20
+
21
+ allplayers_session.import_sheet(spreadsheet, 'Groups or Participant Information')
22
+ ```
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+ Gem::Specification.new do |spec|
3
+ spec.add_dependency 'ci_reporter', ['~> 1.7.0']
4
+ spec.add_dependency 'fastercsv', ['~> 1.5.3']
5
+ spec.add_dependency 'highline', ['~> 1.6.11']
6
+ spec.add_dependency 'allplayers', ['~> 0.1.0']
7
+ spec.authors = ["AllPlayers.com"]
8
+ spec.description = %q{A Ruby tool to handle import spreadsheets into AllPlayers API.}
9
+ spec.email = ['support@allplayers.com']
10
+ spec.files = %w(README.md allplayers_imports.gemspec)
11
+ spec.files += Dir.glob("lib/**/*.rb")
12
+ spec.homepage = 'http://www.allplayers.com/'
13
+ spec.licenses = ['MIT']
14
+ spec.name = 'allplayers_imports'
15
+ spec.require_paths = ['lib']
16
+ spec.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
17
+ spec.summary = spec.description
18
+ spec.version = '0.1.0'
19
+ end
@@ -0,0 +1,839 @@
1
+ # Provides ImportActions using AllPlayers API.
2
+
3
+ require 'rubygems'
4
+ require 'highline/import'
5
+ require 'active_support'
6
+ require 'active_support/core_ext/time/conversions.rb'
7
+ require 'thread'
8
+ require 'logger'
9
+ require 'resolv'
10
+ require 'date'
11
+ require 'faster_csv'
12
+
13
+ # Stop EOF errors in Highline
14
+ HighLine.track_eof = false
15
+
16
+ class DuplicateUserExists < StandardError
17
+ end
18
+
19
+ # Add some tools to Array to make parsing spreadsheet rows easier.
20
+ class Array
21
+ # Little utility to convert array to Hash with defined keys.
22
+ def to_hash(other)
23
+ Hash[ *(0...other.size()).inject([]) { |arr, ix| arr.push(other[ix], self[ix]) } ]
24
+ end
25
+ # Split off first element in each array item, after splitting by pattern, then
26
+ # strip trailing and preceding whitespaces.
27
+ def split_first(pattern)
28
+ arr = []
29
+ self.each do | item |
30
+ arr.push(item.split(pattern)[0].strip)
31
+ end
32
+ arr
33
+ end
34
+ def downcase
35
+ arr = []
36
+ self.each do |item|
37
+ arr.push(item.downcase)
38
+ end
39
+ arr
40
+ end
41
+ def gsub(pattern,replacement)
42
+ arr = []
43
+ self.each do |item|
44
+ arr.push(item.gsub(pattern,replacement))
45
+ end
46
+ arr
47
+ end
48
+ end
49
+
50
+ class Hash
51
+ def key_filter(pattern, replacement = '')
52
+ hsh = {}
53
+ filtered = self.reject { |key,value| key.match(pattern).nil? }
54
+ filtered.each { |key,value| hsh[key.sub(pattern, replacement)] = value }
55
+ hsh
56
+ end
57
+ end
58
+
59
+ class Date
60
+ def to_age
61
+ now = Time.now.utc.to_date
62
+ now.year - self.year - ((now.month > self.month || (now.month == self.month && now.day >= self.day)) ? 0 : 1)
63
+ end
64
+ end
65
+
66
+ # valid_email_address port from Drupal
67
+ class String
68
+ def valid_email_address?
69
+ return !self.match(/^[a-zA-Z0-9_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\']+@((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.?)+|(\[([0-9]{1,3}(\.[0-9]{1,3}){3}|[0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7})\]))$/).nil?
70
+ end
71
+ def active_email_domain?
72
+ domain = self.match(/\@(.+)/)[1]
73
+ Resolv::DNS.open do |dns|
74
+ @mx = dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
75
+ @a = dns.getresources(domain, Resolv::DNS::Resource::IN::A)
76
+ end
77
+ @mx.size > 0 || @a.size > 0
78
+ end
79
+ end
80
+
81
+ # Write the correct header for the csv log
82
+ class ApciLogDevice < Logger::LogDevice
83
+ private
84
+ def add_log_header(file)
85
+ file.write(
86
+ "\"Severity\",\"Date\",\"Severity (Full)\",\"Row\",\"Info\"\n"
87
+ )
88
+ end
89
+ end
90
+
91
+ # Build a Logger::Formatter subclass.
92
+ class ApciFormatter < Logger::Formatter
93
+ Format = "\"%s\",\"[%s#%d]\",\"%5s\",\"%s\",\"%s\"\n"
94
+ def initialize
95
+ @highline = HighLine.new
96
+ super
97
+ end
98
+ # Provide a call() method that returns the formatted message.
99
+ def call(severity, time, program_name, message)
100
+ message_color = severity == 'ERROR' ? @highline.color(message, :red, :bold) : message
101
+ message_color = severity == 'WARN' ? @highline.color(message, :bold) : message_color
102
+ if program_name == program_name.to_i.to_s
103
+ # Abuse program_name as row #
104
+ if program_name.to_i.even?
105
+ say @highline.color('Row ' + program_name + ': ', :cyan, :bold) + message_color
106
+ else
107
+ say @highline.color('Row ' + program_name + ': ', :magenta, :bold) + message_color
108
+ end
109
+ else
110
+ say message_color
111
+ end
112
+ message.gsub!('"', "'")
113
+ Format % [severity[0..0], format_datetime(time), $$, severity, program_name,
114
+ msg2str(message)]
115
+ end
116
+ end
117
+
118
+ # Functions to aid importing any type of spreadsheet to Allplayers.com.
119
+ module AllPlayersImports
120
+ @@sheet_mutex = Mutex.new
121
+ @@stats_mutex = Mutex.new
122
+ @@user_mutex = Mutex.new
123
+ @@email_mutexes = {}
124
+
125
+ # Static UID to email
126
+ @@uuid_map = {}
127
+ # Statistics about operations performed
128
+ @@stats = {}
129
+
130
+ # Cache and honor locks on email to UID req's.
131
+ def email_to_uuid(email, action = nil)
132
+ @@user_mutex.synchronize do
133
+ # If we've cached it, short circuit.
134
+ return @@uuid_map[email] if @@uuid_map.has_key?(email)
135
+ # Haven't cached it, create a targeted Mutex for it.
136
+ # Changed to using monitor, see http://www.velocityreviews.com/forums/t857319-thread-and-mutex-in-ruby-1-8-7-a.html
137
+ @@email_mutexes[email] = Monitor.new unless @@email_mutexes.has_key?(email)
138
+ end
139
+
140
+ user = nil
141
+ # Try to get a targeted lock.
142
+ @@email_mutexes[email].synchronize {
143
+ # Got the lock, short circuit if another thread found our UID.
144
+ return @@uuid_map[email] if @@uuid_map.has_key?(email)
145
+ user = self.user_get_email(email)
146
+ @@uuid_map[email] = user['uuid'] if user.include?('uuid')
147
+ }
148
+ # Caller wants the lock while it tries to generate a user.
149
+ return user['uuid'], @@email_mutexes[email] if action == :lock
150
+ user['uuid']
151
+ end
152
+
153
+ def verify_children(row, description = 'User', matched_uuid = nil)
154
+ # Fields to match
155
+ import = row.reject {|k,v| k != 'first_name' && k != 'last_name'}
156
+ prefixes = ['parent_1_', 'parent_2_']
157
+ matched_parents = []
158
+ ret = nil
159
+ prefixes.each {|prefix|
160
+ parent_description = prefix.split('_').join(' ').strip.capitalize
161
+ if row.has_key?(prefix + 'uuid')
162
+ children = self.user_children_list(row[prefix + 'uuid']['item'].first['uuid'])
163
+ next if children.nil? || children.length <= 0
164
+ children['item'].each do |child|
165
+ kid = self.user_get(child['uuid'])
166
+ next if kid['firstname'].nil?
167
+ if (matched_uuid.nil? || matched_uuid != child['uuid'])
168
+ system = {}
169
+ system['first_name'] = kid['firstname'].downcase if kid.has_key?('firstname')
170
+ system['last_name'] = kid['lastname'].downcase if kid.has_key?('lastname')
171
+ import['first_name'] = import['first_name'].downcase
172
+ import['last_name'] = import['last_name'].downcase
173
+ if (system != import)
174
+ # Keep looking
175
+ next
176
+ end
177
+ # Found it
178
+ @logger.info(get_row_count.to_s) {parent_description + ' has matching child: ' + description + ' ' + row['first_name'] + ' ' + row['last_name']} if ret.nil?
179
+ if matched_uuid.nil?
180
+ matched_uuid = child['uuid']
181
+ end
182
+ if !child.nil?
183
+ ret = {'mail' => kid['email'], 'uuid' => matched_uuid }
184
+ end
185
+ matched_parents.push(prefix)
186
+ break
187
+ end
188
+ end
189
+ end
190
+ }
191
+ # Add existing child to other parent if needed.
192
+ unless matched_uuid.nil?
193
+ prefixes.each {|prefix|
194
+ parent_description = prefix.split('_').join(' ').strip.capitalize
195
+ if row.has_key?(prefix + 'uuid') && !matched_parents.include?(prefix)
196
+ @logger.info(get_row_count.to_s) {'Adding existing child, ' + description + ' ' + row['first_name'] + ' ' + row['last_name'] + ' has matching child : ' + parent_description}
197
+ self.user_create_child(row[prefix + 'uuid']['item'].first['uuid'], '', '', '', '', {:child_uuid => matched_uuid})
198
+ end
199
+ }
200
+ end
201
+
202
+ return ret
203
+ end
204
+
205
+ def get_group_names_from_file
206
+ groups_uuids = {}
207
+ if FileTest.exist?("imported_groups.csv")
208
+ FasterCSV.foreach("imported_groups.csv") do |row|
209
+ groups_uuids[row[1]] = row[2]
210
+ end
211
+ end
212
+ groups_uuids
213
+ end
214
+
215
+ def get_group_rows_from_file
216
+ groups_uuids = {}
217
+ if FileTest.exist?("imported_groups.csv")
218
+ FasterCSV.foreach("imported_groups.csv") do |row|
219
+ groups_uuids[row[0]] = row[2]
220
+ end
221
+ end
222
+ groups_uuids
223
+ end
224
+
225
+ def prepare_row(row_array, column_defs, row_count = nil)
226
+ if row_count
227
+ set_row_count(row_count)
228
+ else
229
+ increment_row_count
230
+ end
231
+ row = row_array.to_hash(column_defs)
232
+ # Convert everything to a string and strip whitespace.
233
+ row.each { |key,value| row.store(key,value.to_s.strip)}
234
+ # Delete empty values.
235
+ row.delete_if { |key,value| value.empty? }
236
+ end
237
+
238
+ def get_row_count
239
+ Thread.current['row_count'] = 0 if Thread.current['row_count'].nil?
240
+ Thread.current['row_count']
241
+ end
242
+
243
+ def increment_row_count
244
+ set_row_count(get_row_count + 1)
245
+ end
246
+
247
+ def set_row_count(count)
248
+ Thread.current['row_count'] = count
249
+ end
250
+
251
+ def increment_stat(type)
252
+ @@stats_mutex.synchronize do
253
+ if @@stats.has_key?(type)
254
+ @@stats[type]+=1
255
+ else
256
+ @@stats[type] = 1
257
+ end
258
+ end
259
+ end
260
+
261
+ def import_sheet(sheet, name, g = nil, wuri = nil, run_character = nil, skip_emails = nil)
262
+ if skip_emails.nil?
263
+ self.remove_headers({:NOTIFICATION_BYPASS => nil, :API_USER_AGENT => nil})
264
+ else
265
+ self.add_headers({:NOTIFICATION_BYPASS => 1, :API_USER_AGENT => 'AllPlayers-Import-Client'})
266
+ end
267
+
268
+ run_char = run_character
269
+ run_char = $run_character unless $run_character.nil?
270
+ rerun_sheet = []
271
+ rerun_row_count = {}
272
+ start_time = Time.now
273
+ @logger.debug('import') {'Started ' + start_time.to_s}
274
+
275
+ set_row_count(0)
276
+ increment_row_count
277
+ # Pull the first row and chunk it, it's just extended field descriptions.
278
+ @logger.info(get_row_count.to_s) {"Skipping Descriptions"}
279
+ sheet.shift
280
+
281
+ # Pull the second row and use it to define columns.
282
+ increment_row_count
283
+ @logger.info(get_row_count.to_s) {"Parsing column labels"}
284
+ begin
285
+ column_defs = sheet.shift.split_first("\n").gsub(/[^0-9a-z]/i, '_').downcase
286
+ rescue
287
+ @logger.info(get_row_count.to_s) {"Error parsing column labels"}
288
+ return
289
+ end
290
+
291
+ if $skip_rows
292
+ @logger.info(get_row_count.to_s) {'Skipping ' + $skip_rows.to_s + ' rows'}
293
+ while get_row_count < $skip_rows do
294
+ sheet.shift
295
+ increment_row_count
296
+ end
297
+ @logger.debug(get_row_count.to_s) {'Skipped ' + $skip_rows.to_s + ' rows'}
298
+ end
299
+
300
+ row_count = get_row_count
301
+ # TODO - Detect sheet type / sanity check by searching column_defs
302
+ if (name == 'Participant Information')
303
+ # mixed sheet... FUN!
304
+ @logger.info(get_row_count.to_s) {"Importing Participants, Parents and Group assignments\n"}
305
+ # Multi-thread
306
+ threads = []
307
+ # Set default thread_count to 7, accept global to change it.
308
+ thread_count = $thread_count.nil? ? 7 : $thread_count
309
+
310
+ for i in 0..thread_count do
311
+ threads << Thread.new {
312
+ until sheet.nil?
313
+ row = nil
314
+ @@sheet_mutex.synchronize do
315
+ row = sheet.shift
316
+ row_count+=1
317
+ end
318
+ unless row.nil?
319
+ formatted_row = self.prepare_row(row, column_defs, row_count)
320
+ if run_char.nil?
321
+ self.import_mixed_user(formatted_row)
322
+ else
323
+ if formatted_row['run_character'].to_s == run_char.to_s
324
+ self.import_mixed_user(formatted_row)
325
+ else
326
+ @logger.info(get_row_count.to_s) {'Skipping row ' + row_count.to_s}
327
+ end
328
+ end
329
+ else
330
+ break
331
+ end
332
+ end
333
+ }
334
+ end
335
+ threads.each_index {|i|
336
+ threads[i].join
337
+ puts 'Thread ' + i.to_s + ' exited.'
338
+ }
339
+ elsif (name == 'Users')
340
+ #if (2 <= (column_defs & ['First Name', 'Last Name']).length)
341
+ @logger.info(get_row_count.to_s) {"Importing Users\n"}
342
+ sheet.each {|row| self.import_user(self.prepare_row(row, column_defs))}
343
+ elsif (name == 'Groups' || name == 'Group Information' || name == 'Duplicates')
344
+ #elsif (2 <= (column_defs & ['Group Name', 'Category']).length)
345
+ @logger.info(get_row_count.to_s) {"Importing Groups\n"}
346
+
347
+ # Multi-thread
348
+ threads = []
349
+ # Set default thread_count to 5, accept global to change it.
350
+ thread_count = $thread_count.nil? ? 5 : $thread_count
351
+ for i in 0..thread_count do
352
+ threads << Thread.new {
353
+ until sheet.nil?
354
+ row = nil
355
+ @@sheet_mutex.synchronize do
356
+ row = sheet.shift
357
+ row_count+=1
358
+ end
359
+ unless row.nil?
360
+ formatted_row = self.prepare_row(row, column_defs, row_count)
361
+ if run_char.nil?
362
+ self.import_group(formatted_row)
363
+ else
364
+ if formatted_row['run_character'].to_s == run_char.to_s
365
+ title = self.import_group(formatted_row)
366
+ if title == formatted_row['group_name']
367
+ rerun_sheet.push(row) if title == formatted_row['group_name']
368
+ rerun_row_count = rerun_row_count.merge(title => get_row_count)
369
+ end
370
+ else
371
+ @logger.info(get_row_count.to_s) {'Skipping row ' + row_count.to_s}
372
+ end
373
+ end
374
+ else
375
+ break
376
+ end
377
+ end
378
+ }
379
+ end
380
+ threads.each_index {|i|
381
+ threads[i].join
382
+ puts 'Thread ' + i.to_s + ' exited.'
383
+ }
384
+ # Retrying rows that didn't find group above.
385
+ rerun_sheet.each {|row|
386
+ formatted_row = self.prepare_row(row, column_defs)
387
+ set_row_count(rerun_row_count[formatted_row['group_name']])
388
+ self.import_group(formatted_row)
389
+ }
390
+ elsif (name == 'Events')
391
+ #elsif (2 <= (column_defs & ['Title', 'Groups Involved', 'Duration (in minutes)']).length)
392
+ @logger.info(get_row_count.to_s) {"Importing Events\n"}
393
+ sheet.each {|row|
394
+ row_count+=1
395
+ response = self.import_event(self.prepare_row(row, column_defs))
396
+ unless g.nil? || wuri.nil?
397
+ g.put_cell_content(wuri.to_s+'/R'+row_count.to_s+'C6', response['nid'], row_count, 6) if response != 'update'
398
+ end
399
+ }
400
+ elsif (name == 'Users in Groups')
401
+ #elsif (2 <= (column_defs & ['Group Name', 'User email', 'Role (Admin, Coach, Player, etc)']).length)
402
+ @logger.info(get_row_count.to_s) {"Importing Users in Groups\n"}
403
+ sheet.each {|row| self.import_user_group_role(self.prepare_row(row, column_defs))}
404
+ else
405
+ @logger.info(get_row_count.to_s) {"Don't know what to do with sheet " + name + "\n"}
406
+ next # Go to the next sheet.
407
+ end
408
+ # Output stats
409
+ seconds = (Time.now - start_time).to_i
410
+ @logger.debug('import') {' stopped ' + Time.now.to_s}
411
+ stats_array = []
412
+ @@stats.each { |key,value| stats_array.push(key.to_s + ': ' + value.to_s) unless value.nil? or value == 0}
413
+ puts
414
+ puts
415
+ @logger.info('import') {'Imported ' + stats_array.sort.join(', ')}
416
+ @logger.info('import') {' in ' + (seconds / 60).to_s + ' minutes ' + (seconds % 60).to_s + ' seconds.'}
417
+ puts
418
+ # End stats
419
+ end
420
+
421
+ def import_mixed_user(row)
422
+ @logger.info(get_row_count.to_s) {'Processing...'}
423
+ # Import Users (Make sure parents come first).
424
+ responses = {}
425
+ ['parent_1_', 'parent_2_', 'participant_'].each {|prefix|
426
+ user = row.key_filter(prefix)
427
+ # Add in Parent email addresses if this is the participant.
428
+ user.merge!(row.reject {|key, value| !key.include?('email_address')}) if prefix == 'participant_'
429
+ description = prefix.split('_').join(' ').strip.capitalize
430
+
431
+ responses[prefix] = import_user(user, description) unless user.empty?
432
+ if !responses[prefix].respond_to?(:has_key?)
433
+ responses[prefix] = {}
434
+ end
435
+ }
436
+
437
+ if responses.has_key?('participant_') && !responses['participant_'].nil?
438
+ # Update participant with responses. We're done with parents.
439
+ row['participant_uuid'] = responses['participant_']['uuid'] if responses['participant_'].has_key?('uuid')
440
+ row['participant_email_address'] = responses['participant_']['mail'] if responses['participant_'].has_key?('mail')
441
+
442
+ # Find the max number of groups being imported
443
+ group_list = row.reject {|key, value| key.match('group_').nil?}
444
+ number_of_groups = 0
445
+ key_int_value = 0
446
+ group_list.each {|key, value|
447
+ key_parts = key.split('_')
448
+ key_parts.each {|part|
449
+ key_int_value = part.to_i
450
+ if (key_int_value > number_of_groups)
451
+ number_of_groups = key_int_value
452
+ end
453
+ }
454
+ }
455
+
456
+ # Create the list of group names to iterate through
457
+ group_names = []
458
+ for i in 1..number_of_groups
459
+ group_names.push('group_' + i.to_s + '_')
460
+ end
461
+
462
+ # Group Assignment + Participant
463
+ group_names.each {|prefix|
464
+ group = row.key_filter(prefix, 'group_')
465
+ user = row.key_filter('participant_')
466
+ responses[prefix] = import_user_group_role(user.merge(group)) unless group.empty?
467
+ }
468
+ end
469
+ end
470
+
471
+ def import_user(row, description = 'User')
472
+ more_params = {}
473
+
474
+ if row['birthdate'].nil?
475
+ @logger.error(get_row_count.to_s) {'No Birth Date Listed. Failed to import ' + description + '.'}
476
+ return {}
477
+ else
478
+ begin
479
+ birthdate = Date.parse(row['birthdate'])
480
+ rescue ArgumentError => err
481
+ @logger.error(get_row_count.to_s) {'Invalid Birth Date. Failed to import ' + description + '.'}
482
+ @logger.error(get_row_count.to_s) {err.message.to_s}
483
+ return {}
484
+ end
485
+ end
486
+
487
+ ('1'..'2').each { |i|
488
+ key = 'parent_' + i + '_email_address'
489
+ if row.has_key?(key)
490
+ parent_uuid = nil
491
+ begin
492
+ parent_uuid = self.user_get_email(row[key])
493
+ rescue DuplicateUserExists => dup_e
494
+ @logger.error(get_row_count.to_s) {'Parent ' + i + ' ' + dup_e.message.to_s}
495
+ end
496
+ if parent_uuid.nil?
497
+ @logger.warn(get_row_count.to_s) {"Can't find account for Parent " + i + ": " + row[key]}
498
+ else
499
+ row['parent_' + i + '_uuid'] = parent_uuid
500
+ end
501
+ end
502
+ }
503
+
504
+ # If 13 or under, verify parent, request allplayers.net email if needed.
505
+ if birthdate.to_age < 14
506
+ # If 13 or under, no email & has parent, request allplayers.net email.
507
+ if !(row.has_key?('parent_1_uuid') || row.has_key?('parent_2_uuid'))
508
+ @logger.error(get_row_count.to_s) {'Missing parents for '+ description +' age 13 or less.'}
509
+ return {}
510
+ end
511
+ end
512
+
513
+ lock = nil
514
+ # Request allplayers.net email if needed.
515
+ if !row.has_key?('email_address')
516
+ # If 13 or under, no email & has parent, request allplayers.net email.
517
+ if row.has_key?('parent_1_uuid') || row.has_key?('parent_2_uuid')
518
+ # Request allplayers.net email
519
+ more_params['email_alternative'] = {:value => 1}
520
+ # TODO - Consider how to send welcome email to parent. (Queue allplayers.net emails in Drupal for cron playback)
521
+ # Create a lock for these parents
522
+ @@user_mutex.synchronize do
523
+ parent_uuids = []
524
+ parent_uuids.push(row['parent_1_uuid']['item'].first['uuid']) if row.has_key?('parent_1_uuid')
525
+ parent_uuids.push(row['parent_2_uuid']['item'].first['uuid']) if row.has_key?('parent_2_uuid')
526
+ parents_key = parent_uuids.sort.join('_')
527
+ # Haven't cached it, create a targeted Mutex for it.
528
+ @@email_mutexes[parents_key] = Mutex.new unless @@email_mutexes.has_key?(parents_key)
529
+ lock = @@email_mutexes[parents_key]
530
+ end
531
+ else
532
+ @logger.error(get_row_count.to_s) {'Missing parents for '+ description +' without email address.'}
533
+ return {}
534
+ end
535
+ else
536
+ # Check if user already
537
+ begin
538
+ uuid, lock = email_to_uuid(row['email_address'], :lock)
539
+ rescue DuplicateUserExists => dup_e
540
+ @logger.error(get_row_count.to_s) {description + ' ' + dup_e.message.to_s}
541
+ return {}
542
+ end
543
+
544
+ unless uuid.nil?
545
+ @logger.warn(get_row_count.to_s) {description + ' already exists: ' + row['email_address'] + ' at UUID: ' + uuid + '. Participant will still be added to groups.'}
546
+ self.verify_children(row, description, uuid)
547
+ return {'mail' => row['email_address'], 'uuid' => uuid}
548
+ else
549
+ if !row['email_address'].valid_email_address?
550
+ @logger.error(get_row_count.to_s) {description + ' has an invalid email address: ' + row['email_address'] + '. Skipping.'}
551
+ return {}
552
+ end
553
+ if !row['email_address'].active_email_domain?
554
+ @logger.error(get_row_count.to_s) {description + ' has an email address with an invalid or inactive domain: ' + row['email_address'] + '. Skipping.'}
555
+ return {}
556
+ end
557
+ end
558
+ end
559
+
560
+ # Check required fields
561
+ missing_fields = ['first_name', 'last_name', 'gender', 'birthdate'].reject {
562
+ |field| row.has_key?(field) && !row[field].nil? && !row[field].empty?
563
+ }
564
+ if !missing_fields.empty?
565
+ @logger.error(get_row_count.to_s) {'Missing required fields for '+ description +': ' + missing_fields.join(', ')}
566
+ return {}
567
+ end
568
+
569
+ @logger.info(get_row_count.to_s) {'Importing ' + description +': ' + row['first_name'] + ' ' + row['last_name']}
570
+
571
+ response = {}
572
+
573
+ # Lock down this email address.
574
+ lock.synchronize {
575
+ # Last minute checks.
576
+ if !row['email_address'].nil? && @@uuid_map.has_key?(row['email_address'])
577
+ @logger.warn(get_row_count.to_s) {description + ' already exists: ' + row['email_address'] + ' at UUID: ' + @@uuid_map[row['email_address']] + '. Participant will still be added to groups.'}
578
+ return {'mail' => row['email_address'], 'uuid' => @@uuid_map[row['email_address']] }
579
+ end
580
+
581
+ # Avoid creating duplicate children.
582
+ existing_child = self.verify_children(row, description)
583
+ return existing_child unless existing_child.nil?
584
+ if row.has_key?('email_address') && row.has_key?('parent_1_uuid')
585
+ more_params['email'] = row['email_address']
586
+ end
587
+ if row.has_key?('parent_1_uuid')
588
+ response = self.user_create_child(
589
+ row['parent_1_uuid']['item'].first['uuid'],
590
+ row['first_name'],
591
+ row['last_name'],
592
+ birthdate,
593
+ row['gender'],
594
+ more_params
595
+ )
596
+ else
597
+ response = self.user_create(
598
+ row['email_address'],
599
+ row['first_name'],
600
+ row['last_name'],
601
+ row['gender'],
602
+ birthdate,
603
+ more_params
604
+ )
605
+ end
606
+
607
+ if !response.nil? && response.has_key?('uuid')
608
+ # Cache the new users UID while we have the lock.
609
+ @@user_mutex.synchronize { @@uuid_map[response['email']] = response['uuid'] }
610
+ end
611
+ }
612
+
613
+ if !response.nil? && response.has_key?('uuid')
614
+ increment_stat('Users')
615
+ increment_stat(description + 's') if description != 'User'
616
+
617
+ # Don't add parent 1, already added with user_create_child.
618
+ response['parenting_2_response'] = self.user_create_child(row['parent_2_uuid']['item'].first['uuid'], '', '', '', '', {:child_uuid => response['uuid']}) if row.has_key?('parent_2_uuid')
619
+ end
620
+
621
+ return response
622
+ rescue RestClient::Exception => e
623
+ @logger.error(get_row_count.to_s) {'Failed to import ' + description + ': ' + e.message}
624
+ end
625
+
626
+ def import_group(row)
627
+ @groups_map = get_group_names_from_file unless defined? @groups_map
628
+ @group_rows = get_group_rows_from_file unless defined? @group_rows
629
+ # Checking name duplication, if duplicate add identifier by type division, league, etc..
630
+ if @group_rows.has_key?(get_row_count.to_s)
631
+ row['uuid'] = @group_rows[get_row_count.to_s]
632
+ end
633
+ begin
634
+ if row['delete']
635
+ begin
636
+ # Make sure registration settings are turned off by making group inactive.
637
+ self.group_update(row['uuid'], {'active' => 0})
638
+ self.group_delete(row['uuid'])
639
+ rescue RestClient::Exception => e
640
+ puts 'There was a problem deleting group:' + row['uuid']
641
+ @logger.info(get_row_count.to_s) {'There was a problem deleting group:' + row['uuid'] + ' ' + e.message}
642
+ else
643
+ @logger.info(get_row_count.to_s) {'Deleting group:' + row['uuid']}
644
+ puts 'Deleting group:' + row['uuid']
645
+ end
646
+ return
647
+ end
648
+ if row.has_key?('group_clone') && row.has_key?('uuid') && !row['group_clone'].empty? && !row['uuid'].empty?
649
+ begin
650
+ self.group_get(row['uuid'])
651
+ self.group_get(row['group_clone'])
652
+ rescue RestClient::Exception => e
653
+ puts 'The group you are trying to clone from can not be found, moving on to creating the group.'
654
+ else
655
+ @logger.info(get_row_count.to_s) {'Cloning settings from group: ' + row['group_clone']}
656
+ self.group_clone(row['uuid'], row['group_clone'])
657
+ return
658
+ end
659
+ elsif row.has_key?('uuid')
660
+ puts 'Group already imported.'
661
+ @logger.info(get_row_count.to_s) {'Group already imported.'}
662
+ return
663
+ end
664
+ if row['owner_uuid']
665
+ begin
666
+ owner = self.user_get(row['owner_uuid'])
667
+ raise if !owner.has_key?('uuid')
668
+ rescue
669
+ puts "Couldn't get group owner from UUID: " + row['owner_uuid'].to_s
670
+ return {}
671
+ end
672
+ else
673
+ puts 'Group import requires group owner'
674
+ return {}
675
+ end
676
+ location = row.key_filter('address_')
677
+ if location['zip'].nil?
678
+ @logger.error(get_row_count.to_s) {'Location ZIP required for group import.'}
679
+ return {}
680
+ end
681
+
682
+ categories = row['group_categories'].split(',') unless row['group_categories'].nil?
683
+ if categories.nil?
684
+ @logger.error(get_row_count.to_s) {'Group Type required for group import.'}
685
+ return {}
686
+ end
687
+ more_params = {}
688
+ more_params['group_type'] = row['group_type'] unless row['group_type'].nil?
689
+
690
+ # Checking name duplication, if duplicate add identifier by type division, league, etc..
691
+ # Only one level deep.
692
+ if @groups_map.has_key?(row['group_name'])
693
+ if row['group_type'] == 'Club'
694
+ if @groups_map.has_key?(row['group_name'] + ' Club')
695
+ row['group_name'] = row['group_name'] + ' Club 1'
696
+ else
697
+ row['group_name'] = row['group_name'] + ' Club'
698
+ end
699
+ elsif row['group_type'] == 'Team'
700
+ if @groups_map.has_key?(row['group_name'] + ' Team')
701
+ if @groups_map.has_key(row['group_name'] + ' 1')
702
+ row['group_above'] = row['group_name'] + ' Club 1'
703
+ row['group_name'] = row['group_name'] + ' 2'
704
+ else
705
+ row['group_above'] = row['group_name'] + ' Club'
706
+ row['group_name'] = row['group_name'] + ' 1'
707
+ end
708
+ else
709
+ row['group_name'] = row['group_name'] + ' Team'
710
+ end
711
+ end
712
+ end
713
+
714
+ if row.has_key?('group_uuid') && !row['group_uuid'].empty?
715
+ more_params['groups_above'] = {'0' => row['group_uuid']}
716
+ elsif row.has_key?('group_above') && !row['group_above'].empty?
717
+ if @groups_map.has_key?(row['group_above'])
718
+ @logger.info(get_row_count.to_s) {'Found group above: ' + row['group_above'] + ' at UUID ' + @groups_map[row['group_above']]}
719
+ more_params['groups_above'] = {@groups_map[row['group_above']] => @groups_map[row['group_above']]}
720
+ else
721
+ response = self.group_search({:title => row['group_above']})
722
+ if response.kind_of?(Array)
723
+ response.each { |group|
724
+ if group['title'] == row['group_above']
725
+ row['group_name'] = row['group_name'] + ' ' + row['group_type'] if group['title'] == row['group_name']
726
+ more_params['groups_above'] = {group['uuid'] => group['uuid']}
727
+ end
728
+ }
729
+ if more_params['groups_above'].nil?
730
+ puts 'Row ' + get_row_count.to_s + "Couldn't find group above: " + row['group_above']
731
+ @logger.error(get_row_count.to_s) {"Couldn't find group above: " + row['group_above']}
732
+ return row['group_name']
733
+ end
734
+ else
735
+ puts 'Row ' + get_row_count.to_s + "Couldn't find group above: " + row['group_above']
736
+ @logger.error(get_row_count.to_s) {"Couldn't find group above: " + row['group_above']}
737
+ return row['group_name']
738
+ end
739
+ end
740
+ end
741
+
742
+ @logger.info(get_row_count.to_s) {'Importing group: ' + row['group_name']}
743
+ response = self.group_create(
744
+ row['group_name'], # Title
745
+ row['group_description'], # Description field
746
+ location,
747
+ categories.last,
748
+ more_params
749
+ )
750
+ @logger.info(get_row_count.to_s) {'Group UUID: ' + response['uuid']}
751
+ rescue RestClient::Exception => e
752
+ @logger.error(get_row_count.to_s) {'Failed to import group: ' + e.message}
753
+ else
754
+ if (response && response.has_key?('uuid'))
755
+ increment_stat('Groups')
756
+ # Writing data into a csv file
757
+ @groups_map[row['group_name']] = response['uuid']
758
+ FasterCSV.open("imported_groups.csv", "a") do |csv|
759
+ csv << [get_row_count, row['group_name'],response['uuid']]
760
+ end
761
+ if row.has_key?('group_clone') && !row['group_clone'].empty?
762
+ @logger.info(get_row_count.to_s) {'Cloning settings from group: ' + row['group_clone']}
763
+ response = self.group_clone(
764
+ response['uuid'],
765
+ row['group_clone'],
766
+ nil
767
+ )
768
+ end
769
+ end
770
+ end
771
+ end
772
+
773
+ def import_user_group_role(row)
774
+ # Check User.
775
+ if row.has_key?('uuid')
776
+ user = self.user_get(row['uuid'])
777
+ elsif row.has_key?('email_address') && !row['email_address'].respond_to?(:to_s)
778
+ begin
779
+ user = self.user_get_email(row['email_address'])
780
+ rescue
781
+ @logger.error(get_row_count.to_s) {"User " + row['email_address'] + " doesn't exist to add to group"}
782
+ return
783
+ end
784
+ else
785
+ @logger.error(get_row_count.to_s) {"User can't be added to group without email address."}
786
+ return
787
+ end
788
+
789
+ # Check Group
790
+ if row.has_key?('group_uuid')
791
+ group_uuid = row['group_uuid']
792
+ else
793
+ @logger.error(get_row_count.to_s) {'User ' + row['email_address'] + " can't be added to group without group uuid."}
794
+ return
795
+ end
796
+
797
+ response = {}
798
+ # Join user to group.
799
+ begin
800
+ if row.has_key?('group_role')
801
+ # Break up any comma separated list of roles into individual roles
802
+ group_roles = row['group_role'].split(',')
803
+ options = {}
804
+ if row.has_key?('group_fee')
805
+ options = case row['group_fee']
806
+ when 'full' then {:should_pay => 1, :payment_method => :full}
807
+ when 'plan' then {:should_pay => 1, :payment_method => :plan}
808
+ else {}
809
+ end
810
+ end
811
+ group_roles.each {|group_role|
812
+ # Remove whitespace
813
+ group_role = group_role.strip
814
+ if row.has_key?('group_webform_id')
815
+ webform_ids = row['group_webform_id'].split(',')
816
+ response = self.user_join_group(group_uuid, user['uuid'], group_role, options, webform_ids)
817
+ else
818
+ response = self.user_join_group(group_uuid, user['uuid'], group_role, options)
819
+ end
820
+ }
821
+ else
822
+ response = self.user_join_group(group_uuid, user['uuid'])
823
+ end
824
+ rescue RestClient::Exception => e
825
+ @logger.error(get_row_count.to_s) {'User ' + user['uuid'] + " failed to join group " + group_uuid.to_s + ': ' + e.message}
826
+ else
827
+ if row.has_key?('group_role')
828
+ @logger.info(get_row_count.to_s) {'User ' + user['uuid'] + " joined group " + group_uuid.to_s + ' with role(s) ' + row['group_role']}
829
+ else
830
+ @logger.info(get_row_count.to_s) {'User ' + user['uuid'] + " joined group " + group_uuid.to_s}
831
+ end
832
+ end
833
+
834
+ #log stuff!!
835
+
836
+ response
837
+ end
838
+
839
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: allplayers_imports
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - AllPlayers.com
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2013-01-16 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: ci_reporter
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 11
29
+ segments:
30
+ - 1
31
+ - 7
32
+ - 0
33
+ version: 1.7.0
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: fastercsv
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 5
45
+ segments:
46
+ - 1
47
+ - 5
48
+ - 3
49
+ version: 1.5.3
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: highline
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 25
61
+ segments:
62
+ - 1
63
+ - 6
64
+ - 11
65
+ version: 1.6.11
66
+ type: :runtime
67
+ version_requirements: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ name: allplayers
70
+ prerelease: false
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ hash: 27
77
+ segments:
78
+ - 0
79
+ - 1
80
+ - 0
81
+ version: 0.1.0
82
+ type: :runtime
83
+ version_requirements: *id004
84
+ description: A Ruby tool to handle import spreadsheets into AllPlayers API.
85
+ email:
86
+ - support@allplayers.com
87
+ executables: []
88
+
89
+ extensions: []
90
+
91
+ extra_rdoc_files: []
92
+
93
+ files:
94
+ - README.md
95
+ - allplayers_imports.gemspec
96
+ - lib/allplayers_imports.rb
97
+ homepage: http://www.allplayers.com/
98
+ licenses:
99
+ - MIT
100
+ post_install_message:
101
+ rdoc_options: []
102
+
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ hash: 3
111
+ segments:
112
+ - 0
113
+ version: "0"
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ hash: 23
120
+ segments:
121
+ - 1
122
+ - 3
123
+ - 6
124
+ version: 1.3.6
125
+ requirements: []
126
+
127
+ rubyforge_project:
128
+ rubygems_version: 1.8.24
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: A Ruby tool to handle import spreadsheets into AllPlayers API.
132
+ test_files: []
133
+