tact 1.2.23 → 2.0.17

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 646f2aeb31eeec006ea63f14c1e323cd8f4943c4
4
- data.tar.gz: 03ae8aee41865c91c1052b986b261c6077f021b3
3
+ metadata.gz: 0c97568e1e4da1c76294dd69bdfc31af58cd5ded
4
+ data.tar.gz: 887f421efa7a7fb0a5272f798706518403a4fbb2
5
5
  SHA512:
6
- metadata.gz: 22c70b517f55076452ed7580571c8d609ef11608a3cd8a9f26d192ff6eaf401b0c6d2adb0fdd6ebeb6a7f200d13bd6689748f442b4755370a1916b9f351037bb
7
- data.tar.gz: 5bb758c735ccf9df330fa73efa6def114e46543843b7d79313b896022a8cf60e2bf027240935086b6144bb7d19e1c0f7417afcf246a16b02edfa6ab5b97c97d7
6
+ metadata.gz: d65d88384b495953b11699693eae7179b22a0af7cdfa818079768da6b26e8f7e7e72d1a6046eb1350eb0eec9e404721956a471906860dd4c7ae75400cf78adfd
7
+ data.tar.gz: 611fcd81f8c5d8d0b451065607195adde2380be76db5640dd0f3d908f2f4133d83b12fce69cc33a97ecee86caac9f34e594383e82463661a42e0bc4a41ee2da8
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ client_secret.json
data/README.md CHANGED
@@ -11,6 +11,7 @@ A command line rolodex.
11
11
  ```
12
12
  -v Current version
13
13
  -h Help
14
+ -s Sync Google contacts
14
15
 
15
16
  <param> Search by name
16
17
  -p <param> Search by number
@@ -28,7 +29,7 @@ A command line rolodex.
28
29
 
29
30
  ## Contributing
30
31
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/ollieshmollie/tact.
32
+ Bug reports and pull requests are welcome on GitHub at https://github.com/olishmollie/tact.
32
33
 
33
34
 
34
35
  ## License
data/Rakefile CHANGED
@@ -1,2 +1,95 @@
1
- require "bundler/gem_tasks"
2
- task :default => :spec
1
+ require 'bundler/gem_tasks'
2
+ require 'fileutils'
3
+
4
+ task :environment do
5
+ require 'tact'
6
+ end
7
+
8
+ namespace :generate do
9
+ desc "Create an empty migration in db/migrate, e.g., rake generate:migration NAME=create_tasks"
10
+ task :migration => :environment do
11
+ unless ENV.has_key?('NAME')
12
+ raise "Must specificy migration name, e.g., rake generate:migration NAME=create_tasks"
13
+ end
14
+
15
+ name = ENV['NAME'].camelize
16
+ filename = "%s_%s.rb" % [Time.now.strftime('%Y%m%d%H%M%S'), ENV['NAME'].underscore]
17
+ path = File.join('db', 'migrate', filename)
18
+
19
+ if File.exist?(path)
20
+ raise "ERROR: File '#{path}' already exists"
21
+ end
22
+
23
+ puts "Creating #{path}"
24
+ File.open(path, 'w+') do |f|
25
+ f.write(<<-EOF.strip_heredoc)
26
+ class #{name} < ActiveRecord::Migration
27
+ def change
28
+ end
29
+ end
30
+ EOF
31
+ end
32
+ end
33
+ end
34
+
35
+ task :make_tact_dir do
36
+ if !File.exists?("#{File.expand_path('~')}/.tact")
37
+ FileUtils.mkdir("#{File.expand_path('~')}/.tact")
38
+ end
39
+ end
40
+
41
+ namespace :db do
42
+ desc "Create databases"
43
+ task :create => [:environment, :make_tact_dir] do
44
+ SQLite3::Database.new(DEV_DB)
45
+ SQLite3::Database.new(TEST_DB)
46
+ end
47
+
48
+ desc "Drop databases"
49
+ task :drop => :environment do
50
+ FileUtils.rm DEV_DB
51
+ FileUtils.rm TEST_DB
52
+ end
53
+
54
+ desc "Run migrations"
55
+ task :migrate => :environment do
56
+ ActiveRecord::Migrator.migrations_paths << File.dirname(__FILE__) + 'db/migrate'
57
+ ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
58
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
59
+ ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
60
+ end
61
+ end
62
+
63
+ desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).'
64
+ task :rollback => :environment do
65
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
66
+ ActiveRecord::Migrator.rollback MIGRATIONS_DIR, step
67
+ end
68
+
69
+ desc "Retrieves the current schema version number"
70
+ task :version do
71
+ puts "Current version: #{ActiveRecord::Migrator.current_version}"
72
+ end
73
+ end
74
+
75
+ desc "Run specs in test environment"
76
+ task :spec => :environment do
77
+ sh 'rspec'
78
+ end
79
+
80
+ desc "Open console with this library required"
81
+ task :console do
82
+ sh 'irb -I lib -r tact.rb'
83
+ end
84
+
85
+ desc "Uninstall local version of gem"
86
+ task :uninstall do
87
+ sh 'yes | gem uninstall tact'
88
+ sh 'rm tact-*' unless Dir.glob('./tact-*').empty?
89
+ end
90
+
91
+ desc "Build and install local gem version"
92
+ task :build => :uninstall do
93
+ sh 'gem build tact.gemspec && gem install tact'
94
+ end
95
+
data/bin/tact CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'tact'
3
3
 
4
- Tact::Tact.new(ARGV).run
4
+ Tact::Tact.new(ARGV).run
@@ -0,0 +1,17 @@
1
+ class CreateTables < ActiveRecord::Migration[4.2]
2
+ def change
3
+ create_table :contacts do |t|
4
+ t.string :first_name, limit: 20
5
+ t.string :last_name, limit: 20
6
+
7
+ t.timestamps
8
+ end
9
+ create_table :phone_numbers do |t|
10
+ t.string :number, limit: 15
11
+ t.string :type, limit: 10
12
+ end
13
+ create_table :emails do |t|
14
+ t.string :address, limit: 50
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ class AddForeignKeys < ActiveRecord::Migration[4.2]
2
+ def change
3
+ add_column :phone_numbers, :contact_id, :integer
4
+ add_column :emails, :contact_id, :integer
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class ChangeTypeOnPhoneNumbers < ActiveRecord::Migration[4.2]
2
+ def change
3
+ rename_column :phone_numbers, :type, :kind
4
+ end
5
+ end
data/lib/tact.rb CHANGED
@@ -1 +1,50 @@
1
- require 'tact/tact'
1
+ # require needed gems
2
+ require 'fileutils'
3
+ require 'active_record'
4
+ require 'sqlite3'
5
+ require 'colored'
6
+
7
+ # require lib files
8
+ require 'tact/authorizable'
9
+ require 'tact/card'
10
+ require 'tact/contact'
11
+ require 'tact/email'
12
+ require 'tact/google_client'
13
+ require 'tact/phone_number'
14
+ require 'tact/rolodex'
15
+ require 'tact/tact'
16
+ require 'tact/version'
17
+
18
+
19
+ APP_ROOT ||= File.join(File.dirname(__FILE__), '../')
20
+ DEV_DB ||= File.join(File.expand_path('~'), '.tact', 'tact.sqlite3')
21
+ TEST_DB ||= File.join(File.expand_path('~'), '.tact', 'tact_test.sqlite3')
22
+ MIGRATIONS_DIR ||= 'db/migrate'
23
+ CLIENT_SECRET ||= File.join(APP_ROOT, 'client_secret.json')
24
+
25
+ # tells AR what db file to use
26
+ if ENV['GEM_ENV'] == 'test'
27
+ ActiveRecord::Base.establish_connection(
28
+ :adapter => 'sqlite3',
29
+ :database => TEST_DB
30
+ )
31
+ else
32
+ ActiveRecord::Base.establish_connection(
33
+ :adapter => 'sqlite3',
34
+ :database => DEV_DB
35
+ )
36
+ end
37
+
38
+ # Make tact directory
39
+ if !File.exists?("#{File.expand_path('~')}/.tact")
40
+ FileUtils.mkdir("#{File.expand_path('~')}/.tact")
41
+ end
42
+
43
+ # Create database
44
+ SQLite3::Database.new(DEV_DB)
45
+ SQLite3::Database.new(TEST_DB)
46
+
47
+ # Run migrations
48
+ ActiveRecord::Migrator.migrations_paths << APP_ROOT + 'db/migrate'
49
+ ActiveRecord::Migration.verbose = false
50
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths)
@@ -0,0 +1,10 @@
1
+ module Authorizable
2
+ def authorize
3
+ system("oauth2l fetch --json #{CLIENT_SECRET} https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/contacts.readonly")
4
+ end
5
+
6
+ def authorized?
7
+ credentials = File.join(File.expand_path('~'), '.oauth2l.token')
8
+ File.exists?(credentials) && !File.zero?(credentials)
9
+ end
10
+ end
data/lib/tact/card.rb CHANGED
@@ -1,21 +1,17 @@
1
- require_relative 'contact'
2
-
3
1
  module Tact
4
2
  class Card
5
3
  attr_reader :contact
6
4
  def initialize(contact, index="*")
7
5
  @contact = contact
8
6
  @index = index
9
- @phone_numbers = contact.phone_numbers
10
- @emails = contact.emails
11
7
  end
12
8
 
13
9
  def to_s
14
10
  string = "=" * 40 + "\n"
15
11
  string += "[#{@index}]".red + " #{@contact.to_s}"
16
- @phone_numbers.each_with_index {|number, i| string += "\s\s" + "[#{i + 1}] " + number.to_s + "\n"}
17
- @emails.each_with_index {|address, i| string += "\s\s\s\s" + "[#{i + 1}] " + address.to_s + "\n"}
12
+ contact.phone_numbers.each_with_index {|number, i| string += "\s\s" + "[#{i + 1}] " + number.to_s + "\n"}
13
+ contact.emails.each_with_index {|address, i| string += "\s\s\s\s" + "[#{i + 1}] " + address.to_s + "\n"}
18
14
  string += "=" * 40 + "\n"
19
15
  end
20
16
  end
21
- end
17
+ end
data/lib/tact/contact.rb CHANGED
@@ -1,74 +1,12 @@
1
- require 'colored'
2
- require_relative 'phone_number'
3
- require_relative "email"
4
-
5
1
  module Tact
6
- class Contact
7
- attr_reader :id
8
- attr_accessor :index, :first_name, :last_name
9
-
10
- @@db = Database.new.db
11
-
12
- def self.from_hash(hash)
13
- contact = self.new(hash["first_name"], hash["last_name"], hash["id"])
14
- end
15
-
16
- def self.all
17
- contact_hashes = @@db.execute("select * from contacts order by last_name asc, first_name asc;")
18
- contact_hashes.each_with_index.map {|c_hash, index| self.from_hash(c_hash) }
19
- end
20
-
21
- def self.find_by_id(id)
22
- contact_hashes = @@db.execute("select * from contacts where id = ?", [id])
23
- self.from_hash(contact_hashes[0]) if !contact_hashes.empty?
24
- end
25
-
26
- def self.find_by_first_name(first_name)
27
- results = @@db.execute("select * from contacts where upper(first_name) = upper(?);", [first_name])
28
- results.map {|c_hash| self.from_hash(c_hash) }
29
- end
30
-
31
- def self.find_by_last_name(last_name)
32
- results = @@db.execute("select * from contacts where upper(last_name) = upper(?);", [last_name])
33
- results.map {|c_hash| self.from_hash(c_hash) }
34
- end
35
-
36
- def self.delete(id)
37
- @@db.execute("delete from contacts where id = ?;", [id])
38
- @@db.execute("select changes();")[0]["changes()"] == 1 ? true : false
39
- end
40
-
41
- def initialize(first_name, last_name, primary_key=nil)
42
- @id = primary_key
43
- @first_name = first_name.downcase.capitalize
44
- @last_name = last_name.downcase.capitalize
45
- end
46
-
47
- def save
48
- begin
49
- if @id == nil
50
- if @@db.execute("insert into contacts (first_name, last_name) values (?, ?);", [@first_name, @last_name])
51
- @id = @@db.execute("select last_insert_rowid()")[0]["last_insert_rowid()"]
52
- self
53
- else
54
- false
55
- end
56
- else
57
- @@db.execute("update contacts set first_name = ?, last_name = ? where id = ?;", [@first_name, @last_name, @id]) ? self : false
58
- end
59
- rescue
60
- puts "Error: Contact already exists".red
61
- end
62
- end
63
-
64
- def phone_numbers
65
- number_hashes = @@db.execute("select * from phone_numbers where contact_id = ? order by type asc;", [@id])
66
- number_hashes.map {|n_hash| PhoneNumber.from_hash(n_hash) }
67
- end
2
+ class Contact < ActiveRecord::Base
3
+ has_many :phone_numbers, dependent: :destroy
4
+ has_many :emails, dependent: :destroy
5
+ validates :last_name, uniqueness: { scope: :first_name }
68
6
 
69
- def emails
70
- email_hashes = @@db.execute("select * from emails where contact_id = ?", [@id])
71
- email_hashes.map {|e_hash| Email.from_hash(e_hash) }
7
+ before_validation do
8
+ first_name.upcase!
9
+ last_name.upcase!
72
10
  end
73
11
 
74
12
  def full_name
@@ -79,4 +17,4 @@ module Tact
79
17
  "#{full_name}\n".green.bold
80
18
  end
81
19
  end
82
- end
20
+ end
data/lib/tact/email.rb CHANGED
@@ -1,57 +1,10 @@
1
- require_relative 'database'
2
-
3
1
  module Tact
4
- class Email
5
- attr_reader :id
6
- attr_accessor :address, :index
7
-
8
- @@db = Database.new.db
9
-
10
- def self.from_hash(hash)
11
- self.new(hash["address"], hash["contact_id"], hash["id"])
12
- end
13
-
14
- def self.all
15
- email_hashes = @@db.execute("select * from emails;")
16
- email_hashes.map {|e_hash| Email.from_hash(e_hash) }
17
- end
18
-
19
- def self.find_by_id(id)
20
- email_hashes = @@db.execute("select * from emails where id = ?", [id])
21
- self.from_hash(email_hashes[0]) if !email_hashes.empty?
22
- end
23
-
24
- def self.find_by_address(address)
25
- email_hashes = @@db.execute("select * from emails where address = ?", [address])
26
- email_hashes.map {|e_hash| self.from_hash(e_hash) }
27
- end
28
-
29
- def self.delete(id)
30
- @@db.execute("delete from emails where id = ?;", [id])
31
- @@db.execute("select changes();")[0]["changes()"] == 1 ? true : false
32
- end
33
-
34
- def initialize(address, contact_id, primary_key=nil)
35
- @id = primary_key
36
- @address = address
37
- @contact_id = contact_id
38
- end
39
-
40
- def save
41
- if @id == nil
42
- if @@db.execute("insert into emails (address, contact_id) values (?, ?);", [@address, @contact_id])
43
- @id = @@db.execute("select last_insert_rowid();")[0]["last_insert_rowid()"]
44
- self
45
- else
46
- false
47
- end
48
- else
49
- @@db.execute("update emails set address = ?, contact_id = ? where id = ?;", [@address, @contact_id, @id]) ? self : false
50
- end
51
- end
2
+ class Email < ActiveRecord::Base
3
+ belongs_to :contact
52
4
 
53
5
  def to_s
54
- "<#{@address}>"
6
+ "<#{address}>"
55
7
  end
8
+
56
9
  end
57
- end
10
+ end
@@ -0,0 +1,156 @@
1
+ require 'json'
2
+
3
+ module Tact
4
+ module GoogleContacts
5
+
6
+ class Entry
7
+ attr_reader :info
8
+
9
+ def self.all
10
+ collection
11
+ end
12
+
13
+ def initialize(info)
14
+ @info = info
15
+ end
16
+
17
+ def first_name
18
+ names[:givenName]
19
+ end
20
+
21
+ def last_name
22
+ names[:familyName]
23
+ end
24
+
25
+ def phone_numbers
26
+ if info[:phoneNumbers]
27
+ info[:phoneNumbers].map do |phone_number|
28
+ PhoneNumber.new(
29
+ number: phone_number[:value],
30
+ kind: phone_number[:type]
31
+ )
32
+ end
33
+ end
34
+ end
35
+
36
+ def emails
37
+ if info[:emailAddresses]
38
+ info[:emailAddresses].map do |email_address|
39
+ Email.new(address: email_address[:value])
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def names
47
+ info[:names][0]
48
+ end
49
+
50
+ def self.collection
51
+ @@collection ||= Fetcher.info_list.reduce(EntriesCollection.new) do |collection, info|
52
+ collection << new(info)
53
+ end
54
+ end
55
+
56
+ private_class_method :collection
57
+ end
58
+
59
+
60
+ class EntriesCollection
61
+ include Enumerable
62
+
63
+ def initialize(entries=[])
64
+ @entries = entries
65
+ end
66
+
67
+ def <<(entry)
68
+ @entries << entry
69
+ end
70
+
71
+ def each
72
+ @entries.each { |c| yield(c) }
73
+ end
74
+ end
75
+
76
+
77
+ class Syncer
78
+
79
+ def initialize(entry)
80
+ @entry = entry
81
+ @new_numbers = []
82
+ @new_emails = []
83
+ end
84
+
85
+ def sync
86
+ contact = find_contact || Contact.new(
87
+ first_name: entry.first_name.upcase,
88
+ last_name: (entry.last_name || "").upcase
89
+ )
90
+ merge_properties(contact)
91
+ contact.save
92
+ end
93
+
94
+ def find_contact
95
+ Contact.find_by(
96
+ first_name: entry.first_name.upcase,
97
+ last_name: (entry.last_name || "").upcase
98
+ )
99
+ end
100
+
101
+ def merge_properties(contact)
102
+ get_new_phone_numbers(contact)
103
+ get_new_emails(contact)
104
+ add_new_phone_numbers(contact)
105
+ add_new_emails(contact)
106
+ end
107
+
108
+ def add_new_phone_numbers(contact)
109
+ contact.phone_numbers << new_numbers
110
+ end
111
+
112
+ def add_new_emails(contact)
113
+ contact.emails << new_emails
114
+ end
115
+
116
+ def get_new_phone_numbers(contact)
117
+ entry.phone_numbers.each do |number|
118
+ if !contact.phone_numbers.any? { |n| n.number == number.number }
119
+ new_numbers << number
120
+ end
121
+ end if entry.phone_numbers
122
+ end
123
+
124
+ def get_new_emails(contact)
125
+ entry.emails.each do |email|
126
+ if !contact.emails.any? { |e| e.address == email.address }
127
+ new_emails << email
128
+ end
129
+ end if entry.emails
130
+ end
131
+
132
+ private
133
+ attr_reader :entry, :new_numbers, :new_emails
134
+
135
+ end
136
+
137
+
138
+ class Fetcher
139
+
140
+ def self.info_list
141
+ info = JSON.parse(json, symbolize_names: true)
142
+ if info[:error]
143
+ puts "ERROR: Please authorize your Google account.".red
144
+ exit
145
+ end
146
+ info[:connections]
147
+ end
148
+
149
+ def self.json
150
+ `curl -H "$(oauth2l header --json #{CLIENT_SECRET} https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/contacts.readonly)"\
151
+ https://people.googleapis.com/v1/people/me/connections?requestMask.includeField=person.names,person.phone_numbers,person.email_addresses`
152
+ end
153
+ end
154
+
155
+ end
156
+ end
@@ -1,78 +1,21 @@
1
- require 'colored'
2
- require_relative 'database'
3
-
4
1
  module Tact
