highrise_assist 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # Highrise Assist
2
+
3
+ highrise_assist is command line tool for 37signals' highrise.
4
+
5
+ ## Description
6
+
7
+ If you are using http://highrisehq.com/, you defiantly can notice that there are some processes in your company, that doesn't feet to the functionality implemented by 37signals' team. Fortunately they have quite strong API, that can be used for custom purposes. In our company we have bunch of such customizations, that we have decided to group in to the gem and deliver to the community. An the first tool is companies and contacts export by tag (read more below). So highrise_assist is a command line set of tools, which let you do custom operations with your data in highrise.
8
+
9
+ ## Installation
10
+
11
+ $ gem install highrise_assist
12
+
13
+ ## Usage
14
+
15
+ Usage: ./bin/highrise_assist COMMAND [options]
16
+
17
+ Commands:
18
+ * export - export next highrise items:
19
+ * cases
20
+ * deals
21
+ * people
22
+ * companies
23
+ * emails
24
+ * notes
25
+ * comments
26
+ * attachments
27
+
28
+ Common options:
29
+ --domain DOMAIN highrise subdomain or full domain name
30
+ --token TOKEN highrise API authentication token
31
+
32
+ Export options:
33
+ --tag TAG filter items with given tag name
34
+ --directory DIRECTORY working directory
35
+ --format FORMAT data format (yaml,xml)
36
+ --skip-attachments don't download attachments
37
+ --skip-cases don't export cases
38
+ --skip-deals don't export deals
39
+ --skip-notes don't export notes
40
+ --skip-emails don't export emails
41
+ --skip-comments don't export comments
42
+
43
+ Misc options:
44
+ -h, --help Show this message
45
+ -v, --version Show version
46
+
47
+ ## Export Command
48
+
49
+ ### Description
50
+
51
+ If you are the using highrise for sales, human resources or some other activities, one day you will want to export data from the system. Here is the original article http://bit.ly/sTQr1W, which is describing the process. It's quite easy and works fine, but with highrise_assistexport you can do much better. This tool introduce you possibility to export companies and contacts with all the hierarchy of data including emails, notes, comments, cases, deals and even attachments (with web console you just don't have such possibility) in xml or yaml formats. It is possible to mark exact business objects with the tag and then just export them.
52
+
53
+ #### The use case 1:
54
+
55
+ As user of higrise
56
+ I want to archive old contacts and companies
57
+ So that my list will be clean
58
+
59
+ Original help request: http://bit.ly/tIDlwk
60
+ What 37signals' team recommends is: "Have you considered adding a tag to those contacts called "archived"?"
61
+ With highrise_assistexport you can easily export all the data tagged as "archived", and than just remove data from the system.
62
+
63
+ #### The use case2:
64
+
65
+ As user of higrise
66
+ I want to export all my data including attachments
67
+ So that I can move data to the other management systems
68
+
69
+ With highrise_assistexport you can easily do this operation.
70
+
71
+ ## Keep your higrise always clean!
72
+
73
+ ### Synopsis
74
+
75
+ $ highrise_assist export OPTIONS
76
+
77
+ ### Example
78
+
79
+ $ highrise_assist export \
80
+ --domain MYSUBDOMAIN \
81
+ --token 11111111111111111111111111111111 \
82
+ --directory highrise_data \
83
+ --tag old-clients \
84
+ --format xml \
85
+ --skip-attachments
86
+
87
+ Export result:
88
+
89
+ $ tree --dirsfirst
90
+ .
91
+ ├── cases
92
+ │   ├── case-573785-test-case
93
+ │   │   ├── attachments
94
+ │   │   │   └── 22039573-20111111-p7km4bk33ugutcrrb5mxxw7fjj.jpeg
95
+ │   │   ├── case-573785-test-case.xml
96
+ │   │   └── note-78169727-test.xml
97
+ │   └── case-573786-what-is-highrise
98
+ │   ├── attachments
99
+ │   └── case-573786-what-is-highrise.xml
100
+ ├── deals
101
+ │   └── deal-1471928-test-deal
102
+ │   ├── attachments
103
+ │   ├── deal-1471928-test-deal.xml
104
+ │   └── note-78499917.xml
105
+ └── persons
106
+ └── person-92632832-test-test
107
+ ├── attachments
108
+ ├── cases
109
+ │   └── case-573785-test-case -> ../../../cases/case-573785-test-case
110
+ ├── deals
111
+ │   └── deal-1471928-test-deal -> ../../../deals/deal-1471928-test-deal
112
+ └── person-92632832-test-test.xml
113
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "highrise_assist"
4
+
5
+ HighriseAssist::Runner.run(ARGV)
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "highrise_assist/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "highrise_assist"
7
+ s.version = HighriseAssist::VERSION
8
+ s.authors = ["Andriy Yanko"]
9
+ s.email = ["andriy.yanko@gmail.com"]
10
+ s.homepage = "https://github.com/railsware/highrise_assist"
11
+ s.summary = %q{Assist for 37signals' highrise}
12
+ s.description = %q{Assist for 37signals' highrise}
13
+
14
+ s.rubyforge_project = "highrise_assist"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_runtime_dependency "highrise", "~>3.0.0"
22
+ s.add_runtime_dependency "net-http-persistent", "~>2.3"
23
+ end
@@ -0,0 +1,56 @@
1
+ require "fileutils"
2
+
3
+ module HighriseAssist
4
+ module Command
5
+ class Base
6
+ class << self
7
+ def inherited(klass)
8
+ Command.names.push klass.name.split("::").last.underscore
9
+ super(klass)
10
+ end
11
+ end
12
+
13
+ def initialize(options)
14
+ @options = options.dup
15
+ authenticate
16
+ end
17
+
18
+ attr_reader :options
19
+
20
+ def run
21
+ raise NotImplementedError, "'run' is not implemented by #{self.class.name}"
22
+ end
23
+
24
+ protected
25
+
26
+ def authenticate
27
+ require_option!(:domain, :token)
28
+
29
+ Highrise::Base.site = "https://" + options[:domain]
30
+ Highrise::Base.user = options[:token]
31
+ end
32
+
33
+ def require_option!(*names)
34
+ names.each do |name|
35
+ options[name].blank? and abort "#{name} option required"
36
+ end
37
+ end
38
+
39
+ def log(message)
40
+ puts message
41
+ end
42
+
43
+ def file_transfer
44
+ @file_transfer ||= FileTransfer.new(options)
45
+ end
46
+
47
+ def download_file(*args)
48
+ file_transfer.download(*args)
49
+ end
50
+
51
+ def upload_file(*args)
52
+ file_transfer.upload(*args)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,231 @@
1
+ module HighriseAssist
2
+ module Command
3
+ class Export < Base
4
+
5
+ FORMATS = %w(yaml xml)
6
+
7
+ def initialize(*)
8
+ super
9
+
10
+ require_option!(:directory)
11
+ @root_dir = File.expand_path(options[:directory])
12
+
13
+ @format = options[:format] || FORMATS.first
14
+ FORMATS.include?(@format) or abort "Unsupported format: #{@format}"
15
+
16
+ @format_method = "to_#{@format}".to_sym
17
+
18
+ @tags = []
19
+
20
+ @parties = {}
21
+ end
22
+
23
+ attr_reader :root_dir, :format, :format_method
24
+
25
+ def run
26
+ authenticate
27
+
28
+ log "* Fetching data ..."
29
+
30
+ fetch_tags if options[:tag]
31
+
32
+ %w(Person Company).each do |type|
33
+ collection = fetch_collection(type)
34
+ store_collection(collection)
35
+ @parties[type] = collection
36
+ end
37
+
38
+ %w(Kase Deal).each do |type|
39
+ next if options[:skip_items].include?(type)
40
+ collection = fetch_collection(type)
41
+ filter_casedeal_collection!(collection)
42
+ store_collection(collection)
43
+ symlink_casedeal_collection(collection)
44
+ end
45
+
46
+ log "Done."
47
+ end
48
+
49
+ protected
50
+
51
+ def fetch_tags
52
+ log "* Fetching tags ..."
53
+ @tags = Highrise::Tag.find(:all).select { |t| t.name == options[:tag] }
54
+ @tags.empty? and abort "Unknown tag #{options[:tag]}"
55
+ end
56
+
57
+ def fetch_collection(type)
58
+ klass = Highrise.const_get(type)
59
+ if @tags.empty?
60
+ collection = klass.find(:all)
61
+ else
62
+ collection = []
63
+ @tags.each do |tag|
64
+ collection += klass.find(:all, :params => { :tag_id => tag.id } )
65
+ end
66
+ collection.uniq!
67
+ end
68
+ log ""
69
+ log "# #{type}: Found #{collection.size} items"
70
+ collection
71
+ end
72
+
73
+ # Person, Company, Kase, Deal
74
+ def store_collection(collection)
75
+ return if collection.empty?
76
+ base_dir = File.join(root_dir, item_dirname(collection.first))
77
+ FileUtils.mkdir_p(base_dir)
78
+
79
+ size = collection.size
80
+ collection.each_with_index do |item, index|
81
+ log ""
82
+ log("= #{index+1}of#{size}:")
83
+
84
+ dir = File.join(base_dir, item_filename(item))
85
+ FileUtils.mkdir_p(dir)
86
+
87
+ store_item(item, dir, " * ")
88
+ store_item_collection(item, "Note", :notes, dir) unless options[:skip_items].include?("Note")
89
+ store_item_collection(item, "Email", :emails, dir) unless options[:skip_items].include?("Email")
90
+ end
91
+ end
92
+
93
+ # Note, Email,
94
+ # Note Attachments, Email Attachments,
95
+ # Note Comments, Email Comments,
96
+ # Note Comment Attachments, Email Comment, Attachments
97
+ def store_item_collection(item, type, collection_name, base_dir)
98
+ collection = item.send(collection_name)
99
+ log " * Found #{collection.size} #{collection_name}"
100
+
101
+ attachment_dir = File.join(base_dir, "attachments")
102
+ FileUtils.mkdir_p(attachment_dir)
103
+
104
+ collection.each_with_index do |item, index|
105
+ attachments = item.attributes['attachments'] || []
106
+
107
+ unless options[:skip_items].include?("Comment")
108
+ comments = item.comments
109
+ item.attributes['comments'] = comments
110
+ else
111
+ comments = []
112
+ end
113
+
114
+ log " * Found #{comments.size} comments, #{attachments.size} attachments"
115
+ store_item(item, base_dir, " * ")
116
+
117
+ attachments.each do |attachment|
118
+ download_attachment(attachment, attachment_dir, " * ")
119
+ end
120
+
121
+ comments.each do |comment|
122
+ (comment.attributes['attachments'] || []).each do |attachment|
123
+ download_attachment(attachment, attachment_dir, " * ")
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ def store_item(item, dir, log_prefix = "")
130
+ file = "#{item_filename(item)}.#{format}"
131
+ path = File.join(dir, file)
132
+ log "#{log_prefix}Store #{file}"
133
+ File.open(path, "w") { |f| f << item.send(format_method) }
134
+ end
135
+
136
+ def download_attachment(attachment, dir, log_prefix = "")
137
+ return if options[:skip_attachments]
138
+
139
+ file = "#{attachment.attributes['id']}-#{attachment.attributes['name']}"
140
+ path = File.join(dir, file)
141
+
142
+ log "#{log_prefix}Download attachment #{file}"
143
+ download_file(attachment.attributes['url'], path)
144
+ end
145
+
146
+ def filter_casedeal_collection!(collection)
147
+ collection.reject! do |casedeal|
148
+ parties = casedeal_parties(casedeal)
149
+ not parties.any? { |party| fetched_party?(party) }
150
+ end
151
+
152
+ log "# Filtered to #{collection.size} items"
153
+ end
154
+
155
+ def symlink_casedeal_collection(collection)
156
+ collection.each do |casedeal|
157
+ casedeal_parties(casedeal).each do |party|
158
+ next unless fetched_party?(party)
159
+ symlink_casedeal_to_party(casedeal, party)
160
+ end
161
+ end
162
+ end
163
+
164
+ def casedeal_parties(casedeal)
165
+ parties = []
166
+ parties += casedeal.parties
167
+ parties.push(casedeal.party) if casedeal.is_a?(Highrise::Deal)
168
+ parties.compact!
169
+ parties.uniq!
170
+ parties
171
+ end
172
+
173
+ def fetched_party?(party)
174
+ !!@parties[party.attributes['type']].detect { |p| p.id == party.id }
175
+ end
176
+
177
+ def symlink_casedeal_to_party(casedeal, party)
178
+ source_name = item_filename(casedeal)
179
+ destination_name = item_filename(party)
180
+ source_path = File.join('..', '..', '..', item_dirname(casedeal), source_name)
181
+ party_dir = File.join(root_dir, item_dirname(party), item_filename(party))
182
+ destination_dir = File.join(party_dir, item_dirname(casedeal))
183
+
184
+ FileUtils.mkdir_p(destination_dir)
185
+ Dir.chdir(destination_dir) do
186
+ log " Symlink #{source_name} to #{destination_name}"
187
+ FileUtils.ln_s(source_path, source_name)
188
+ end
189
+ end
190
+
191
+ def item_dirname(item)
192
+ case item
193
+ when Highrise::Party
194
+ item_dirname(item.type_object)
195
+ when Highrise::Person
196
+ "persons"
197
+ when Highrise::Company
198
+ "companies"
199
+ when Highrise::Kase
200
+ "cases"
201
+ when Highrise::Deal
202
+ "deals"
203
+ else
204
+ raise "Unknown #{item.class}"
205
+ end
206
+ end
207
+
208
+ def item_filename(item)
209
+ case item
210
+ when Highrise::Party
211
+ "#{item.attributes['type']}-#{item.id}-#{item.name}"
212
+ when Highrise::Person
213
+ "Person-#{item.id}-#{item.name}"
214
+ when Highrise::Company
215
+ "Company-#{item.id}-#{item.name}"
216
+ when Highrise::Kase
217
+ "Case-#{item.id}-#{item.name}"
218
+ when Highrise::Deal
219
+ "Deal-#{item.id}-#{item.name}"
220
+ when Highrise::Email
221
+ "Email-#{item.id}-#{item.title}"
222
+ when Highrise::Note
223
+ "Note-#{item.id}-#{item.body}"[0,64]
224
+ else
225
+ raise "Unknown #{item.class}"
226
+ end.parameterize
227
+ end
228
+
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,20 @@
1
+ module HighriseAssist
2
+ module Command
3
+ class << self
4
+ def names
5
+ @names ||= []
6
+ end
7
+
8
+ def defined?(name)
9
+ names.include?(name)
10
+ end
11
+
12
+ def run(name, options)
13
+ self.const_get(name.classify).new(options).run
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ require "highrise_assist/command/base"
20
+ require "highrise_assist/command/export"
@@ -0,0 +1,30 @@
1
+ require "net/http/persistent"
2
+
3
+ module HighriseAssist
4
+ class FileTransfer
5
+ def initialize(options)
6
+ @options = options
7
+
8
+ @http = Net::HTTP::Persistent.new('highrise-file-transfer')
9
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
10
+ @http
11
+ end
12
+
13
+ def download(from, to)
14
+ uri = URI.parse(from)
15
+
16
+ get_request = Net::HTTP::Get.new(uri.request_uri)
17
+ get_request.basic_auth @options[:token], ''
18
+
19
+ @http.request(uri, get_request) do |response|
20
+ open(to, 'w') do |io|
21
+ response.read_body { |chunk| io.write chunk }
22
+ end
23
+ end
24
+ end
25
+
26
+ def upload(from, to)
27
+ raise NotImplementedError
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,73 @@
1
+ # encoding: utf-8
2
+
3
+ $KCODE = 'u' if RUBY_VERSION =~ /^1.8/
4
+
5
+ require "optparse"
6
+
7
+ module HighriseAssist
8
+ class Runner
9
+ def self.run(*args)
10
+ new(*args).run!
11
+ end
12
+
13
+ def initialize(argv)
14
+ @argv = argv
15
+ @options = {}
16
+ @options[:skip_items] = []
17
+ end
18
+
19
+ def run!
20
+ parser = OptionParser.new do |o|
21
+ o.banner = "Usage: #{$0} COMMAND [options]"
22
+
23
+ o.separator ""
24
+ o.separator "Commands:"
25
+ o.separator " * export - export next highrise items:"
26
+ %w(cases deals people companies emails notes comments attachments).each do |item|
27
+ o.separator " * #{item}"
28
+ end
29
+
30
+ o.separator ""
31
+ o.separator "Common options:"
32
+ o.on("--domain DOMAIN", "highrise subdomain or full domain name") { |v| set_domain_option(v) }
33
+ o.on("--token TOKEN", "highrise API authentication token") { |v| @options[:token] = v }
34
+ o.separator ""
35
+
36
+ o.separator "Export options:"
37
+ o.on("--tag TAG", "filter items with given tag name") { |v| @options[:tag] = v }
38
+ o.on("--directory DIRECTORY", "working directory") { |v| @options[:directory] = v }
39
+ o.on("--format FORMAT", "data format (#{Command::Export::FORMATS.join(',')}) ") { |v| @options[:format] = v }
40
+ o.on("--skip-attachments", "don't download attachments") { @options[:skip_attachments] = true }
41
+ o.on("--skip-cases", "don't export cases") { @options[:skip_items] << "Kase" }
42
+ o.on("--skip-deals", "don't export deals") { @options[:skip_items] << "Deal" }
43
+ o.on("--skip-notes", "don't export notes") { @options[:skip_items] << "Note" }
44
+ o.on("--skip-emails", "don't export emails") { @options[:skip_items] << "Email" }
45
+ o.on("--skip-comments", "don't export comments") { @options[:skip_items] << "Comment" }
46
+
47
+ o.separator ""
48
+ o.separator "Misc options:"
49
+ o.on_tail("-h", "--help", "Show this message") { puts o; exit }
50
+ o.on_tail('-v', '--version', "Show version") { puts HighriseAssist::VERSION; exit }
51
+ end
52
+
53
+ parser.parse!(@argv)
54
+
55
+ command = @argv.first
56
+
57
+ Command.defined?(command) or raise OptionParser::ParseError, "Unknown command #{command.inspect}"
58
+
59
+ Command.run(command, @options)
60
+ rescue OptionParser::ParseError => e
61
+ warn e.message
62
+ puts parser.help
63
+ exit 1
64
+ end
65
+
66
+ private
67
+
68
+ def set_domain_option(value)
69
+ @options[:domain] = value.include?('.') ? value : "#{value}.highrisehq.com"
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,3 @@
1
+ module HighriseAssist
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,9 @@
1
+ require "highrise_assist/version"
2
+
3
+ require "highrise_ext"
4
+
5
+ module HighriseAssist
6
+ autoload :Runner, "highrise_assist/runner"
7
+ autoload :Command, "highrise_assist/command"
8
+ autoload :FileTransfer, "highrise_assist/file_transfer"
9
+ end
@@ -0,0 +1,25 @@
1
+ require "highrise"
2
+
3
+ Highrise::Base.format = :xml
4
+
5
+ Highrise::Party.class_eval do
6
+ def notes
7
+ Highrise::Note.find_all_across_pages(:from => "/#{type_collection_name}/#{id}/notes.xml")
8
+ end
9
+
10
+ def emails
11
+ Highrise::Email.find_all_across_pages(:from => "/#{type_collection_name}/#{id}/emails.xml")
12
+ end
13
+
14
+ def name
15
+ type_object.name
16
+ end
17
+
18
+ def type_object
19
+ Highrise.const_get(attributes['type']).new(attributes)
20
+ end
21
+
22
+ def type_collection_name
23
+ Highrise.const_get(attributes['type']).collection_name
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: highrise_assist
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Andriy Yanko
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-11-17 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: highrise
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 7
29
+ segments:
30
+ - 3
31
+ - 0
32
+ - 0
33
+ version: 3.0.0
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: net-http-persistent
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 5
45
+ segments:
46
+ - 2
47
+ - 3
48
+ version: "2.3"
49
+ type: :runtime
50
+ version_requirements: *id002
51
+ description: Assist for 37signals' highrise
52
+ email:
53
+ - andriy.yanko@gmail.com
54
+ executables:
55
+ - highrise_assist
56
+ extensions: []
57
+
58
+ extra_rdoc_files: []
59
+
60
+ files:
61
+ - .gitignore
62
+ - Gemfile
63
+ - README.md
64
+ - Rakefile
65
+ - bin/highrise_assist
66
+ - highrise_assist.gemspec
67
+ - lib/highrise_assist.rb
68
+ - lib/highrise_assist/command.rb
69
+ - lib/highrise_assist/command/base.rb
70
+ - lib/highrise_assist/command/export.rb
71
+ - lib/highrise_assist/file_transfer.rb
72
+ - lib/highrise_assist/runner.rb
73
+ - lib/highrise_assist/version.rb
74
+ - lib/highrise_ext.rb
75
+ homepage: https://github.com/railsware/highrise_assist
76
+ licenses: []
77
+
78
+ post_install_message:
79
+ rdoc_options: []
80
+
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 3
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ hash: 3
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ requirements: []
102
+
103
+ rubyforge_project: highrise_assist
104
+ rubygems_version: 1.8.6
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: Assist for 37signals' highrise
108
+ test_files: []
109
+