threetee-roart 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +17 -0
- data/README.rdoc +85 -0
- data/Rakefile +35 -0
- data/lib/roart.rb +49 -0
- data/lib/roart/callbacks.rb +25 -0
- data/lib/roart/connection.rb +62 -0
- data/lib/roart/core/array.rb +11 -0
- data/lib/roart/core/hash.rb +15 -0
- data/lib/roart/core/string.rb +21 -0
- data/lib/roart/errors.rb +11 -0
- data/lib/roart/history.rb +115 -0
- data/lib/roart/roart.rb +27 -0
- data/lib/roart/ticket.rb +381 -0
- data/lib/roart/ticket_page.rb +65 -0
- data/spec/roart/callbacks_spec.rb +49 -0
- data/spec/roart/connection_spec.rb +7 -0
- data/spec/roart/core/array_spec.rb +13 -0
- data/spec/roart/core/hash_spec.rb +16 -0
- data/spec/roart/core/string_spec.rb +13 -0
- data/spec/roart/history_spec.rb +60 -0
- data/spec/roart/roart_spec.rb +11 -0
- data/spec/roart/ticket_page_spec.rb +59 -0
- data/spec/roart/ticket_spec.rb +440 -0
- data/spec/roart_spec.rb +8 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/test_data/full_history.txt +126 -0
- data/spec/test_data/single_history.txt +26 -0
- data/threetee-roart.gemspec +37 -0
- metadata +105 -0
data/lib/roart/roart.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Roart
|
2
|
+
|
3
|
+
def self.check_keys!(hash, required)
|
4
|
+
unless required.inject(true) do |inc, attr|
|
5
|
+
inc ? hash.keys.include?(attr.to_sym) : nil
|
6
|
+
end
|
7
|
+
raise ArgumentError, "Not all required fields entered"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
module MethodFunctions
|
13
|
+
|
14
|
+
def add_methods!
|
15
|
+
@attributes.each do |key, value|
|
16
|
+
(class << self; self; end).send :define_method, key do
|
17
|
+
return @attributes[key]
|
18
|
+
end
|
19
|
+
(class << self; self; end).send :define_method, "#{key}=" do |new_val|
|
20
|
+
@attributes[key] = new_val
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
data/lib/roart/ticket.rb
ADDED
@@ -0,0 +1,381 @@
|
|
1
|
+
module Roart
|
2
|
+
|
3
|
+
module Tickets
|
4
|
+
|
5
|
+
DefaultAttributes = %w(queue owner creator subject status priority initial_priority final_priority requestors cc admin_cc created starts started due resolved told last_updated time_estimated time_worked time_left)
|
6
|
+
RequiredAttributes = %w(queue subject)
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
class Ticket
|
11
|
+
|
12
|
+
include Roart::MethodFunctions
|
13
|
+
include Roart::Callbacks
|
14
|
+
|
15
|
+
attr_reader :full, :history, :saved
|
16
|
+
|
17
|
+
# Creates a new ticket. Attributes queue and subject are required. Expects a hash with the attributes of the ticket.
|
18
|
+
#
|
19
|
+
# ticket = MyTicket.new(:queue => "Some Queue", :subject => "The System is Down.")
|
20
|
+
# ticket.id #-> This will be the ID of the ticket in the RT System.
|
21
|
+
#
|
22
|
+
def initialize(attributes)
|
23
|
+
Roart::check_keys!(attributes, Roart::Tickets::RequiredAttributes)
|
24
|
+
if attributes.is_a?(Hash)
|
25
|
+
@attributes = Roart::Tickets::DefaultAttributes.to_hash.merge(attributes)
|
26
|
+
@attributes.update(:id => 'ticket/new')
|
27
|
+
@saved = false
|
28
|
+
else
|
29
|
+
raise ArgumentError, "Expects a hash."
|
30
|
+
end
|
31
|
+
@history = false
|
32
|
+
add_methods!
|
33
|
+
end
|
34
|
+
|
35
|
+
# Loads all information for a ticket from RT and lets full to true.
|
36
|
+
# This changes the ticket object and adds methods for all the fields on the ticket.
|
37
|
+
# Custom fields will be prefixed with 'cf' so a custom field of 'phone'
|
38
|
+
# would be cf_phone. custom fields hold their case from how they are defined in RT, so a custom field of PhoneNumber would be cf_PhoneNumber and a custom field of phone_number would be cf_phone_number
|
39
|
+
#
|
40
|
+
def load_full!
|
41
|
+
unless self.full
|
42
|
+
ticket = self.class.find(self.id)
|
43
|
+
@attributes = ticket.instance_variable_get("@attributes")
|
44
|
+
add_methods!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
#loads the ticket history from rt
|
49
|
+
#
|
50
|
+
def histories
|
51
|
+
@histories ||= Roart::History.default(:ticket => self)
|
52
|
+
end
|
53
|
+
|
54
|
+
# if a ticket is new, calling save will create it in the ticketing system and assign the id that it gets to the id attribute. It returns true if the save was successful, and false if something went wrong
|
55
|
+
#
|
56
|
+
def save
|
57
|
+
if self.id == "ticket/new"
|
58
|
+
self.create
|
59
|
+
else
|
60
|
+
self.before_update
|
61
|
+
uri = "#{self.class.connection.server}/REST/1.0/ticket/#{self.id}/edit"
|
62
|
+
payload = @attributes.clone
|
63
|
+
payload.delete(:id)
|
64
|
+
payload = payload.to_content_format
|
65
|
+
resp = self.class.connection.post(uri, :content => payload)
|
66
|
+
resp = resp.split("\n")
|
67
|
+
raise "Ticket Update Failed" unless resp.first.include?("200")
|
68
|
+
if resp[2].match(/^# Ticket (\d+) updated./)
|
69
|
+
self.after_update
|
70
|
+
true
|
71
|
+
else
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Add a comment to a ticket
|
78
|
+
# Example:
|
79
|
+
# tix = Ticket.find(1000)
|
80
|
+
# tix.comment("This is a comment", :time_worked => 45, :cc => 'someone@example.com')
|
81
|
+
def comment(comment, opt = {})
|
82
|
+
comment = {:text => comment, :action => 'Correspond'}.merge(opt)
|
83
|
+
|
84
|
+
uri = "#{self.class.connection.server}/REST/1.0/ticket/#{self.id}/comment"
|
85
|
+
payload = comment.to_content_format
|
86
|
+
resp = self.class.connection.post(uri, :content => payload)
|
87
|
+
resp = resp.split("\n")
|
88
|
+
raise "Ticket Comment Failed" unless resp.first.include?("200")
|
89
|
+
!!resp[2].match(/^# Message recorded/)
|
90
|
+
end
|
91
|
+
|
92
|
+
# works just like save, but if the save fails, it raises an exception instead of silently returning false
|
93
|
+
#
|
94
|
+
def save!
|
95
|
+
raise "Ticket Create Failed" unless self.save
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def create #:nodoc:
|
102
|
+
self.before_create
|
103
|
+
uri = "#{self.class.connection.server}/REST/1.0/ticket/new"
|
104
|
+
payload = @attributes.to_content_format
|
105
|
+
resp = self.class.connection.post(uri, :content => payload)
|
106
|
+
resp = resp.split("\n")
|
107
|
+
raise "Ticket Create Failed" unless resp.first.include?("200")
|
108
|
+
if tid = resp[2].match(/^# Ticket (\d+) created./)
|
109
|
+
@attributes[:id] = tid[1].to_i
|
110
|
+
self.after_create
|
111
|
+
true
|
112
|
+
else
|
113
|
+
false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def create! #:nodoc:
|
118
|
+
raise "Ticket Create Failed" unless self.create
|
119
|
+
true
|
120
|
+
end
|
121
|
+
|
122
|
+
class << self #class methods
|
123
|
+
|
124
|
+
# Searches for a ticket or group of tickets with an active record like interface.
|
125
|
+
#
|
126
|
+
# Find has 3 different ways to search for tickets
|
127
|
+
#
|
128
|
+
# * search for tickets by the id. This will search for the Ticket with the exact id and will automatically load the entire ticket into the object (full will return true).
|
129
|
+
# * search for all tickets with a hash for search options by specifying :all along with your options. This will return an array of tickets or an empty array if no tickets are found that match the options.
|
130
|
+
# * search for a single ticket with a hash for search options by specifying :first along with your options. This will return a single ticket object or nil if no tickets are found.
|
131
|
+
#
|
132
|
+
# A hash of options for search paramaters are passed in as the last argument.
|
133
|
+
#
|
134
|
+
# ====Parameters
|
135
|
+
# * <tt>:queue</tt> or <tt>:queues</tt> - the name of a queue in the ticket system. This can be specified as a string, a symbol or an array of strings or symbols. The array will search for tickets included in either queue.
|
136
|
+
# * <tt>:status</tt> - the status of the tickets to search for. This can be specified as a string, a symbol or an array of strings or symbols.
|
137
|
+
# * <tt>:subject</tt>, <tt>:content</tt>, <tt>content_type</tt>, <tt>file_name</tt> - takes a string and searches for that string in the respective field.
|
138
|
+
# * <tt>:created</tt>, <tt>:started</tt>, <tt>:resolved</tt>, <tt>:told</tt>, <tt>:last_updated</tt>, <tt>:starts</tt>, <tt>:due</tt>, <tt>:updated</tt> - looks for dates for the respective fields. Can take a Range, Array, String, Time. Range will find all tickets between the two dates (after the first, before the last). Array works the same way, using #first and #last on the array. The elements should be either db-time formatted strings or Time objects. Time will be formatted as a db string. String will be passed straight to the search.
|
139
|
+
# * <tt>:custom_fields</tt> - takes a hash of custom fields to search for. the key should be the name of the field exactly how it is in RT and the value will be what to search for.
|
140
|
+
#
|
141
|
+
# ==== Examples
|
142
|
+
#
|
143
|
+
# # find first
|
144
|
+
# MyTicket.find(:first)
|
145
|
+
# MyTicket.find(:first, :queue => 'My Queue')
|
146
|
+
# MyTicket.find(:first, :status => [:new, :open])
|
147
|
+
# MyTicket.find(:first, :queue => 'My Queue', :status => :resolved)
|
148
|
+
# MyTicket.find(:first, :custom_fields => {:phone => '8675309'})
|
149
|
+
#
|
150
|
+
# # find all
|
151
|
+
# MyTicket.find(:all, :subject => 'is down')
|
152
|
+
# MyTicket.find(:all, :created => [Time.now - 300, Time.now])
|
153
|
+
# MyTicket.find(:all, :queues => ['my queue', 'issues'])
|
154
|
+
#
|
155
|
+
# # find by id
|
156
|
+
# MyTicket.find(12345)
|
157
|
+
#
|
158
|
+
def find(*args)
|
159
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
160
|
+
case args.first
|
161
|
+
when :first then find_initial(options)
|
162
|
+
when :all then find_all(options)
|
163
|
+
else find_by_ids(args, options)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
# Gives or Sets the connection object for the RT Server.
|
169
|
+
# Accepts 3 parameters :server, :user, and :pass. Call this
|
170
|
+
# at the top of your subclass to create the connection,
|
171
|
+
# class Ticket < Roart::Ticket
|
172
|
+
# connection :server => 'server', :user => 'user', :pass => 'pass'
|
173
|
+
# end
|
174
|
+
#
|
175
|
+
def connection(options=nil)
|
176
|
+
if options
|
177
|
+
@connection = Roart::Connection.new(options)
|
178
|
+
else
|
179
|
+
defined?(@connection) ? @connection : nil
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Adds a default queue to search each time. This is overridden by
|
184
|
+
# specifically including a :queue option in your find method. This can
|
185
|
+
# be an array of queue names or a string with a single queue name.
|
186
|
+
#
|
187
|
+
def default_queue(options=nil)
|
188
|
+
if options
|
189
|
+
@default_queue = options
|
190
|
+
else
|
191
|
+
defined?(@default_queue) ? @default_queue : nil
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# creates a new ticket object and immediately saves it to the database.
|
196
|
+
def create(options)
|
197
|
+
ticket = self.new(options)
|
198
|
+
ticket.save
|
199
|
+
ticket
|
200
|
+
end
|
201
|
+
|
202
|
+
protected
|
203
|
+
|
204
|
+
def instantiate(attrs) #:nodoc:
|
205
|
+
object = nil
|
206
|
+
if attrs.is_a?(Array)
|
207
|
+
array = Array.new
|
208
|
+
attrs.each do |attr|
|
209
|
+
object = self.allocate
|
210
|
+
object.instance_variable_set("@attributes", attr)
|
211
|
+
object.send("add_methods!")
|
212
|
+
array << object
|
213
|
+
end
|
214
|
+
return array
|
215
|
+
elsif attrs.is_a?(Hash)
|
216
|
+
object = self.allocate
|
217
|
+
object.instance_variable_set("@attributes", attrs)
|
218
|
+
object.send("add_methods!")
|
219
|
+
end
|
220
|
+
object.instance_variable_set("@history", false)
|
221
|
+
object
|
222
|
+
end
|
223
|
+
|
224
|
+
def find_initial(options={}) #:nodoc:
|
225
|
+
options.update(:limit => 1)
|
226
|
+
find_all(options).first
|
227
|
+
end
|
228
|
+
|
229
|
+
def find_all(options) #:nodoc:
|
230
|
+
uri = construct_search_uri(options)
|
231
|
+
tickets = get_tickets_from_search_uri(uri)
|
232
|
+
end
|
233
|
+
|
234
|
+
def find_by_ids(args, options) #:nodoc:
|
235
|
+
get_ticket_by_id(args.first)
|
236
|
+
end
|
237
|
+
|
238
|
+
def page_array(uri) #:nodoc:
|
239
|
+
page = self.connection.get(uri)
|
240
|
+
raise TicketSystemError, "Can't get ticket." unless page
|
241
|
+
page = page.split("\n")
|
242
|
+
status = page.delete_at(0)
|
243
|
+
if status.include?("200")
|
244
|
+
page.delete_if{|x| !x.include?(":")}
|
245
|
+
page
|
246
|
+
else
|
247
|
+
raise TicketSystemInterfaceError, "Error Getting Ticket: #{status}"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def get_tickets_from_search_uri(uri) #:nodoc:
|
252
|
+
page = page_array(uri)
|
253
|
+
page.extend(Roart::TicketPage)
|
254
|
+
page = page.to_search_array
|
255
|
+
self.instantiate(page)
|
256
|
+
end
|
257
|
+
|
258
|
+
def get_ticket_from_uri(uri) #:nodoc:
|
259
|
+
page = page_array(uri)
|
260
|
+
page.extend(Roart::TicketPage)
|
261
|
+
page = page.to_hash
|
262
|
+
ticket = self.instantiate(page)
|
263
|
+
ticket.instance_variable_set("@full", true)
|
264
|
+
ticket
|
265
|
+
end
|
266
|
+
|
267
|
+
def get_ticket_by_id(id) #:nodoc:
|
268
|
+
uri = "#{self.connection.server}/REST/1.0/ticket/"
|
269
|
+
uri << id.to_s
|
270
|
+
get_ticket_from_uri(uri)
|
271
|
+
end
|
272
|
+
|
273
|
+
def construct_search_uri(options={}) #:nodoc:
|
274
|
+
uri = "#{self.connection.server}/REST/1.0/search/ticket?"
|
275
|
+
uri << 'orderby=-Created&' if options.delete(:order)
|
276
|
+
unless options.empty? && default_queue.nil?
|
277
|
+
uri << 'query= '
|
278
|
+
query = Array.new
|
279
|
+
|
280
|
+
if options[:queues] || options[:queue]
|
281
|
+
add_queue!(query, options[:queues] || options[:queue])
|
282
|
+
else
|
283
|
+
add_queue!(query, default_queue)
|
284
|
+
end
|
285
|
+
add_dates!(query, options)
|
286
|
+
add_searches!(query, options)
|
287
|
+
add_status!(query, options[:status])
|
288
|
+
add_custom_fields!(query, options[:custom_fields])
|
289
|
+
|
290
|
+
query << options[:conditions].to_s.chomp if options[:conditions]
|
291
|
+
|
292
|
+
uri << query.join(" AND ")
|
293
|
+
end
|
294
|
+
uri
|
295
|
+
end
|
296
|
+
|
297
|
+
def add_queue!(uri, queue) #:nodoc:
|
298
|
+
return false unless queue
|
299
|
+
if queue.is_a?(Array)
|
300
|
+
queues = Array.new
|
301
|
+
queue.each do |name|
|
302
|
+
queues << "Queue = '#{name}'"
|
303
|
+
end
|
304
|
+
uri << '( ' + queues.join(' OR ') + ' )'
|
305
|
+
elsif queue.is_a?(String) || queue.is_a?(Symbol)
|
306
|
+
uri << "Queue = '#{queue.to_s}'"
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def add_custom_fields!(uri, options) #:nodoc:
|
311
|
+
return false unless options
|
312
|
+
options.each do |field, value|
|
313
|
+
if value.is_a?(Array)
|
314
|
+
valpart = Array.new
|
315
|
+
for val in value
|
316
|
+
valpart << "'CF.{#{field}}' = '#{val.to_s}'"
|
317
|
+
end
|
318
|
+
uri << '( ' + valpart.join(" OR ") + ' )'
|
319
|
+
elsif value.is_a?(String)
|
320
|
+
uri << "'CF.{#{field}}' = '#{value.to_s}'"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def add_status!(uri, options) #:nodoc:
|
326
|
+
return false unless options
|
327
|
+
parts = Array.new
|
328
|
+
if options.is_a?(Array)
|
329
|
+
statpart = Array.new
|
330
|
+
for status in options
|
331
|
+
statpart << "Status = '#{status.to_s}'"
|
332
|
+
end
|
333
|
+
parts << '( ' + statpart.join(" OR ") + ' )'
|
334
|
+
elsif options.is_a?(String) || options.is_a?(Symbol)
|
335
|
+
parts << "Status = '#{options.to_s}'"
|
336
|
+
end
|
337
|
+
uri << parts
|
338
|
+
end
|
339
|
+
|
340
|
+
def add_searches!(uri, options) #:nodoc:
|
341
|
+
search_fields = %w( subject content content_type file_name owner requestors cc admin_cc)
|
342
|
+
options.each do |key, value|
|
343
|
+
if search_fields.include?(key.to_s)
|
344
|
+
key = key.to_s.camelize
|
345
|
+
parts = Array.new
|
346
|
+
if value.is_a?(Array)
|
347
|
+
value.each do |v|
|
348
|
+
parts << "#{key} LIKE '#{v}'"
|
349
|
+
end
|
350
|
+
uri << '( ' + parts.join(" AND ") + ' )'
|
351
|
+
elsif value.is_a?(String)
|
352
|
+
uri << "#{key} LIKE '#{value}'"
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def add_dates!(uri, options) #:nodoc:
|
359
|
+
date_field = %w( created started resolved told last_updated starts due updated )
|
360
|
+
options.each do |key, value|
|
361
|
+
if date_field.include?(key.to_s)
|
362
|
+
key = key.to_s.camelize
|
363
|
+
parts = Array.new
|
364
|
+
if value.is_a?(Range) or value.is_a?(Array)
|
365
|
+
parts << "#{key} > '#{value.first.is_a?(Time) ? value.first.strftime("%Y-%m-%d %H:%M:%S") : value.first.to_s}'"
|
366
|
+
parts << "#{key} < '#{value.last.is_a?(Time) ? value.last.strftime("%Y-%m-%d %H:%M:%S") : value.last.to_s}'"
|
367
|
+
elsif value.is_a?(String)
|
368
|
+
parts << "#{key} > '#{value.to_s}'"
|
369
|
+
elsif value.is_a?(Time)
|
370
|
+
parts << "#{key} > '#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
|
371
|
+
end
|
372
|
+
uri << '( ' + parts.join(" AND ") + ' )'
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
end
|
378
|
+
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Roart
|
2
|
+
|
3
|
+
module TicketPage
|
4
|
+
|
5
|
+
IntKeys = %w[id]
|
6
|
+
|
7
|
+
def to_hash
|
8
|
+
hash = Hash.new
|
9
|
+
self.delete_if{|x| !x.include?(":")}
|
10
|
+
self.each do |ln|
|
11
|
+
ln = ln.split(":")
|
12
|
+
if ln[0] && ln[0].match(/^CF-.+/)
|
13
|
+
key = ln.delete_at(0)
|
14
|
+
key = "cf_" + key[3..key.length].gsub(/ /, "_")
|
15
|
+
else
|
16
|
+
key = ln.delete_at(0).strip.underscore
|
17
|
+
value = ln.join(":").strip
|
18
|
+
hash[key.to_sym] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
hash[:id] = hash[:id].split("/").last.to_i
|
22
|
+
hash
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_search_array
|
26
|
+
array = Array.new
|
27
|
+
self.delete_if{|x| !x.include?(":")}
|
28
|
+
self.each do |ln|
|
29
|
+
hash = Hash.new
|
30
|
+
ln = ln.split(":")
|
31
|
+
id = ln.delete_at(0).strip.underscore
|
32
|
+
sub = ln.join(":").strip
|
33
|
+
hash[:id] = id.to_i
|
34
|
+
hash[:subject] = sub
|
35
|
+
hash[:full] = false
|
36
|
+
hash[:history] = false
|
37
|
+
array << hash
|
38
|
+
end
|
39
|
+
array
|
40
|
+
end
|
41
|
+
|
42
|
+
# TODO: Don't throw away attachments (/^ {13})
|
43
|
+
def to_history_hash
|
44
|
+
hash = Hash.new
|
45
|
+
self.delete_if{|x| !x.include?(":") && !x.match(/^ {9}/) && !x.match(/^ {13}/)}
|
46
|
+
self.each do |ln|
|
47
|
+
if ln.match(/^ {9}/) && !ln.match(/^ {13}/)
|
48
|
+
hash[:content] << "\n" + ln.strip if hash[:content]
|
49
|
+
elsif ln.match(/^ {13}/)
|
50
|
+
hash[:attachments] << "\n" + ln.strip if hash[:attachments]
|
51
|
+
else
|
52
|
+
ln = ln.split(":")
|
53
|
+
unless ln.size == 1 || ln.first == 'Ticket' # we don't want to override the ticket method.
|
54
|
+
key = ln.delete_at(0).strip.underscore
|
55
|
+
value = ln.join(":").strip
|
56
|
+
hash[key.to_sym] = IntKeys.include?(key) ? value.to_i : value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
hash
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|