5
- class PhoneNumber
6
- attr_reader :id
7
- attr_accessor :number, :index, :type
8
-
9
- @@db = Database.new.db
10
-
11
- def self.format_number(number)
12
- n = number.gsub(/[^\d]/, "")
13
- chars = n.chars
14
- if chars.count == 10
15
- chars.insert(3, '-')
16
- chars.insert(7, '-')
17
- elsif chars.count == 11
18
- chars.insert(0, '+')
19
- chars.insert(2, ' ')
20
- chars.insert(6, '-')
21
- chars.insert(10, '-')
22
- elsif chars.count == 7
23
- chars.insert(3, '-')
24
- else
25
- return n
26
- end
27
- return chars.join
28
- end
29
-
30
- def self.from_hash(hash)
31
- self.new(hash["type"], hash["number"], hash["contact_id"], hash["id"])
32
- end
33
-
34
- def self.all
35
- number_hashes = @@db.execute("select * from phone_numbers;")
36
- number_hashes.map {|n_hash| self.from_hash(n_hash) }
37
- end
38
-
39
- def self.delete(id)
40
- @@db.execute("delete from phone_numbers where id = ?;", [id])
41
- @@db.execute("select changes();")[0]["changes()"] == 1 ? true : false
42
- end
2
+ class PhoneNumber < ActiveRecord::Base
3
+ belongs_to :contact
43
4
 
