has_vcards 0.20.3 → 1.0.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. data/MIT-LICENSE +4 -1
  2. data/README.md +18 -0
  3. data/Rakefile +28 -0
  4. data/app/assets/javascripts/has_vcards/application.js +13 -0
  5. data/app/assets/stylesheets/has_vcards/application.css +15 -0
  6. data/app/controllers/has_vcards/directory_lookup_controller.rb +21 -0
  7. data/app/controllers/has_vcards/phone_numbers_controller.rb +16 -0
  8. data/app/controllers/has_vcards/vcards_controller.rb +22 -0
  9. data/app/helpers/has_vcards/application_helper.rb +18 -0
  10. data/app/input/has_vcards/zip_locality_input.rb +16 -0
  11. data/app/models/has_vcards/address.rb +42 -0
  12. data/app/models/has_vcards/concerns/has_vcards.rb +42 -0
  13. data/app/models/has_vcards/phone_number.rb +61 -0
  14. data/app/models/has_vcards/vcard.rb +123 -0
  15. data/app/models/has_vcards/vcard/directory_address.rb +25 -0
  16. data/app/models/has_vcards/vcard/directory_lookup.rb +123 -0
  17. data/app/views/has_vcards/layouts/has_vcards/application.html.erb +14 -0
  18. data/app/views/{people → has_vcards/people}/_address_form.html.haml +1 -1
  19. data/app/views/{phone_numbers → has_vcards/phone_numbers}/_form.html.haml +0 -0
  20. data/app/views/{phone_numbers → has_vcards/phone_numbers}/_list.html.haml +0 -0
  21. data/app/views/{phone_numbers → has_vcards/phone_numbers}/_nested_form.html.haml +0 -0
  22. data/app/views/{phone_numbers → has_vcards/phone_numbers}/_phone_number.html.haml +0 -0
  23. data/app/views/{vcards → has_vcards/vcards}/_address.html.haml +0 -0
  24. data/app/views/{vcards → has_vcards/vcards}/_directory_lookup.html.haml +0 -0
  25. data/app/views/{vcards → has_vcards/vcards}/_directory_matches.html.haml +0 -0
  26. data/app/views/{vcards → has_vcards/vcards}/_form.html.haml +1 -1
  27. data/app/views/{vcards → has_vcards/vcards}/_show.html.haml +1 -1
  28. data/app/views/{vcards → has_vcards/vcards}/directory_lookup.js.erb +0 -0
  29. data/config/locales/de.yml +11 -18
  30. data/config/locales/en.yml +21 -6
  31. data/config/locales/fr.yml +6 -18
  32. data/config/routes.rb +1 -1
  33. data/db/migrate/20121113120000_create_honorific_prefixes_table.rb +1 -1
  34. data/db/migrate/20140617113710_rename_tables_for_namespacing.rb +8 -0
  35. data/db/migrate/20140619101337_remove_object_from_phone_number.rb +6 -0
  36. data/db/migrate/20140619102041_rename_vcard_object_to_reference.rb +6 -0
  37. data/db/migrate/20140619121756_refactor_honorific_prefix.rb +5 -0
  38. data/lib/has_vcards.rb +5 -18
  39. data/lib/has_vcards/engine.rb +29 -0
  40. data/lib/has_vcards/version.rb +4 -1
  41. data/spec/controllers/has_vcards/phone_numbers_controller_spec.rb +42 -0
  42. data/spec/controllers/has_vcards/vcards_controller_spec.rb +42 -0
  43. data/spec/dummy/README.rdoc +28 -0
  44. data/spec/dummy/Rakefile +6 -0
  45. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  46. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  47. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  48. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  49. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  50. data/spec/dummy/bin/bundle +3 -0
  51. data/spec/dummy/bin/rails +4 -0
  52. data/spec/dummy/bin/rake +4 -0
  53. data/spec/dummy/config.ru +4 -0
  54. data/spec/dummy/config/application.rb +29 -0
  55. data/spec/dummy/config/boot.rb +5 -0
  56. data/spec/dummy/config/database.yml +37 -0
  57. data/spec/dummy/config/environment.rb +5 -0
  58. data/spec/dummy/config/environments/development.rb +37 -0
  59. data/spec/dummy/config/environments/production.rb +83 -0
  60. data/spec/dummy/config/environments/test.rb +39 -0
  61. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  62. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  63. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  64. data/spec/dummy/config/initializers/inflections.rb +16 -0
  65. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  66. data/spec/dummy/config/initializers/session_store.rb +3 -0
  67. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  68. data/spec/dummy/config/locales/en.yml +23 -0
  69. data/spec/dummy/config/routes.rb +4 -0
  70. data/spec/dummy/config/secrets.yml +22 -0
  71. data/spec/dummy/db/development.sqlite3 +0 -0
  72. data/spec/dummy/db/migrate/20140620151641_create_somethings.rb +7 -0
  73. data/spec/dummy/db/schema.rb +66 -0
  74. data/spec/dummy/db/test.sqlite3 +0 -0
  75. data/spec/dummy/log/development.log +2513 -0
  76. data/spec/dummy/log/test.log +11671 -0
  77. data/spec/dummy/public/404.html +67 -0
  78. data/spec/dummy/public/422.html +67 -0
  79. data/spec/dummy/public/500.html +66 -0
  80. data/spec/dummy/public/favicon.ico +0 -0
  81. data/spec/factories/addresses.rb +7 -0
  82. data/spec/factories/phone_numbers.rb +7 -0
  83. data/spec/factories/vcards.rb +5 -0
  84. data/spec/models/has_vcards/address_spec.rb +58 -0
  85. data/spec/models/has_vcards/concerns/has_vcards_spec.rb +50 -0
  86. data/spec/models/has_vcards/phone_number_spec.rb +82 -0
  87. data/spec/models/has_vcards/vcard_spec.rb +172 -0
  88. data/spec/rails_helper.rb +18 -0
  89. data/spec/spec_helper.rb +74 -0
  90. metadata +262 -47
  91. data/README.markdown +0 -14
  92. data/app/controllers/directory_lookup_controller.rb +0 -17
  93. data/app/controllers/phone_numbers_controller.rb +0 -14
  94. data/app/controllers/vcards_controller.rb +0 -20
  95. data/app/helpers/has_vcards_helper.rb +0 -13
  96. data/app/input/zip_locality_input.rb +0 -14
  97. data/app/models/address.rb +0 -40
  98. data/app/models/honorific_prefix.rb +0 -6
  99. data/app/models/phone_number.rb +0 -43
  100. data/app/models/vcard.rb +0 -112
  101. data/app/models/vcard/directory_address.rb +0 -21
  102. data/app/models/vcard/directory_lookup.rb +0 -122
  103. data/config/application.rb +0 -19
  104. data/config/boot.rb +0 -6
  105. data/lib/has_vcards/railtie.rb +0 -19
