motion-addressbook 1.2.0 → 1.4.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/.gitignore CHANGED
@@ -8,5 +8,8 @@ Gemfile.lock
8
8
  vendor/Pods/Pods.xcodeproj/project.pbxproj
9
9
  vendor/Pods/build*
10
10
 
11
+ abhack/*.bridgesupport
12
+ abhack/build*
13
+
11
14
  .dat*.00*
12
- pkg
15
+ pkg
data/.travis.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  language: objective-c
2
- before_install:
3
- - source /Users/travis/.bash_profile
4
- - rvm use 1.9.3
5
- install: bundle install
6
- script: bundle exec rake spec
2
+ before_install: rvm use 1.9.3
3
+ notifications:
4
+ email:
5
+ recipients:
6
+ - alex@alexrothenberg.com
7
+ - jmay@pobox.com
data/Gemfile CHANGED
@@ -1,8 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  # need to do this here as alphabetically "bubble-wrap" comes after "address_book" and require fails otherwise
4
- gem 'bubble-wrap'
4
+ gem 'bubble-wrap', '~> 1.3'
5
5
 
6
6
  # Specify your gem's dependencies in motion-addressbook.gemspec
7
7
  gemspec
8
-
data/README.md CHANGED
@@ -8,7 +8,14 @@ Apple's [Address Book Programming Guide for iOS](http://developer.apple.com/libr
8
8
 
9
9
  ## Installation
10
10
 
11
- Add this line to your application's Gemfile:
11
+ ### If you're using `bundler` (this is recommended):
12
+
13
+ Add these lines to your application's `Rakefile`:
14
+
15
+ require 'bundler'
16
+ Bundler.require
17
+
18
+ Add this line to your application's `Gemfile`:
12
19
 
13
20
  gem 'motion-addressbook'
14
21
 
@@ -16,8 +23,11 @@ And then execute:
16
23
 
17
24
  $ bundle
18
25
 
19
- Or install it yourself as:
26
+ ### Manually without bundler
27
+
28
+ Or install it yourself (remember to add the bubble-wrap dependency) as:
20
29
 
30
+ $ gem install bubble-wrap
21
31
  $ gem install motion-addressbook
22
32
 
23
33
  ## Usage
@@ -130,10 +140,14 @@ AddressBook::Person.find_or_new_by_email('alex@example.com')
130
140
  ### Create a new Contact and save in Contacts app
131
141
 
132
142
  ```ruby
133
- AddressBook::Person.create(:first_name => 'Alex', :last_name => 'Rothenberg', :email => 'alex@example.com')
143
+ AddressBook::Person.create(:first_name => 'Alex', :last_name => 'Rothenberg', :email => [{ :value => 'alex@example.com', :label => 'Home'}], , :phones => [{ :value => '9920149993', :label => 'Mobile'}])
134
144
  # => #<AddressBook::Person:0xe4e3a80 @attributes={:first_name=>"Alex", :last_name=>"Rothenberg", :job_title=>nil, :department=>nil, :organization=>nil} @ab_person=#<__NSCFType:0xe4bbef0>>
135
- ```
136
145
 
146
+ # Multiple emails/phones ex.
147
+
148
+ AddressBook::Person.create(:first_name => 'Alex', :last_name => 'Rothenberg', :emails => ["a@mail.com", "b@gmail.com", "c@gmail.com", {:value => 'ashish@gmail.com', :label => 'Personal'} ], :phones => ['1234','2345','4567'])
149
+ => #<AddressBook::Person:0x9ce23b0 @address_book=#<__NSCFType:0x9ce2660> @ab_person=#<__NSCFType:0x9ce2450> @attributes=nil>
150
+ ```
137
151
  ### Update existing contact
138
152
 
139
153
  ```ruby
data/Rakefile CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env rake
2
2
  $:.unshift("/Library/RubyMotion/lib")
3
- require 'motion/project'
3
+ require 'motion/project/template/ios'
4
4
  require "bundler/gem_tasks"
5
5
  Bundler.setup
6
6
  Bundler.require
7
7
 
8
8
  require 'bubble-wrap/test'
9
+
10
+ Motion::Project::App.setup do |app|
11
+ # Use `rake config' to see complete project settings.
12
+ app.name = 'AddressBook'
13
+ end
data/abhack/abhack.h ADDED
@@ -0,0 +1,8 @@
1
+ #import <AddressBook/AddressBook.h>
2
+
3
+ @interface ABHack : NSObject
4
+
5
+ + (NSDate *) getDateProperty: (ABPropertyID) property from: (ABRecordRef) ab_person;
6
+ + (NSDate *) getDateValueAtIndex: (int) index from: (ABMutableMultiValueRef) ab_multi_value;
7
+
8
+ @end
data/abhack/abhack.m ADDED
@@ -0,0 +1,15 @@
1
+ #import "abhack.h"
2
+
3
+ @implementation ABHack
4
+
5
+ + (NSDate *) getDateProperty: (ABPropertyID) property from: (ABRecordRef) ab_person
6
+ {
7
+ return (NSDate *)ABRecordCopyValue(ab_person, property);
8
+ }
9
+
10
+ + (NSDate *) getDateValueAtIndex: (int) index from: (ABMultiValueRef) ab_multi_value
11
+ {
12
+ return (NSDate *)ABMultiValueCopyValueAtIndex(ab_multi_value, index);
13
+ }
14
+
15
+ @end
@@ -1,5 +1,5 @@
1
1
  module Motion
2
2
  module Addressbook
3
- VERSION = "1.2.0"
3
+ VERSION = "1.4.0"
4
4
  end
5
5
  end
@@ -8,6 +8,12 @@ BW.require 'motion/address_book/addr_book.rb'
8
8
  BW.require 'motion/address_book/person.rb'
9
9
  BW.require 'motion/address_book/group.rb'
10
10
  BW.require 'motion/address_book/multi_valued.rb'
11
+ BW.require 'motion/address_book/source.rb'
12
+
11
13
  BW.require 'motion/address_book/picker.rb' do
12
14
  file('motion/address_book/picker.rb').uses_framework('AddressBookUI')
13
15
  end
16
+
17
+ Motion::Project::App.setup do |app|
18
+ app.vendor_project(File.expand_path(File.join(File.dirname(__FILE__), '../abhack')), :static)
19
+ end
@@ -5,9 +5,15 @@ module AddressBook
5
5
  def initialize
6
6
  @ab = AddressBook.address_book
7
7
  end
8
- def people
9
- ABAddressBookCopyArrayOfAllPeople(ab).map do |ab_person|
10
- AddressBook::Person.new({}, ab_person, :address_book => ab)
8
+ def people(opts = {})
9
+ if opts[:source]
10
+ ABAddressBookCopyArrayOfAllPeopleInSource(ab, opts[:source].ab_source).map do |ab_person|
11
+ AddressBook::Person.new({}, ab_person, :address_book => ab)
12
+ end
13
+ else
14
+ ABAddressBookCopyArrayOfAllPeople(ab).map do |ab_person|
15
+ AddressBook::Person.new({}, ab_person, :address_book => ab)
16
+ end
11
17
  end
12
18
  end
13
19
  def count
@@ -24,6 +30,9 @@ module AddressBook
24
30
  def person(id)
25
31
  (p = ABAddressBookGetPersonWithRecordID(ab, id)) && Person.new(nil, p, :address_book => ab)
26
32
  end
33
+ def changedSince(timestamp)
34
+ people.select {|p| p.modification_date > timestamp}
35
+ end
27
36
 
28
37
  def groups
29
38
  ABAddressBookCopyArrayOfAllGroups(@ab).map do |ab_group|
@@ -40,5 +49,10 @@ module AddressBook
40
49
  def notify_changes(callback, context)
41
50
  ABAddressBookRegisterExternalChangeCallback(ab, callback, context)
42
51
  end
52
+
53
+ def sources
54
+ # ABAddressBookCopyArrayOfAllSources(ab).map {|s| ABRecordCopyValue(s, KABSourceTypeProperty)}
55
+ ABAddressBookCopyArrayOfAllSources(ab).map {|s| Source.new(s)}
56
+ end
43
57
  end
44
58
  end
@@ -32,9 +32,10 @@ module AddressBook
32
32
  self
33
33
  end
34
34
 
35
- def new?
35
+ def new_record?
36
36
  uid == KABRecordInvalidID
37
37
  end
38
+ alias :new? :new_record?
38
39
 
39
40
  def delete!
40
41
  unless new?
@@ -77,7 +78,7 @@ module AddressBook
77
78
  end
78
79
 
79
80
  def <<(person_or_group)
80
- raise "Must save member before adding to group" if person_or_group.new?
81
+ raise ArgumentError, "Must save member before adding to group" if person_or_group.new?
81
82
  ABGroupAddMember(ab_group, person_or_group.ab_record, error)
82
83
  end
83
84
 
@@ -1,15 +1,17 @@
1
1
  module AddressBook
2
2
  class MultiValued
3
+ attr_reader :mv_type
4
+
3
5
  def initialize(opts)
4
6
  unless opts.one?
5
7
  raise ArgumentError, "MultiValued requires :attributes *or* :ab_multi_value argument"
6
8
  end
7
9
 
8
10
  if opts[:ab_multi_value]
9
- # @ab_multi_value = ABMultiValueCreateMutableCopy(opts[:ab_multi_value])
10
- @ab_multi_value = opts[:ab_multi_value]
11
+ @ab_multi_value = ABMultiValueCreateMutableCopy(opts[:ab_multi_value])
11
12
  else
12
13
  @attributes = opts[:attributes]
14
+ raise ArgumentError, "Empty multi-value objects are not allowed" if @attributes.empty?
13
15
  end
14
16
  end
15
17
 
@@ -26,7 +28,7 @@ module AddressBook
26
28
  count.times.map do |i|
27
29
  label = ABMultiValueCopyLabelAtIndex(@ab_multi_value, i)
28
30
  label_val = ABAddressBookCopyLocalizedLabel(label)
29
- data = ab_record_to_dict(ABMultiValueCopyValueAtIndex(@ab_multi_value, i))
31
+ data = ab_record_to_dict(i)
30
32
  data.merge(:label => label_val)
31
33
  end
32
34
  end
@@ -35,25 +37,51 @@ module AddressBook
35
37
  @ab_multi_value ||= convert_dictionary_into_multi_value
36
38
  end
37
39
 
40
+ def localized_label(str)
41
+ LabelMap[str] || str
42
+ end
43
+
38
44
  def convert_dictionary_into_multi_value
39
- if @attributes.find {|rec| rec[:value]}
40
- mv = ABMultiValueCreateMutable(KABMultiStringPropertyType)
45
+ @mv_type = multi_value_property_type
46
+ mv = ABMultiValueCreateMutable(mv_type)
47
+
48
+ case mv_type
49
+ when KABMultiStringPropertyType
41
50
  @attributes.each do |rec|
42
- ABMultiValueAddValueAndLabel(mv, rec[:value], rec[:label], nil)
51
+ ABMultiValueAddValueAndLabel(mv, rec[:value], localized_label(rec[:label]), nil)
43
52
  end
44
- mv
45
- else
46
- mv = ABMultiValueCreateMutable(KABMultiDictionaryPropertyType)
53
+ when KABMultiDateTimePropertyType
54
+ @attributes.each do |rec|
55
+ ABMultiValueAddValueAndLabel(mv, rec[:date], localized_label(rec[:label]), nil)
56
+ end
57
+ else # KABMultiDictionaryPropertyType
47
58
  @attributes.each do |rec|
48
- ABMultiValueAddValueAndLabel(mv, dict_to_ab_record(rec), rec[:label], nil)
59
+ if value = dict_to_ab_record(rec)
60
+ ABMultiValueAddValueAndLabel(mv, value, localized_label(rec[:label]), nil)
61
+ end
62
+ end
63
+ end
64
+
65
+ mv
66
+ end
67
+
68
+ def multi_value_property_type
69
+ if @ab_multi_value
70
+ ABMultiValueGetPropertyType(@ab_multi_value)
71
+ else
72
+ if attributes.find {|rec| rec[:value]}
73
+ KABMultiStringPropertyType
74
+ elsif attributes.find {|rec| rec[:date]}
75
+ KABMultiDateTimePropertyType
76
+ else
77
+ KABMultiDictionaryPropertyType
49
78
  end
50
- mv
51
79
  end
52
80
  end
53
81
 
54
82
  # these are for mapping fields in a kABMultiDictionaryPropertyType record
55
83
  # to keys in a standard hash (NSDictionary)
56
- @@attribute_map = {
84
+ PropertyMap = {
57
85
  KABPersonAddressStreetKey => :street,
58
86
  KABPersonAddressCityKey => :city,
59
87
  KABPersonAddressStateKey => :state,
@@ -71,20 +99,61 @@ module AddressBook
71
99
  KABPersonInstantMessageUsernameKey => :username
72
100
  }
73
101
 
102
+ LabelMap = {
103
+ "mobile" => KABPersonPhoneMobileLabel ,
104
+ "iphone" => KABPersonPhoneIPhoneLabel ,
105
+ "main" => KABPersonPhoneMainLabel ,
106
+ "home_fax" => KABPersonPhoneHomeFAXLabel,
107
+ "work_fax" => KABPersonPhoneWorkFAXLabel,
108
+ "pager" => KABPersonPhonePagerLabel ,
109
+ "work" => KABWorkLabel ,
110
+ "home" => KABHomeLabel ,
111
+ "other" => KABOtherLabel ,
112
+ "home page"=> KABPersonHomePageLabel,
113
+ "anniversary"=> KABPersonAnniversaryLabel
114
+ }
115
+
74
116
  def dict_to_ab_record(h)
75
- @@attribute_map.each_with_object({}) do |(ab_key, attr_key), ab_record|
117
+ h = PropertyMap.each_with_object({}) do |(ab_key, attr_key), ab_record|
76
118
  ab_record[ab_key] = h[attr_key] if h[attr_key]
77
119
  end
120
+ h.any? ? h : nil
78
121
  end
79
122
 
80
- def ab_record_to_dict(ab_record)
81
- case ab_record
82
- when String
83
- {:value => ab_record}
84
- else
85
- @@attribute_map.each_with_object({}) do |(ab_key, attr_key), dict|
123
+ def ab_record_to_dict(i)
124
+ case multi_value_property_type
125
+ when KABStringPropertyType
126
+ {:value => ABMultiValueCopyValueAtIndex(@ab_multi_value, i)}
127
+ when KABDateTimePropertyType
128
+ {:date => ABHack.getDateValueAtIndex(i, from: @ab_multi_value)}
129
+ when KABDictionaryPropertyType
130
+ ab_record = ABMultiValueCopyValueAtIndex(@ab_multi_value, i)
131
+ PropertyMap.each_with_object({}) do |(ab_key, attr_key), dict|
86
132
  dict[attr_key] = ab_record[ab_key] if ab_record[ab_key]
87
133
  end
134
+ else
135
+ raise TypeError, "Unknown MultiValue property type"
136
+ end
137
+ end
138
+
139
+ def <<(rec)
140
+ case multi_value_property_type
141
+ when KABMultiStringPropertyType
142
+ ABMultiValueAddValueAndLabel(ab_multi_value, rec[:value], localized_label(rec[:label]), nil)
143
+ when KABMultiDateTimePropertyType
144
+ ABMultiValueAddValueAndLabel(ab_multi_value, rec[:date], localized_label(rec[:label]), nil)
145
+ when KABMultiDictionaryPropertyType
146
+ ABMultiValueAddValueAndLabel(ab_multi_value, dict_to_ab_record(rec), localized_label(rec[:label]), nil)
147
+ else
148
+ raise TypeError, "Unknown MultiValue property type"
149
+ end
150
+
151
+ @attributes = convert_multi_value_into_dictionary
152
+ end
153
+
154
+ def first_for(label)
155
+ if rec = attributes.find {|r| r[:label] == label.to_s}
156
+ rec[:value] ? rec[:value] : rec
88
157
  end
89
158
  end
90
159
  end
@@ -75,15 +75,15 @@ module AddressBook
75
75
  def self.attribute_map
76
76
  {
77
77
  :first_name => KABPersonFirstNameProperty,
78
- :middle_name => KABPersonMiddleNameProperty,
78
+ :middle_name => KABPersonMiddleNameProperty,
79
79
  :last_name => KABPersonLastNameProperty,
80
- :suffix => KABPersonSuffixProperty,
81
- :nickname => KABPersonNicknameProperty,
80
+ :suffix => KABPersonSuffixProperty,
81
+ :nickname => KABPersonNicknameProperty,
82
82
  :job_title => KABPersonJobTitleProperty,
83
83
  :department => KABPersonDepartmentProperty,
84
84
  :organization => KABPersonOrganizationProperty,
85
- # :dob => KABPersonBirthdayProperty,
86
- :note => KABPersonNoteProperty
85
+ :birthday => KABPersonBirthdayProperty,
86
+ :note => KABPersonNoteProperty
87
87
  }
88
88
  end
89
89
  def attribute_map
@@ -93,6 +93,8 @@ module AddressBook
93
93
  def method_missing(name, *args)
94
94
  if attribute_name = getter?(name)
95
95
  get(attribute_name)
96
+ # if getter?(name)
97
+ # get(name)
96
98
  elsif attribute_name = setter?(name)
97
99
  set(attribute_name, args.first)
98
100
  else
@@ -119,8 +121,15 @@ module AddressBook
119
121
  def getter?(method_name)
120
122
  if self.class.is_attribute? method_name
121
123
  method_name
124
+ # true
122
125
  else
123
126
  nil
127
+ # attribute = method_name.split('_').last
128
+ # if ['email', 'phone'].include?(attribute)
129
+ # true
130
+ # else
131
+ # false
132
+ # end
124
133
  end
125
134
  end
126
135
  def setter?(method_name)
@@ -157,6 +166,8 @@ module AddressBook
157
166
  end
158
167
 
159
168
  def get(attribute_name)
169
+ # label, attribute = attribute_name.split('_')
170
+ # self.send("#{attribute}s").first_for(label)
160
171
  attributes[attribute_name.to_sym] ||= get_field(attribute_map[attribute_name])
161
172
  end
162
173
 
@@ -187,6 +198,10 @@ module AddressBook
187
198
  find_by(attribute_name, criteria) || new_by(attribute_name, criteria)
188
199
  end
189
200
 
201
+ def photo_image
202
+ UIImage.alloc.initWithData(photo)
203
+ end
204
+
190
205
  def photo
191
206
  ABPersonCopyImageData(ab_person)
192
207
  end
@@ -196,7 +211,9 @@ module AddressBook
196
211
  end
197
212
 
198
213
  def get_multi_valued(field)
199
- MultiValued.new(:ab_multi_value => ABRecordCopyValue(ab_person, field))
214
+ if mv = ABRecordCopyValue(ab_person, field)
215
+ MultiValued.new(:ab_multi_value => mv)
216
+ end
200
217
  end
201
218
 
202
219
  def phones
@@ -235,6 +252,10 @@ module AddressBook
235
252
  get_multi_valued(KABPersonRelatedNamesProperty)
236
253
  end
237
254
 
255
+ def dates
256
+ get_multi_valued(KABPersonDateProperty)
257
+ end
258
+
238
259
  def email; email_values.first; end
239
260
  def phone; phone_values.first; end
240
261
  def url; urls.attributes.first[:value]; end
@@ -276,18 +297,56 @@ module AddressBook
276
297
  get_field(KABPersonKindProperty) == KABPersonKindOrganization
277
298
  end
278
299
 
279
- # must stash date values in instance variables or RubyMotion throws a malloc error
300
+ def modification_date
301
+ # workaround for RubyMotion bug: blows up when fetching NSDate properties
302
+ # see http://hipbyte.myjetbrains.com/youtrack/issue/RM-81
303
+ # still broken in RubyMotion 2.0
304
+ ABHack.getDateProperty(KABPersonModificationDateProperty, from: ab_person)
305
+ # when RubyMotion bug is fixed, this should just be
306
+ # get_field(KABPersonModificationDateProperty)
307
+ end
308
+
280
309
  def creation_date
281
- @creation_date = get_field(KABPersonCreationDateProperty)
310
+ ABHack.getDateProperty(KABPersonCreationDateProperty, from: ab_person)
282
311
  end
283
312
 
284
- def modification_date
285
- @modification_date = get_field(KABPersonModificationDateProperty)
313
+ # replace *all* properties of an existing Person with new values
314
+ def replace(new_attributes)
315
+ @attributes = new_attributes
316
+ load_ab_person
317
+ end
318
+
319
+ def source
320
+ s = ABPersonCopySource(ab_person)
321
+ Source.new(s)
322
+ # fetching KABSourceNameProperty always seems to return NULL
323
+ # ABRecordCopyValue(s, KABSourceTypeProperty)
324
+ end
325
+
326
+ def linked_people
327
+ recs = ABPersonCopyArrayOfAllLinkedPeople(ab_person).mutableCopy
328
+ recs.delete(ab_person) # LinkedPeople always includes self
329
+ recs.map do |linked_rec|
330
+ Person.new(nil, linked_rec, :address_book => address_book)
331
+ end
332
+ end
333
+
334
+ def to_vcard
335
+ self.class.vcard_for(self)
336
+ end
337
+
338
+ def self.vcard_for(people)
339
+ if people.respond_to? :map
340
+ ab_persons = people.map(&:ab_person)
341
+ else
342
+ ab_persons = [people.ab_person]
343
+ end
344
+ ABPersonCreateVCardRepresentationWithPeople(ab_persons)
286
345
  end
287
346
 
288
347
  private
289
348
 
290
- def single_value_property_map
349
+ def self.single_value_property_map
291
350
  {
292
351
  KABPersonFirstNameProperty => :first_name,
293
352
  KABPersonLastNameProperty => :last_name,
@@ -297,12 +356,12 @@ module AddressBook
297
356
  KABPersonJobTitleProperty => :job_title,
298
357
  KABPersonDepartmentProperty => :department,
299
358
  KABPersonOrganizationProperty => :organization,
300
- # KABPersonBirthdayProperty => :dob,
359
+ KABPersonBirthdayProperty => :birthday,
301
360
  KABPersonNoteProperty => :note
302
361
  }
303
362
  end
304
363
 
305
- def multi_value_property_map
364
+ def self.multi_value_property_map
306
365
  {
307
366
  KABPersonPhoneProperty => :phones,
308
367
  KABPersonEmailProperty => :emails,
@@ -310,7 +369,8 @@ module AddressBook
310
369
  KABPersonURLProperty => :urls,
311
370
  KABPersonSocialProfileProperty => :social_profiles,
312
371
  KABPersonInstantMessageProperty => :im_profiles,
313
- KABPersonRelatedNamesProperty => :related_names
372
+ KABPersonRelatedNamesProperty => :related_names,
373
+ KABPersonDateProperty => :dates
314
374
  }
315
375
  end
316
376
 
@@ -318,9 +378,11 @@ module AddressBook
318
378
  def load_ab_person
319
379
  @attributes ||= {}
320
380
 
321
- single_value_property_map.each do |ab_property, attr_key|
381
+ Person.single_value_property_map.each do |ab_property, attr_key|
322
382
  if attributes[attr_key]
323
383
  set_field(ab_property, attributes[attr_key])
384
+ else
385
+ remove_field(ab_property)
324
386
  end
325
387
  end
326
388
 
@@ -330,16 +392,23 @@ module AddressBook
330
392
  set_field(KABPersonKindProperty, KABPersonKindPerson)
331
393
  end
332
394
 
333
- multi_value_property_map.each do |ab_property, attr_key|
395
+ Person.multi_value_property_map.each do |ab_property, attr_key|
334
396
  if attributes[attr_key]
335
397
  set_multi_valued(ab_property, attributes[attr_key])
398
+ else
399
+ remove_field(ab_property)
336
400
  end
337
401
  end
402
+
403
+ ab_person
338
404
  end
339
405
 
406
+ # populate attributes from existing ABPerson
340
407
  def import_ab_person
341
408
  @attributes = {}
342
- single_value_property_map.each do |ab_property, attr_key|
409
+ @modification_date = nil
410
+
411
+ Person.single_value_property_map.each do |ab_property, attr_key|
343
412
  if value = get_field(ab_property)
344
413
  @attributes[attr_key] = value
345
414
  end
@@ -349,9 +418,11 @@ module AddressBook
349
418
  @attributes[:is_org] = true
350
419
  end
351
420
 
352
- multi_value_property_map.each do |ab_property, attr_key|
353
- if (value = get_multi_valued(ab_property).attributes) && value.any?
354
- @attributes[attr_key] = value
421
+ Person.multi_value_property_map.each do |ab_property, attr_key|
422
+ if value = get_multi_valued(ab_property)
423
+ if value.attributes.any?
424
+ @attributes[attr_key] = value.attributes
425
+ end
355
426
  end
356
427
  end
357
428
 
@@ -364,10 +435,20 @@ module AddressBook
364
435
  end
365
436
  end
366
437
  def get_field(field)
367
- ABRecordCopyValue(ab_person, field)
438
+ if field == KABPersonBirthdayProperty
439
+ # special case: RubyMotion blows up on NSDate properties
440
+ # see http://hipbyte.myjetbrains.com/youtrack/issue/RM-81
441
+ ABHack.getDateProperty(field, from: ab_person)
442
+ else
443
+ ABRecordCopyValue(ab_person, field)
444
+ end
445
+ end
446
+ def remove_field(field)
447
+ ABRecordRemoveValue(ab_person, field, nil)
368
448
  end
369
449
 
370
450
  def set_multi_valued(field, values)
451
+ values = values.map { |value| ( (value.kind_of?String) ? {:value => value} : value)}
371
452
  if values && values.any?
372
453
  multi_field = MultiValued.new(:attributes => values)
373
454
  ABRecordSetValue(ab_person, field, multi_field.ab_multi_value, nil)
@@ -0,0 +1,17 @@
1
+ module AddressBook
2
+ class Source
3
+ attr_reader :ab_source
4
+
5
+ def initialize(ab_source)
6
+ @ab_source = ab_source
7
+ end
8
+
9
+ def type
10
+ ABRecordCopyValue(ab_source, KABSourceTypeProperty)
11
+ end
12
+ end
13
+
14
+ def local?
15
+ type == KABSourceTypeLocal
16
+ end
17
+ end
@@ -2,8 +2,11 @@
2
2
  require File.expand_path('../lib/motion-addressbook/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
- gem.authors = ["Alex Rothenberg"]
6
- gem.email = ["alex@alexrothenberg.com"]
5
+ gem.name = "motion-addressbook"
6
+ gem.version = Motion::Addressbook::VERSION
7
+
8
+ gem.authors = ["Alex Rothenberg", "Jason May"]
9
+ gem.email = ["alex@alexrothenberg.com", "jmay@pobox.com"]
7
10
  gem.description = %q{A RubyMotion wrapper around the iOS Address Book framework}
8
11
  gem.summary = %q{A RubyMotion wrapper around the iOS Address Book framework}
9
12
  gem.homepage = ""
@@ -11,11 +14,10 @@ Gem::Specification.new do |gem|
11
14
  gem.files = `git ls-files`.split($\)
12
15
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
- gem.name = "motion-addressbook"
15
17
  gem.require_paths = ["lib"]
16
- gem.version = Motion::Addressbook::VERSION
17
-
18
- gem.add_dependency 'bubble-wrap'
19
- gem.add_development_dependency 'rake'
18
+
19
+ gem.add_dependency 'bubble-wrap', '~> 1.3'
20
+
21
+ gem.add_development_dependency 'rake'
20
22
  gem.add_development_dependency 'rspec'
21
23
  end
@@ -23,6 +23,11 @@ describe AddressBook::Group do
23
23
  it "should not be new" do
24
24
  @group.should.not.be.new
25
25
  end
26
+
27
+ it "should not add unsaved person records" do
28
+ p = @ab.new_person({:first_name => 'Alice', :last_name => 'Artichoke'})
29
+ lambda {@group << p}.should.raise ArgumentError
30
+ end
26
31
  end
27
32
 
28
33
  describe 'a group with members' do
@@ -1,4 +1,18 @@
1
1
  describe AddressBook::MultiValued do
2
+ describe 'a new multi-value' do
3
+ it 'should convert to localized labels' do
4
+ AddressBook::MultiValued::LabelMap.size.should.equal 11
5
+ AddressBook::MultiValued::LabelMap.each do |label, localized|
6
+ mv = AddressBook::MultiValued.new(:attributes => [{:label => label, :value => 'test'}])
7
+ ABMultiValueCopyLabelAtIndex(mv.ab_multi_value, 0).should.equal localized
8
+ end
9
+ end
10
+
11
+ it "should not allow empty input" do
12
+ ->{AddressBook::MultiValued.new(:attributes => [])}.should.raise ArgumentError
13
+ end
14
+ end
15
+
2
16
  describe 'a string multi-value' do
3
17
  before do
4
18
  @attributes = [
@@ -25,6 +39,16 @@ describe AddressBook::MultiValued do
25
39
  mv2 = AddressBook::MultiValued.new(:ab_multi_value => abmv)
26
40
  mv2.attributes.should.equal @attributes
27
41
  end
42
+
43
+ describe 'after appending' do
44
+ before do
45
+ @mv << {:label => 'work', :value => 'another string'}
46
+ end
47
+
48
+ it "should have new values" do
49
+ @mv.count.should.equal 4
50
+ end
51
+ end
28
52
  end
29
53
 
30
54
  describe 'a dictionary multi-value' do
@@ -57,10 +81,91 @@ describe AddressBook::MultiValued do
57
81
  it 'should have correct internal representation' do
58
82
  internal = @mv.ab_multi_value
59
83
  ABMultiValueGetCount(internal).should.equal 2
60
- ABMultiValueCopyLabelAtIndex(internal, 0).should.equal "home"
61
- ABMultiValueCopyLabelAtIndex(internal, 1).should.equal "work"
84
+ ABMultiValueCopyLabelAtIndex(internal, 0).should.equal KABHomeLabel
85
+ ABMultiValueCopyLabelAtIndex(internal, 1).should.equal KABWorkLabel
62
86
  ABMultiValueCopyValueAtIndex(internal, 0).keys.count.should.equal 3
63
87
  ABMultiValueCopyValueAtIndex(internal, 1).keys.count.should.equal 3
64
88
  end
89
+
90
+ describe 'after appending' do
91
+ before do
92
+ @mv << {:label => 'summer', :city => 'Key West', :state => 'FL'}
93
+ end
94
+
95
+ it "should have new values" do
96
+ @mv.count.should.equal 3
97
+ end
98
+ end
99
+ end
100
+
101
+ describe 'a date multi-value' do
102
+ before do
103
+ @attributes = [
104
+ {
105
+ :label => 'birthday',
106
+ :date => NSDate.dateWithNaturalLanguageString('April 5, 1962')
107
+ }, {
108
+ :label => 'anniversary',
109
+ :date => NSDate.dateWithNaturalLanguageString('September 22, 1994')
110
+ }, {
111
+ :label => 'death',
112
+ :date => NSDate.dateWithNaturalLanguageString('December 1, 2008')
113
+ }
114
+ ]
115
+ @mv = AddressBook::MultiValued.new(:attributes => @attributes)
116
+ end
117
+
118
+ it 'should be countable' do
119
+ @mv.count.should.equal 3
120
+ end
121
+
122
+ it 'should be reversible' do
123
+ abmv = @mv.ab_multi_value
124
+ mv2 = AddressBook::MultiValued.new(:ab_multi_value => abmv)
125
+ mv2.attributes.should.equal @attributes
126
+ # 3.should.equal 3
127
+ end
128
+
129
+ it 'should not explode' do
130
+ abmv = @mv.ab_multi_value
131
+ mv2 = AddressBook::MultiValued.new(:ab_multi_value => abmv)
132
+ dt = mv2.attributes[1][:date]
133
+ t = NSDate.dateWithNaturalLanguageString('September 22, 1994')
134
+ dt.should.equal t
135
+ dt.should.equal t
136
+ dt.should.equal t
137
+ end
138
+
139
+ describe 'after appending' do
140
+ before do
141
+ @mv << {:label => 'graduation', :date => NSDate.dateWithNaturalLanguageString('June 1, 1983')}
142
+ end
143
+
144
+ it "should have new values" do
145
+ @mv.count.should.equal 4
146
+ end
147
+ end
148
+ end
149
+
150
+ describe 'a broken multi-value' do
151
+ before do
152
+ @attributes = [{:label => 'work', :value => nil}]
153
+ @mv = AddressBook::MultiValued.new(:attributes => @attributes)
154
+ end
155
+
156
+ it 'should ignore the missing entry' do
157
+ @mv.size.should.equal 0
158
+ end
159
+ end
160
+
161
+ describe 'a date multi-value' do
162
+ before do
163
+ @attributes = [{:label => 'anniversary', :date => Time.now}]
164
+ @mv = AddressBook::MultiValued.new(:attributes => @attributes)
165
+ end
166
+
167
+ it 'should round-trip input' do
168
+ @mv.attributes.should.equal @attributes
169
+ end
65
170
  end
66
171
  end
@@ -11,6 +11,9 @@ describe AddressBook::Person do
11
11
  it 'should create but not save in the address book' do
12
12
  @alex.should.be.new_record
13
13
  end
14
+ it 'should have no mod date' do
15
+ @alex.modification_date.should.be.nil
16
+ end
14
17
  it 'should have initial values' do
15
18
  @alex.first_name.should == 'Alex'
16
19
  @alex.last_name.should == 'Testy'
@@ -28,11 +31,15 @@ describe AddressBook::Person do
28
31
  describe 'existing' do
29
32
  before do
30
33
  @email = unique_email
31
- @alex = @ab.create_person(new_alex(@email))
34
+ @origdata = new_alex(@email)
35
+ @alex = @ab.create_person(@origdata)
32
36
  end
33
37
  after do
34
38
  @alex.delete!
35
39
  end
40
+ it 'should have a mod date' do
41
+ @alex.modification_date.should.not.be.nil
42
+ end
36
43
  describe '.find_by_uid' do
37
44
  it 'should find match' do
38
45
  alex = @ab.person(@alex.uid)
@@ -101,6 +108,24 @@ describe AddressBook::Person do
101
108
  @person.delete!
102
109
  end
103
110
  end
111
+
112
+ describe ".replace" do
113
+ before do
114
+ warn "DOING REPLACE"
115
+ @newdata = {
116
+ :first_name => 'Alexander',
117
+ :last_name => "Testy",
118
+ :organization => "Acme, Inc.",
119
+ :emails => [{:label => 'work', :value => @origdata[:emails].first[:value]}]
120
+ }
121
+ @alex.replace(@newdata)
122
+ @alex.save
123
+ end
124
+
125
+ it "should have the new contents" do
126
+ @alex.attributes.should == @newdata
127
+ end
128
+ end
104
129
  end
105
130
 
106
131
  describe '.find_or_new_by_XXX - new or existing' do
@@ -141,6 +166,7 @@ describe AddressBook::Person do
141
166
  :department => 'Development',
142
167
  :organization => 'The Company',
143
168
  :note => 'some important guy',
169
+ :birthday => NSDate.dateWithNaturalLanguageString('July 1, 1982'),
144
170
  # :mobile_phone => '123 456 7890', :office_phone => '987 654 3210',
145
171
  :phones => [
146
172
  {:label => 'mobile', :value => '123 456 7899'},
@@ -157,6 +183,10 @@ describe AddressBook::Person do
157
183
  { :label => 'home page', :value => "http://www.mysite.com/" },
158
184
  { :label => 'work', :value => 'http://dept.bigco.com/' },
159
185
  { :label => 'school', :value => 'http://state.edu/college' }
186
+ ],
187
+ :dates => [
188
+ { :label => 'anniversary', :date => NSDate.dateWithNaturalLanguageString('October 9, 2009') },
189
+ { :label => 'apotheosis', :date => NSDate.dateWithNaturalLanguageString('April 1, 2013') }
160
190
  ]
161
191
  }
162
192
  end
@@ -169,6 +199,7 @@ describe AddressBook::Person do
169
199
  it 'should not be existing' do
170
200
  @ab_person.should.be.new_record
171
201
  @ab_person.should.not.be.exists
202
+ @ab_person.modification_date.should.be.nil
172
203
  end
173
204
 
174
205
  it 'should be able to get each of the single value fields' do
@@ -213,11 +244,26 @@ describe AddressBook::Person do
213
244
  @ab_person.organization.should.equal 'new organization'
214
245
  end
215
246
 
247
+ def empty_image(width, height)
248
+ UIGraphicsBeginImageContext(CGSizeMake(width, height) )
249
+ CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), '#ffffff'.to_color)
250
+ image = UIGraphicsGetImageFromCurrentImageContext()
251
+ UIGraphicsEndImageContext()
252
+ image
253
+ end
254
+
216
255
  it 'should be able to set the photo' do
217
- image = CIImage.emptyImage
218
- data = UIImagePNGRepresentation(UIImage.imageWithCIImage image)
256
+ data = UIImagePNGRepresentation empty_image(1,1)
219
257
  @ab_person.photo = data
220
- UIImagePNGRepresentation(@ab_person.photo).should.equal data
258
+ @ab_person.photo.should.not == nil
259
+ @ab_person.photo.should.equal data
260
+ end
261
+
262
+ it 'should be able to get the photo as an image' do
263
+ data = UIImagePNGRepresentation empty_image(1,1)
264
+ @ab_person.photo = data
265
+ @ab_person.photo_image.size
266
+ @ab_person.photo_image.size.should.equal CGSizeMake(1,1)
221
267
  end
222
268
  end
223
269
 
@@ -243,6 +289,7 @@ describe AddressBook::Person do
243
289
 
244
290
  describe 'once saved' do
245
291
  before do
292
+ @before = Time.now
246
293
  @before_count = AddressBook.count
247
294
  @ab_person.save
248
295
  end
@@ -255,6 +302,11 @@ describe AddressBook::Person do
255
302
  @ab_person.should.be.exists
256
303
  end
257
304
 
305
+ # it 'should populate timestamps' do
306
+ # @ab_person.modification_date.should.not.be.nil
307
+ # should.satisfy {@ab_person.modification_date > @before}
308
+ # end
309
+
258
310
  it "should increment the count" do
259
311
  AddressBook.count.should.equal @before_count+1
260
312
  end
@@ -264,7 +316,7 @@ describe AddressBook::Person do
264
316
  end
265
317
 
266
318
  it 'should have scalar properties' do
267
- [:first_name, :middle_name, :last_name, :job_title, :department, :organization, :note].each do |attr|
319
+ [:first_name, :middle_name, :last_name, :job_title, :department, :organization, :note, :birthday].each do |attr|
268
320
  @ab_person.send(attr).should.equal @attributes[attr]
269
321
  end
270
322
  end
@@ -371,7 +423,8 @@ describe AddressBook::Person do
371
423
  :last_name => 'Whorfin',
372
424
  :organization => 'Acme Inc.',
373
425
  :is_org => true,
374
- :note => 'big important company'
426
+ :note => 'big important company',
427
+ :birthday => NSDate.dateWithNaturalLanguageString('August 17, 1947')
375
428
  )
376
429
  end
377
430
 
@@ -391,6 +444,7 @@ describe AddressBook::Person do
391
444
  @person.getter?('job_title' ).should.be truthy
392
445
  @person.getter?('department' ).should.be truthy
393
446
  @person.getter?('organization').should.be truthy
447
+ @person.getter?('birthday').should.be truthy
394
448
  end
395
449
  it 'should know what is not a getter' do
396
450
  @person.getter?('nonesense' ).should.be falsey
@@ -448,4 +502,69 @@ describe AddressBook::Person do
448
502
  end
449
503
  end
450
504
  end
505
+
506
+ describe "multiple emails/phone #'s handling" do
507
+ it "should accept multiple emails/phone #'s as array of strings for new records" do
508
+ person = @ab.new_person(
509
+ :first_name => 'Ashish',
510
+ :last_name => 'Upadhyay',
511
+ :email => ['a@mail.com','a@mail.com','a@mail.com'],
512
+ :phones => ['1212999222','1212999333','1212999444'],
513
+ )
514
+ person.should.be.new_record
515
+ end
516
+ it "should accept multiple emails/phone #'s as array of hashes for new records" do
517
+ person = @ab.new_person(
518
+ :first_name => 'Ashish',
519
+ :last_name => 'Upadhyay',
520
+ :email => [{ :value => 'a@mail.com' } , { :value => 'a@mail.com' } , { :value => 'a@mail.com' } ] ,
521
+ :phones => [{ :value => '1212999222' } , { :value => '1212999333' } , { :value => '1212999444' } ] ,
522
+ )
523
+ person.should.be.new_record
524
+ end
525
+ it "should accept multiple emails/phone #'s as array of combination of strings or hashes for new records" do
526
+ person = @ab.new_person(
527
+ :first_name => 'Ashish',
528
+ :last_name => 'Upadhyay',
529
+ :email => [ { :value => 'a@mail.com' } , 'a@mail.com' , { :value => 'a@mail.com', :label => 'Office'}] ,
530
+ :phones => [ '1212999222' , { :value => '1212999333', :label => 'Personal' } , { :value => '1212999444' } ] ,
531
+ )
532
+ person.should.be.new_record
533
+ end
534
+ end
535
+
536
+ describe 'vcard' do
537
+ before do
538
+ @alex = @ab.create_person(new_alex(unique_email))
539
+ @jason = @ab.create_person(new_alex('jason@example.com'))
540
+ end
541
+ after do
542
+ @jason.delete!
543
+ @alex.delete!
544
+ end
545
+
546
+ describe '.vcard_for' do
547
+ it 'creates a vcard for a single person' do
548
+ alex_vcard = AddressBook::Person.vcard_for(@alex).to_s
549
+ alex_vcard.should.include? 'BEGIN:VCARD'
550
+ alex_vcard.should.include? "EMAIL;type=INTERNET;type=HOME;type=pref:#{@alex.email}"
551
+ alex_vcard.should.include? 'END:VCARD'
552
+ end
553
+ it 'creates a vcard for an array of people' do
554
+ alex_and_jason_vcard = AddressBook::Person.vcard_for([@alex, @jason]).to_s
555
+ alex_and_jason_vcard.should.include? 'BEGIN:VCARD'
556
+ alex_and_jason_vcard.should.include? "EMAIL;type=INTERNET;type=HOME;type=pref:#{@alex.email}"
557
+ alex_and_jason_vcard.should.include? "EMAIL;type=INTERNET;type=HOME;type=pref:#{@jason.email}"
558
+ alex_and_jason_vcard.should.include? 'END:VCARD'
559
+ end
560
+ end
561
+ describe '#to_vcard' do
562
+ it 'knows how to create vcard for itself' do
563
+ alex_vcard = @alex.to_vcard.to_s
564
+ alex_vcard.should.include? 'BEGIN:VCARD'
565
+ alex_vcard.should.include? "EMAIL;type=INTERNET;type=HOME;type=pref:#{@alex.email}"
566
+ alex_vcard.should.include? 'END:VCARD'
567
+ end
568
+ end
569
+ end
451
570
  end
metadata CHANGED
@@ -1,32 +1,33 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: motion-addressbook
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Alex Rothenberg
9
+ - Jason May
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2013-04-04 00:00:00.000000000 Z
13
+ date: 2013-05-17 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: bubble-wrap
16
17
  requirement: !ruby/object:Gem::Requirement
17
18
  none: false
18
19
  requirements:
19
- - - ! '>='
20
+ - - ~>
20
21
  - !ruby/object:Gem::Version
21
- version: '0'
22
+ version: '1.3'
22
23
  type: :runtime
23
24
  prerelease: false
24
25
  version_requirements: !ruby/object:Gem::Requirement
25
26
  none: false
26
27
  requirements:
27
- - - ! '>='
28
+ - - ~>
28
29
  - !ruby/object:Gem::Version
29
- version: '0'
30
+ version: '1.3'
30
31
  - !ruby/object:Gem::Dependency
31
32
  name: rake
32
33
  requirement: !ruby/object:Gem::Requirement
@@ -62,6 +63,7 @@ dependencies:
62
63
  description: A RubyMotion wrapper around the iOS Address Book framework
63
64
  email:
64
65
  - alex@alexrothenberg.com
66
+ - jmay@pobox.com
65
67
  executables: []
66
68
  extensions: []
67
69
  extra_rdoc_files: []
@@ -72,6 +74,8 @@ files:
72
74
  - LICENSE
73
75
  - README.md
74
76
  - Rakefile
77
+ - abhack/abhack.h
78
+ - abhack/abhack.m
75
79
  - lib/motion-addressbook.rb
76
80
  - lib/motion-addressbook/version.rb
77
81
  - motion-addressbook.gemspec
@@ -82,6 +86,7 @@ files:
82
86
  - motion/address_book/multi_valued.rb
83
87
  - motion/address_book/person.rb
84
88
  - motion/address_book/picker.rb
89
+ - motion/address_book/source.rb
85
90
  - spec/address_book/group_spec.rb
86
91
  - spec/address_book/multi_value_spec.rb
87
92
  - spec/address_book/multi_valued_spec.rb
@@ -104,7 +109,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
104
109
  version: '0'
105
110
  segments:
106
111
  - 0
107
- hash: 3256350264444301144
112
+ hash: -259703607999042735
108
113
  required_rubygems_version: !ruby/object:Gem::Requirement
109
114
  none: false
110
115
  requirements:
@@ -113,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
118
  version: '0'
114
119
  segments:
115
120
  - 0
116
- hash: 3256350264444301144
121
+ hash: -259703607999042735
117
122
  requirements: []
118
123
  rubyforge_project:
119
124
  rubygems_version: 1.8.24