44
- def self.find_by_id(id)
45
- number_hashes = @@db.execute("select * from phone_numbers where id = ?", [id])
46
- self.from_hash(number_hashes[0]) if !number_hashes.empty?
5
+ after_initialize :format_number
6
+ before_save :format_number
7
+ before_save do
8
+ self.kind = kind ? kind.downcase.capitalize : "Cell"
47
9
  end
48
10
 
49
- def self.find_by_number(number)
50
- number_hashes = @@db.execute("select * from phone_numbers where number = ?", [number])
51
- number_hashes.map {|n_hash| self.from_hash(n_hash) }
52
- end
53
-
54
- def initialize(type, number, contact_id, primary_key=nil)
55
- @id = primary_key
56
- @type = type.downcase.capitalize
57
- @number = number.gsub(/\D/, "")
58
- @contact_id = contact_id
11
+ def to_s
12
+ "#{kind.yellow}: #{number.bold}"
59
13
  end
60
14
 
61
- def save
62
- if @id == nil
63
- if @@db.execute("INSERT INTO phone_numbers (type, number, contact_id) values (?, ?, ?);", [@type, @number, @contact_id])
64
- @id = @@db.execute("select last_insert_rowid();")[0]["last_insert_rowid()"]
65
- self
66
- else
67
- false
68
- end
69
- else
70
- @@db.execute("update phone_numbers set type = ?, number = ?, contact_id = ? where id = ?;", [@type, @number, @contact_id]) ? self : false
71
- end
15
+ def format_number
16
+ n = number.gsub(/[^\d]/, "")
17
+ self.number = n.gsub(/\A(?:\d{1,3})?(\d{3})(\d{3})(\d{4})\z/, '(\1) \2-\3')
72
18
  end