data/MIT-LICENSE CHANGED
@@ -1,4 +1,7 @@
1
- Copyright (c) 2008 [name of plugin creator]
1
+ Copyright 2007-2014 Simon Hürlimann <simon.huerlimann@cyt.ch>
2
+ Copyright 2009-2014 CyT GmbH <http://www.cyt.ch>
3
+ Copyright 2008 Agrabah GmbH <http://www.agrabah.ch>
4
+ Copyright 2007-2010 ZytoLabor <http://www.zyto-labor.com>
2
5
 
3
6
  Permission is hereby granted, free of charge, to any person obtaining
4
7
  a copy of this software and associated documentation files (the
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ has_vcards
2
+ ==========
3
+
4
+ [![Build Status](https://secure.travis-ci.org/huerlisi/has_vcards.png)](http://travis-ci.org/huerlisi/has_vcards)
5
+
6
+ Rails plugin providing VCard like contact and address models and helpers.
7
+
8
+ Install
9
+ =======
10
+
11
+ In Rails simply add
12
+
13
+ gem 'has_vcards'
14
+
15
+ License
16
+ =======
17
+
18
+ Released under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'HasVcards'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('{app,config,db,lib}/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path('../spec/dummy/Rakefile', __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require 'rspec/core'
23
+ require 'rspec/core/rake_task'
24
+
25
+ desc 'Run all specs in spec directory (excluding plugin specs)'
26
+ RSpec::Core::RakeTask.new(spec: 'app:db:test:prepare')
27
+
28
+ task default: :spec
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,21 @@
1
+ module HasVcards
2
+ class DirectoryLookupController < ApplicationController
3
+ def search
4
+ @selector = params[:selector]
5
+ vcard_params = extract_vcard_params(params, @selector)
6
+
7
+ @vcard = Vcard.new(vcard_params)
8
+ render 'vcards/directory_lookup'
9
+ end
10
+
11
+ private
12
+
13
+ def extract_vcard_params(params, selector)
14
+ keys = selector.delete(']').split('[')
15
+ vcard_params = params
16
+ keys.each { |key| vcard_params = vcard_params[key] }
17
+
18
+ vcard_params
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ module HasVcards
2
+ class PhoneNumbersController < ApplicationController
3
+ # Use inherited resources logic
4
+ inherit_resources
5
+ respond_to :html, :js
6
+
7
+ # Relations
8
+ optional_belongs_to :vcard
9
+
10
+ # Optionally support in place editing
11
+ if respond_to? :in_place_edit_for
12
+ in_place_edit_for :phone_number, :phone_number_type
13
+ in_place_edit_for :phone_number, :number
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module HasVcards
2
+ class VcardsController < ApplicationController
3
+ # Use inherited resources logic
4
+ inherit_resources
5
+ respond_to :html, :js
6
+
7
+ def directory_lookup
8
+ @vcard = Vcard.find(params[:id])
9
+ end
10
+
11
+ def directory_update
12
+ @vcard = Vcard.find(params[:id])
13
+ new_params = params[:vcard].select { |key, _| ['family_name', 'given_name', 'street_address', 'postal_code', 'locality'].include?(key) }
14
+
15
+ @vcard.update_attributes(new_params)
16
+
17
+ @vcard.save
18
+
19
+ redirect_to @vcard.reference
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ module HasVcards
2
+ # View helpers
3
+ #
4
+ # These helpers are available by default.
5
+ module ApplicationHelper
6
+ def address(vcard, line_separator = '<br/>')
7
+ vcard.address_lines.map { |line| h(line) }.join(line_separator).html_safe
8
+ end
9
+
10
+ def full_address(vcard, line_separator = '<br/>')
11
+ vcard.full_address_lines.map { |line| h(line) }.join(line_separator).html_safe
12
+ end
13
+
14
+ def contact(vcard, line_separator = '<br/>', label_separator = ' ')
15
+ vcard.contact_lines(label_separator).map { |line| h(line) }.join(line_separator).html_safe
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ module HasVcards
2
+ class ZipLocalityInput < SimpleForm::Inputs::StringInput
3
+ def zip_codes
4
+ SwissMatch.zip_codes.map { |zip| zip.to_s }.inspect
5
+ end
6
+
7
+ def input
8
+ input_html_options[:type] = 'text'
9
+ input_html_options[:autocomplete] = 'off'
10
+ input_html_options['data-provide'] = 'typeahead'
11
+ input_html_options['data-source'] = zip_codes
12
+
13
+ super
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+
3
+ module HasVcards
4
+ class Address < ActiveRecord::Base
5
+ # Access restrictions
6
+ attr_accessible :extended_address, :street_address, :post_office_box, :postal_code, :locality, :zip_locality if defined?(ActiveModel::MassAssignmentSecurity)
7
+
8
+ belongs_to :vcard
9
+
10
+ # Validations
11
+ include I18nHelpers
12
+
13
+ def validate_address
14
+ errors.add_on_blank(:postal_code)
15
+ errors.add_on_blank(:locality)
16
+
17
+ return unless street_address.blank? && extended_address.blank? && post_office_box.blank?
18
+
19
+ errors.add(:street_address, "#{t_attr(:street_address, Vcard)} #{I18n.translate('errors.messages.empty')}")
20
+ errors.add(:extended_address, "#{t_attr(:extended_address, Vcard)} #{I18n.translate('errors.messages.empty')}")
21
+ errors.add(:post_office_box, "#{t_attr(:post_office_box, Vcard)} #{I18n.translate('errors.messages.empty')}")
22
+ end
23
+
24
+ # Helpers
25
+ def to_s
26
+ I18n.translate('has_vcards.address.to_s',
27
+ street_address: street_address,
28
+ postal_code: postal_code,
29
+ locality: locality
30
+ )
31
+ end
32
+
33
+ # Composed attributes
34
+ def zip_locality
35
+ "#{postal_code} #{locality}"
36
+ end
37
+
38
+ def zip_locality=(value)
39
+ self.postal_code, self.locality = value.split(' ', 2)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ module HasVcards
2
+ module Concerns
3
+ # ActiveRecord extensions
4
+ #
5
+ # Including this module in your ActiveRecord classes sets up associations and helpers to integrate Vcards with that model.
6
+ #
7
+ # Use something like this to use it:
8
+ #
9
+ # class Something < ActiveRecord::Base
10
+ # include HasVcards::Concerns::HasVcards
11
+ # end
12
+ module HasVcards
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ scope :by_name, ->(name) { { include: :vcard, order: 'vcards.full_name', conditions: Vcard.by_name_conditions(name) } }
17
+
18
+ # Vcards
19
+ has_many :vcards, class_name: 'HasVcards::Vcard', as: 'reference', autosave: true, validate: true
20
+ accepts_nested_attributes_for :vcards
21
+
22
+ # Single/Main vcard
23
+ has_one :vcard, class_name: 'HasVcards::Vcard', as: 'reference', autosave: true, validate: true
24
+ accepts_nested_attributes_for :vcard
25
+
26
+ delegate :full_name, :nickname, :family_name, :given_name, :additional_name, :honorific_prefix, :honorific_suffix, to: :vcard
27
+ delegate :full_name=, :nickname=, :family_name=, :given_name=, :additional_name=, :honorific_prefix=, :honorific_suffix=, to: :vcard
28
+
29
+ def vcard_with_autobuild
30
+ vcard_without_autobuild || build_vcard
31
+ end
32
+ alias_method_chain :vcard, :autobuild
33
+
34
+ # Access restrictions
35
+ if defined?(ActiveModel::MassAssignmentSecurity)
36
+ attr_accessible :full_name, :nickname, :family_name, :given_name, :honorific_prefix, :honorific_suffix,
37
+ :vcard_attributes, :vcards_attributes
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,61 @@
1
+ # Phone Number / Contact model
2
+ #
3
+ # This model holds a contact for a Vcard. It is called PhoneNumber in
4
+ # compliance with the vCard spec, but can hold any kind of contact like mobile
5
+ # phone, email, or homepage.
6
+ #
7
+ # There can be multiple phone numbers be assigned to a vcard, where they are
8
+ # available as the #contacts association.
9
+ #
10
+ # The #label method uses the I18n locales to translate the phone_number_type.
11
+ # You may add additional ones in under the
12
+ # 'activerecord.attributes.has_vcards/phone_number.phone_number_type_enum'
13
+ # scope.
14
+ module HasVcards
15
+ class PhoneNumber < ActiveRecord::Base
16
+ # Access restrictions
17
+ attr_accessible :phone_number_type, :number if defined?(ActiveModel::MassAssignmentSecurity)
18
+
19
+ # Vcard association
20
+ belongs_to :vcard, inverse_of: :contacts
21
+
22
+ # Validation
23
+ validates_presence_of :number
24
+
25
+ # phone number types
26
+ scope :by_type, ->(value) { where(phone_number_type: value) }
27
+ scope :phone, by_type('phone')
28
+ scope :fax, by_type('fax')
29
+ scope :mobile, by_type('mobile')
30
+ scope :email, by_type('email')
31
+
32
+ def label
33
+ I18n.translate(phone_number_type, scope: 'activerecord.attributes.has_vcards/phone_number.phone_number_type_enum', default: phone_number_type.titleize)
34
+ end
35
+
36
+ # String
37
+ def to_s(format = :default, separator = ': ')
38
+ case format
39
+ when :label
40
+ return [label, number].compact.join(separator)
41
+ else
42
+ return number
43
+ end
44
+ end
45
+
46
+ # Generate a contact URL
47
+ def to_url
48
+ scheme =
49
+ case phone_number_type
50
+ when 'phone' then 'tel'
51
+ when 'mobile' then 'tel'
52
+ when 'fax' then 'fax'
53
+ when 'email' then 'mailto'
54
+ end
55
+
56
+ return unless scheme
57
+
58
+ "#{scheme}:#{number}"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,123 @@
1
+ # encoding: utf-8
2
+
3
+ # Vcard class
4
+ #
5
+ # This is the main model containing vcards information. It can be assigned to
6
+ # any kind of model by assigning the 'reference'.
7
+ #
8
+ # To include in a model, you may use the 'has_vcards' helper:
9
+ #
10
+ # class Something < ActiveRecord::Base
11
+ # has_vcards
12
+ # end
13
+ module HasVcards
14
+ class Vcard < ActiveRecord::Base
15
+ # Reference
16
+ belongs_to :reference, polymorphic: true
17
+
18
+ # Addresses
19
+ has_one :address, autosave: true, validate: true
20
+ has_many :addresses, autosave: true, validate: true
21
+
22
+ accepts_nested_attributes_for :addresses
23
+ accepts_nested_attributes_for :address
24
+
25
+ delegate :post_office_box, :extended_address, :street_address, :locality, :region, :postal_code, :country_name, :zip_locality, to: :address
26
+ delegate :post_office_box=, :extended_address=, :street_address=, :locality=, :region=, :postal_code=, :country_name=, :zip_locality=, to: :address
27
+
28
+ def address_with_autobuild
29
+ address_without_autobuild || build_address
30
+ end
31
+ alias_method_chain :address, :autobuild
32
+
33
+ # Contacts
34
+ has_many :contacts, class_name: 'PhoneNumber', inverse_of: :vcard do
35
+ def build_defaults
36
+ # TODO: i18nify
37
+ ['Tel. geschäft', 'Tel. privat', 'Handy', 'E-Mail'].each do |phone_number_type|
38
+ next if select { |contact| contact.phone_number_type == phone_number_type }.present?
39
+ build(phone_number_type: phone_number_type)
40
+ end
41
+ end
42
+ end
43
+
44
+ accepts_nested_attributes_for :contacts,
45
+ reject_if: proc { |attributes| attributes['number'].blank? },
46
+ allow_destroy: true
47
+
48
+ # Access restrictions
49
+ if defined?(ActiveModel::MassAssignmentSecurity)
50
+ attr_accessible :full_name, :nickname, :address_attributes, :family_name, :given_name,
51
+ :post_office_box, :extended_address, :street_address, :locality, :region,
52
+ :postal_code, :country_name, :zip_locality, :contacts_attributes,
53
+ :honorific_prefix, :honorific_suffix
54
+ end
55
+
56
+ # SwissMatch
57
+ include Vcard::DirectoryLookup
58
+
59
+ scope :active, conditions: { active: true }
60
+ scope :by_name, ->(name) { { conditions: by_name_conditions(name) } }
61
+ scope :with_address, joins(:address).includes(:address)
62
+
63
+ # Validations
64
+ include I18nHelpers
65
+
66
+ def validate_name
67
+ return if full_name.present?
68
+
69
+ errors.add(:full_name, "#{t_attr(:full_name, Vcard)} #{I18n.translate('errors.messages.empty')}")
70
+ errors.add(:family_name, "#{t_attr(:family_name, Vcard)} #{I18n.translate('errors.messages.empty')}")
71
+ errors.add(:given_name, "#{t_attr(:given_name, Vcard)} #{I18n.translate('errors.messages.empty')}")
72
+ end
73
+
74
+ # Convenience accessors
75
+ def full_name
76
+ result = read_attribute(:full_name)
77
+ result ||= [ family_name, given_name ].compact.join(' ')
78
+
79
+ result
80
+ end
81
+
82
+ def abbreviated_name
83
+ return read_attribute(:full_name) if read_attribute(:full_name)
84
+
85
+ [given_name.try(:first).try(:upcase), family_name].compact.join('. ')
86
+ end
87
+
88
+ # Advanced finders
89
+ def self.by_name_conditions(name)
90
+ ['vcards.full_name LIKE :name OR vcards.family_name LIKE :name OR vcards.given_name LIKE :name OR vcards.nickname LIKE :name', { name: name }]
91
+ end
92
+
93
+ def self.find_by_name(name)
94
+ find :first, conditions: by_name_conditions(name)
95
+ end
96
+
97
+ def self.find_all_by_name(name)
98
+ find :all, conditions: by_name_conditions(name)
99
+ end
100
+
101
+ # Helper methods
102
+ def address_lines
103
+ lines = [extended_address, street_address, post_office_box, "#{postal_code} #{locality}"]
104
+
105
+ # Only return non-empty lines
106
+ lines.reject(&:blank?).compact.map(&:strip)
107
+ end
108
+
109
+ def full_address_lines
110
+ lines = [honorific_prefix, full_name] + address_lines
111
+
112
+ # Only return non-empty lines
113
+ lines.reject(&:blank?).compact.map(&:strip)
114
+ end
115
+
116
+ def contact_lines
117
+ lines = contacts
118
+
119
+ # Only return non-empty lines
120
+ lines.map(&:to_s).reject(&:blank?).compact.map(&:strip)
121
+ end
122
+ end
123
+ end