motion-addressbook 1.2.0 → 1.4.0

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