73
19
 
74
- def to_s
75
- "#{type.yellow}: #{PhoneNumber.format_number(number).bold}"
76
- end
77
20
  end
78
- end
21
+ end
data/lib/tact/rolodex.rb CHANGED
@@ -1,72 +1,76 @@
1
- require 'sqlite3'
2
- require 'colored'
3
- require_relative 'card'
4
- require_relative 'database'
5
-
6
1
  module Tact
7
2
  class Rolodex
8
3
 
9
4
  def initialize
10
5
  @cards = load_cards
11
- @db = Database.new.db
12
6
  end
13
7
 
14
8
  def load_cards
15
- cards = Contact.all.each_with_index.map do |contact, index|
9
+ cards = Contact.all.order(last_name: :asc, first_name: :asc).each_with_index.map do |contact, index|
16
10
  Card.new(contact, index + 1)
17
11
  end
18
12
  cards
19
13
  end
20
14
 
21
15
  def add_contact(first_name, last_name)
22
- Contact.new(first_name, last_name).save
16
+ begin
17
+ Contact.create!(
18
+ first_name: first_name,
19
+ last_name: last_name
20
+ )
21
+ rescue
22
+ puts 'Error: Contact already exists'.red
23
+ end
23
24
  end
24
25
 
25
- def add_phone_number(contact_index, type, number)
26
+ def add_phone_number(contact_index, kind, number)
26
27
  contact = find_contact(contact_index)
27
- PhoneNumber.new(type, number, contact.id).save
28
+ PhoneNumber.create(
29
+ kind: kind,
30
+ number: number,
31
+ contact: contact
32
+ )
28
33
  end
29
34
 
30
35
  def add_email(contact_index, address)
31
36
  contact = find_contact(contact_index)
32
- Email.new(address, contact.id).save
37
+ Email.create(
38
+ address: address,
39
+ contact: contact
40
+ )
33
41
  end
34
42
 
35
43
  def delete_contact(contact_index)
36
- contact = find_contact(contact_index)
37
- Contact.delete(contact.id)
44
+ find_contact(contact_index).destroy
38
45
  end
39
46
 
40
47
  def delete_phone_number(contact_index, num_index)
41
- phone_number = find_phone_number(contact_index, num_index)
42
- PhoneNumber.delete(phone_number.id)
48
+ find_phone_number(contact_index, num_index).destroy
43
49
  end
44
50
 
45
51
  def delete_email(contact_index, email_index)
46
- email = find_email(contact_index, email_index)
47
- Email.delete(email.id)
52
+ find_email(contact_index, email_index).destroy
48
53
  end
49
54
 
50
55
  def edit_contact_name(contact_index, new_first_name, new_last_name)
51
56
  contact = find_contact(contact_index)
52
- contact.first_name = new_first_name.downcase.capitalize
53
- contact.last_name = new_last_name.downcase.capitalize
54
- contact.save
57
+ contact.update_attributes(
58
+ first_name: new_first_name,
59
+ last_name: new_last_name
60
+ )
55
61
  end
56
62
 
57
63
  def edit_phone_number(contact_index, num_index, new_type, new_number)
58
- new_type = new_type.downcase.capitalize
59
- new_number = new_number.gsub(/\D/, "")
60
64
  phone_number = find_phone_number(contact_index, num_index)
61
- phone_number.type = new_type
62
- phone_number.number = new_number
63
- phone_number.save
65
+ phone_number.update_attributes(
66
+ type: new_type,
67
+ number: new_number
68
+ )
64
69
  end
65
70
 
66
71
  def edit_email(contact_index, email_index, new_address)
67
72
  email = find_email(contact_index, email_index)
68
- email.address = new_address
69
- email.save
73
+ email.update_attributes(address: new_address)
70
74
  end
71
75
 
72
76
  def find_contact(contact_index)
@@ -98,21 +102,26 @@ module Tact
98
102
  end
99
103
  end
100
104
 
105
+ # TODO: Add specs for find methods
101
106
  def find_by_name(param)
102
- param = param.split(" ").map {|name| name.capitalize }.join(" ")
107
+ param = param.split(" ").map {|name| name.upcase }.join(" ")
103
108
  search_results = []
104
109
  @cards.each {|card| search_results.push(card) if card.contact.full_name.include?(param)}
105
110
  search_results
106
111
  end
107
112
 
108
113
  def find_by_number(param)
109
- contact_ids = @db.execute("select distinct contact_id from phone_numbers where number like '%#{param}%';")
110
- contact_ids.map {|hash| convert_to_card(hash["contact_id"]) }
114
+ phone_numbers = PhoneNumber.includes(:contact).where('number LIKE ?', "%#{param}%")
115
+ phone_numbers.map do |phone_number|
116
+ convert_to_card(phone_number.contact.id)
117
+ end
111
118
  end
112
119
 
113
120
  def find_by_email(param)
114
- contact_ids = @db.execute("select distinct contact_id from emails where address like '%#{param}%';")
115
- contact_ids.map {|hash| convert_to_card(hash["contact_id"]) }
121
+ emails = Email.includes(:contact).where('address LIKE ?', "%#{param}%")
122
+ emails.map do |email|
123
+ convert_to_card(email.contact.id)
124
+ end
116
125
  end
117
126
 
118
127
  def convert_to_card(contact_id)
@@ -120,6 +129,10 @@ module Tact
120
129
  results[0]
121
130
  end
122
131
 
132
+ def length
133
+ @cards.count
134
+ end
135
+
123
136
  def to_s
124
137
  string = ""
125
138
  @cards.each {|card| string += card.to_s}
data/lib/tact/tact.rb CHANGED
@@ -1,10 +1,10 @@
1
- require_relative "rolodex.rb"
2
1
  require 'optparse'
3
- require 'tact/version'
4
- require 'colored'
2
+ require_relative 'authorizable.rb'
5
3
 
6
4
  module Tact
7
5
  class Tact
6
+ include Authorizable
7
+
8
8
  def initialize(args)
9
9
  @dex = Rolodex.new
10
10
  @options = {}
@@ -20,12 +20,15 @@ module Tact
20
20
  opt.on('-d', '--delete', 'Delete entry') do
21
21
  @options[:delete] = true
22
22
  end
23
- opt.on('-p', '--phone', 'Edit phone number') do
23
+ opt.on('-p', '--phone', 'Phone number') do
24
24
  @options[:number] = true
25
25
  end
26
- opt.on('-e', '--email', 'Edit email') do
26
+ opt.on('-e', '--email', 'Email') do
27
27
  @options[:email] = true
28
28
  end
29
+ opt.on('-s', '--sync', 'Sync Google contacts') do
30
+ @options[:sync] = true
31
+ end
29
32
  opt.on("-v", "--version", "Version") do
30
33
  @options[:version] = true
31
34
  end
@@ -41,23 +44,24 @@ module Tact
41
44
  end
42
45
 
43
46
  def help_message
44
- <<~EOF
45
-
46
- -v Current version
47
- -h Help
47
+ <<-EOF
48
+
49
+ -v Current version
50
+ -h Help
51
+ -s Sync with Google Contacts
48
52
 
49
- <param> Search by name
50
- -p <param> Search by number
51
- -e <param> Search by email
52
- -n <first> <last> Adds new name
53
- -np <index> <type> <num> Adds contact number
54
- -ne <index> <address> Adds contact email
55
- -d <index> Deletes contact
56
- -dp <index> <num_index> Deletes contact number
57
- -de <index> <e_index> Deletes contact email
58
- -u <index> <first> <last> Edits contact name
59
- -up <index> <num_index> <type> <num> Edits contact number
60
- -ue <index> <e_index> <address> Edits contact email
53
+ <param> Search by name
54
+ -p <param> Search by number
55
+ -e <param> Search by email
56
+ -n <first> <last> Adds new name
57
+ -np <index> <type> <num> Adds contact number
58
+ -ne <index> <address> Adds contact email
59
+ -d <index> Deletes contact
60
+ -dp <index> <num_index> Deletes contact number
61
+ -de <index> <e_index> Deletes contact email
62
+ -u <index> <first> <last> Edits contact name
63
+ -up <index> <num_index> <type> <num> Edits contact number
64
+ -ue <index> <e_index> <address> Edits contact email
61
65
 
62
66
  EOF
63
67
  end
@@ -117,7 +121,15 @@ module Tact
117
121
  new_address = @args[2]
118
122
  @dex.edit_email(contact_index, email_index, new_address)
119
123
  end
120
-
124
+
125
+ elsif @options[:sync]
126
+ authorize unless authorized?
127
+ puts "Syncing Google contacts..."
128
+ GoogleContacts::Entry.all.each do |entry|
129
+ syncer = GoogleContacts::Syncer.new(entry)
130
+ syncer.sync
131
+ end
132
+
121
133
  elsif @options[:help]
122
134
  puts help_message
123
135
  elsif @options[:version]
@@ -202,4 +214,4 @@ module Tact
202
214
  end
203
215
  end
204
216
  end
205
- end
217
+ end
data/lib/tact/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Tact
2
- VERSION = "1.2.23"
2
+ VERSION = "2.0.17"
3
3
  end
data/tact.gemspec CHANGED
@@ -16,12 +16,21 @@ Gem::Specification.new do |spec|
16
16
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
17
  f.match(%r{^(test|spec|features)/})
18
18
  end
19
+ spec.files << 'client_secret.json'
20
+
19
21
  spec.executables = ["tact"]
20
22
  spec.require_paths = ["lib"]
21
23
 
22
24
  spec.add_runtime_dependency "sqlite3", "~> 1.3"
23
25
  spec.add_runtime_dependency "colored", "~> 1.2"
26
+ spec.add_runtime_dependency "activerecord", "~> 5.0"
24
27
 
25
28
  spec.add_development_dependency "bundler", "~> 1.13"
26
29
  spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rspec", "~> 3.6"
31
+ spec.add_development_dependency "database_cleaner", "~> 1.4"
32
+
33
+ spec.requirements << "oauth2l"
34
+
35
+ spec.post_install_message = "Thanks for installing tact! Run `tact -h` for help. Note: If you would like to sync your contacts with Google, please install oauth2l: 'https://github.com/google/oauth2l'"
27
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tact
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.23
4
+ version: 2.0.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - ollieshmollie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-12-20 00:00:00.000000000 Z
11
+ date: 2017-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,34 @@ dependencies:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
82
  version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: database_cleaner
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.4'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.4'
69
111
  description:
70
112
  email:
71
113
  - oliverduncan@icloud.com
@@ -82,11 +124,16 @@ files:
82
124
  - bin/console
83
125
  - bin/setup
84
126
  - bin/tact
127
+ - client_secret.json
128
+ - db/migrate/20170515014719_create_tables.rb
129
+ - db/migrate/20170515015242_add_foreign_keys.rb
130
+ - db/migrate/20170515130759_change_type_on_phone_numbers.rb
85
131
  - lib/tact.rb
132
+ - lib/tact/authorizable.rb
86
133
  - lib/tact/card.rb
87
134
  - lib/tact/contact.rb
88
- - lib/tact/database.rb
89
135
  - lib/tact/email.rb
136
+ - lib/tact/google_client.rb
90
137
  - lib/tact/phone_number.rb
91
138
  - lib/tact/rolodex.rb
92
139
  - lib/tact/tact.rb
@@ -96,7 +143,8 @@ homepage: https://www.github.com/ollieshmollie/tact
96
143
  licenses:
97
144
  - MIT
98
145
  metadata: {}
99
- post_install_message:
146
+ post_install_message: 'Thanks for installing tact! Run `tact -h` for help. Note: If
147
+ you would like to sync your contacts with Google, please install oauth2l: ''https://github.com/google/oauth2l'''
100
148
  rdoc_options: []
101
149
  require_paths:
102
150
  - lib
@@ -110,9 +158,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
158
  - - ">="
111
159
  - !ruby/object:Gem::Version
112
160
  version: '0'
113
- requirements: []
161
+ requirements:
162
+ - oauth2l
114
163
  rubyforge_project:
115
- rubygems_version: 2.5.1
164
+ rubygems_version: 2.6.11
116
165
  signing_key:
117
166
  specification_version: 4
118
167
  summary: A command line rolodex.
data/lib/tact/database.rb DELETED
@@ -1,47 +0,0 @@
1
- require 'sqlite3'
2
-
3
- module Tact
4
- class Database
5
- attr_reader :db
6
-
7
- def initialize
8
- home_dir = File.expand_path("~")
9
- Dir.mkdir("#{home_dir}/.tact") unless File.exists?("#{home_dir}/.tact")
10
- @db = SQLite3::Database.new("#{File.expand_path("~")}/.tact/tact.sqlite3")
11
- @db.results_as_hash = true
12
- @db.execute("pragma foreign_keys = on")
13
- create_tables
14
- end
15
-
16
- def create_tables
17
- contact_table = <<~eof
18
- CREATE TABLE IF NOT EXISTS contacts(
19
- id integer primary key,
20
- first_name varchar(255) not null,
21
- last_name varchar(255) not null,
22
- constraint name_unique unique (first_name, last_name)
23
- );
24
- eof
25
- phone_numbers_table = <<~eof
26
- CREATE TABLE IF NOT EXISTS phone_numbers(
27
- id integer primary key,
28
- type varchar(255),
29
- number varchar(255),
30
- contact_id int,
31
- foreign key (contact_id) references contacts(id) on delete cascade
32
- );
33
- eof
34
- emails_table = <<~eof
35
- CREATE TABLE IF NOT EXISTS emails(
36
- id integer primary key,
37
- address varchar(255),
38
- contact_id,
39
- foreign key (contact_id) references contacts(id) on delete cascade
40
- );
41
- eof
42
- @db.execute(contact_table)
43
- @db.execute(phone_numbers_table)
44
- @db.execute(emails_table)
45
- end
46
- end
47
- end