ludo-roart 0.1.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
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) : nil
6
+ end
7
+ raise ArgumentError, "Not all required fields entered"
8
+ end
9
+ end
10
+
11
+ def self.check_keys(hash, required)
12
+ unless required.inject(true) do |inc, attr|
13
+ inc ? hash.keys.include?(attr.to_sym) : nil
14
+ end
15
+ return false
16
+ end
17
+ return true
18
+ end
19
+
20
+ module MethodFunctions
21
+
22
+ def add_methods!
23
+ @attributes.each do |key, value|
24
+ (class << self; self; end).send :define_method, key do
25
+ return @attributes[key]
26
+ end
27
+ (class << self; self; end).send :define_method, "#{key}=" do |new_val|
28
+ @attributes[key] = new_val
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,461 @@
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 text).inject({}){|memo, k| memo[k] = nil; memo}
6
+ RequiredAttributes = %w(queue subject)
7
+
8
+ end
9
+
10
+ class Ticket
11
+
12
+ include Roart::MethodFunctions
13
+ include Roart::Callbacks
14
+ require File.join(File.dirname(__FILE__), %w[ validations.rb ])
15
+ include Roart::Validations
16
+
17
+ attr_reader :full, :history, :saved
18
+
19
+ # Creates a new ticket. Attributes queue and subject are required. Expects a hash with the attributes of the ticket.
20
+ #
21
+ # ticket = MyTicket.new(:queue => "Some Queue", :subject => "The System is Down.")
22
+ # ticket.id #-> This will be the ID of the ticket in the RT System.
23
+ #
24
+ def initialize(attributes=nil)
25
+ if attributes
26
+ @attributes = Roart::Tickets::DefaultAttributes.merge(attributes)
27
+ else
28
+ @attributes = Roart::Tickets::DefaultAttributes
29
+ end
30
+ @attributes.update(:id => 'ticket/new')
31
+ @saved = false
32
+ @history = false
33
+ @new_record = true
34
+ add_methods!
35
+ end
36
+
37
+ # Loads all information for a ticket from RT and lets full to true.
38
+ # This changes the ticket object and adds methods for all the fields on the ticket.
39
+ # Custom fields will be prefixed with 'cf' so a custom field of 'phone'
40
+ # 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
41
+ #
42
+ def load_full!
43
+ unless self.full
44
+ ticket = self.class.find(self.id)
45
+ @attributes = ticket.instance_variable_get("@attributes")
46
+ add_methods!
47
+ end
48
+ end
49
+
50
+ #loads the ticket history from rt
51
+ #
52
+ def histories
53
+ @histories ||= Roart::History.default(:ticket => self)
54
+ end
55
+
56
+ # 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
57
+ #
58
+ def save
59
+ if self.id == "ticket/new"
60
+ self.create
61
+ else
62
+ self.update
63
+ end
64
+ end
65
+
66
+ # Add a comment to a ticket
67
+ # Example:
68
+ # tix = Ticket.find(1000)
69
+ # tix.comment("This is a comment", :time_worked => 45, :cc => 'someone@example.com')
70
+ def comment(comment, opt = {})
71
+ comment = {:text => comment, :action => 'Correspond'}.merge(opt)
72
+
73
+ uri = "#{self.class.connection.server}/REST/1.0/ticket/#{self.id}/comment"
74
+ payload = comment.to_content_format
75
+ resp = self.class.connection.post(uri, :content => payload)
76
+ resp = resp.split("\n")
77
+ raise TicketSystemError, "Ticket Comment Failed" unless resp.first.include?("200")
78
+ !!resp[2].match(/^# Message recorded/)
79
+ end
80
+
81
+ # works just like save, but if the save fails, it raises an exception instead of silently returning false
82
+ #
83
+ def save!
84
+ raise TicketSystemError, "Ticket Create Failed" unless self.save
85
+ true
86
+ end
87
+
88
+ def new_record?
89
+ return @new_record
90
+ end
91
+
92
+ protected
93
+
94
+ def create #:nodoc:
95
+ self.before_create
96
+ uri = "#{self.class.connection.server}/REST/1.0/ticket/new"
97
+ payload = @attributes.to_content_format
98
+ resp = self.class.connection.post(uri, :content => payload)
99
+
100
+ process_save_response(resp, :create)
101
+ end
102
+
103
+ def update
104
+ self.before_update
105
+ uri = "#{self.class.connection.server}/REST/1.0/ticket/#{self.id}/edit"
106
+ payload = @attributes.clone
107
+ payload.delete("text")
108
+ payload.delete("id") # Can't have text in an update, only create, use comment for updateing
109
+ payload = payload.to_content_format
110
+ resp = self.class.connection.post(uri, :content => payload)
111
+
112
+ process_save_response(resp, :update)
113
+ end
114
+
115
+ SUCCESS_CODES = (200..299).to_a
116
+ CLIENT_ERROR_CODES = (400..499).to_a
117
+
118
+ def process_save_response(response, action)
119
+ errors.clear
120
+ action_name = action.to_s.capitalize
121
+
122
+ lines = response.split("\n").reject { |l| l.blank? }
123
+
124
+ status_line = lines.shift
125
+ status_line.present? or
126
+ raise TicketSystemError, "Ticket #{action_name} Failed (blank response)"
127
+
128
+ version, status_code, status_text = status_line.split(/\s+/,2)
129
+
130
+ if SUCCESS_CODES.include?(status_code.to_i) && lines[0] =~ /^# Ticket (\d+) (created|updated)/
131
+ @attributes[:id] = $1.to_i if $2 == 'created'
132
+ @new_record = false
133
+ @saved = true
134
+ self.__send__("after_#{action}")
135
+ return true
136
+ elsif (SUCCESS_CODES + CLIENT_ERROR_CODES).include?(status_code.to_i)
137
+ lines[0] =~ /^# Could not (create|update) ticket/ and lines.shift
138
+ lines.each { |line| errors.add_to_base(line) if line =~ /^#/ }
139
+ return false
140
+ else
141
+ raise TicketSystemError, "Ticket #{action_name} Failed (#{status_line})"
142
+ end
143
+ end
144
+
145
+ def create! #:nodoc:
146
+ raise TicketSystemError, "Ticket Create Failed" unless self.create
147
+ true
148
+ end
149
+
150
+ class << self #class methods
151
+
152
+ # Searches for a ticket or group of tickets with an active record like interface.
153
+ #
154
+ # Find has 3 different ways to search for tickets
155
+ #
156
+ # * 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).
157
+ # * 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.
158
+ # * 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.
159
+ #
160
+ # A hash of options for search paramaters are passed in as the last argument.
161
+ #
162
+ # ====Parameters
163
+ # * <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.
164
+ # * <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.
165
+ # * <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.
166
+ # * <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.
167
+ # * <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.
168
+ #
169
+ # ==== Examples
170
+ #
171
+ # # find first
172
+ # MyTicket.find(:first)
173
+ # MyTicket.find(:first, :queue => 'My Queue')
174
+ # MyTicket.find(:first, :status => [:new, :open])
175
+ # MyTicket.find(:first, :queue => 'My Queue', :status => :resolved)
176
+ # MyTicket.find(:first, :custom_fields => {:phone => '8675309'})
177
+ #
178
+ # # find all
179
+ # MyTicket.find(:all, :subject => 'is down')
180
+ # MyTicket.find(:all, :created => [Time.now - 300, Time.now])
181
+ # MyTicket.find(:all, :queues => ['my queue', 'issues'])
182
+ #
183
+ # # find by id
184
+ # MyTicket.find(12345)
185
+ #
186
+ def find(*args)
187
+ options = args.last.is_a?(Hash) ? args.pop : {}
188
+ case args.first
189
+ when :first then find_initial(options)
190
+ when :all then find_all(options)
191
+ else find_by_ids(args, options)
192
+ end
193
+ end
194
+
195
+
196
+ # Accepts parameters for connecting to an RT server.
197
+ # Required:
198
+ # :server sets the URL for the rt server, :ie http://rt.server.com/
199
+ # Optional:
200
+ # :user sets the username to connect to RT
201
+ # :pass sets the password for the user to connect with
202
+ # :adapter is the connection adapter to connect with. Defaults to Mechanize
203
+ #
204
+ # class Ticket < Roart::Ticket
205
+ # connection :server => 'server', :user => 'user', :pass => 'pass'
206
+ # end
207
+ #
208
+ def connection(options=nil)
209
+ if options
210
+ @connection = Roart::Connection.new({:adapter => "mechanize"}.merge(options))
211
+ else
212
+ defined?(@connection) ? @connection : nil
213
+ end
214
+ end
215
+
216
+ # Sets the username and password used to connect to the RT server
217
+ # Required:
218
+ # :user sets the username to connect to RT
219
+ # :pass sets the password for the user to connect with
220
+ # This can be used to change a connection once the Ticket class has
221
+ # been initialized. Not required if you sepecify :user and :pass in
222
+ # the connection method
223
+ #
224
+ # class Ticket < Roart::Ticket
225
+ # connection :server => 'server'
226
+ # authenticate :user => 'user', :pass => 'pass'
227
+ # end
228
+ #
229
+ def authenticate(options)
230
+ @connection.authenticate(options)
231
+ end
232
+
233
+ # Adds a default queue to search each time. This is overridden by
234
+ # specifically including a :queue option in your find method. This can
235
+ # be an array of queue names or a string with a single queue name.
236
+ #
237
+ def default_queue(options=nil)
238
+ if options
239
+ @default_queue = options
240
+ else
241
+ defined?(@default_queue) ? @default_queue : nil
242
+ end
243
+ end
244
+
245
+ # creates a new ticket object and immediately saves it to the database.
246
+ def create(options)
247
+ ticket = self.new(options)
248
+ ticket.save
249
+ ticket
250
+ end
251
+
252
+ protected
253
+
254
+ def instantiate(attrs) #:nodoc:
255
+ object = nil
256
+ if attrs.is_a?(Array)
257
+ array = Array.new
258
+ attrs.each do |attr|
259
+ object = self.allocate
260
+ object.instance_variable_set("@attributes", attr)
261
+ object.send("add_methods!")
262
+ array << object
263
+ end
264
+ return array
265
+ elsif attrs.is_a?(Hash) || attrs.is_a?(HashWithIndifferentAccess)
266
+ object = self.allocate
267
+ object.instance_variable_set("@attributes", attrs)
268
+ object.send("add_methods!")
269
+ end
270
+ object.instance_variable_set("@history", false)
271
+ object.instance_variable_set("@new_record", false)
272
+ object
273
+ end
274
+
275
+ def find_initial(options={}) #:nodoc:
276
+ options.update(:limit => 1)
277
+ find_all(options).first
278
+ end
279
+
280
+ def find_all(options) #:nodoc:
281
+ uri = construct_search_uri(options)
282
+ tickets = get_tickets_from_search_uri(uri)
283
+ end
284
+
285
+ def find_by_ids(args, options) #:nodoc:
286
+ raise ArgumentError, "First argument must be :all or :first, or an ID with no hash options" unless args.first.is_a?(Fixnum) || args.first.is_a?(String)
287
+ get_ticket_by_id(args.first)
288
+ end
289
+
290
+ def page_array(uri) #:nodoc:
291
+ page = self.connection.get(uri)
292
+ raise TicketSystemError, "Can't get ticket." unless page
293
+ page = page.split("\n")
294
+ status = page.delete_at(0)
295
+ if status.include?("200")
296
+ page.delete_if{|x| !x.include?(":")}
297
+ page
298
+ else
299
+ raise TicketSystemInterfaceError, "Error Getting Ticket: #{status}"
300
+ end
301
+ end
302
+
303
+ def page_list_array(uri) #:nodoc:
304
+ page = self.connection.get(uri)
305
+ raise TicketSystemInterfaceError, "Can't get ticket." unless page
306
+ page = page.split("\n")
307
+ status = page.delete_at(0)
308
+ if status.include?("200")
309
+ page = page.join("\n")
310
+ chunks = page.split(/^--$/)
311
+ page = []
312
+ for chunk in chunks
313
+ chunk = chunk.split("\n")
314
+ chunk.delete_if{|x| !x.include?(":")}
315
+ page << chunk
316
+ end
317
+ page
318
+ else
319
+ raise TicketSystemInterfaceError, "Error Getting Ticket: #{status}"
320
+ end
321
+ end
322
+
323
+ def get_tickets_from_search_uri(uri) #:nodoc:
324
+ page = page_list_array(uri + "&format=l")
325
+ page.extend(Roart::TicketPage)
326
+ page = page.to_search_list_array
327
+ array = Array.new
328
+ for ticket in page
329
+ ticket = self.instantiate(ticket)
330
+ ticket.instance_variable_set("@full", true)
331
+ array << ticket
332
+ end
333
+ array ||= []
334
+ end
335
+
336
+ def get_ticket_from_uri(uri) #:nodoc:
337
+ page = page_array(uri)
338
+ page.extend(Roart::TicketPage)
339
+ unless page = page.to_hash
340
+ raise TicketNotFoundError, "No ticket matching search criteria found."
341
+ end
342
+ ticket = self.instantiate(page)
343
+ ticket.instance_variable_set("@full", true)
344
+ ticket
345
+ end
346
+
347
+ def get_ticket_by_id(id) #:nodoc:
348
+ uri = "#{self.connection.server}/REST/1.0/ticket/"
349
+ uri << id.to_s
350
+ get_ticket_from_uri(uri)
351
+ end
352
+
353
+ def construct_search_uri(options={}) #:nodoc:
354
+ uri = "#{self.connection.server}/REST/1.0/search/ticket?"
355
+ uri << 'orderby=-Created&' if options.delete(:order)
356
+ unless options.empty? && default_queue.nil?
357
+ uri << 'query= '
358
+ query = Array.new
359
+
360
+ if options[:queues] || options[:queue]
361
+ add_queue!(query, options[:queues] || options[:queue])
362
+ else
363
+ add_queue!(query, default_queue)
364
+ end
365
+ add_dates!(query, options)
366
+ add_searches!(query, options)
367
+ add_status!(query, options[:status])
368
+ add_custom_fields!(query, options[:custom_fields])
369
+
370
+ query << options[:conditions].to_s.chomp if options[:conditions]
371
+
372
+ uri << query.join(" AND ")
373
+ end
374
+ uri
375
+ end
376
+
377
+ def add_queue!(uri, queue) #:nodoc:
378
+ return false unless queue
379
+ if queue.is_a?(Array)
380
+ queues = Array.new
381
+ queue.each do |name|
382
+ queues << "Queue = '#{name}'"
383
+ end
384
+ uri << '( ' + queues.join(' OR ') + ' )'
385
+ elsif queue.is_a?(String) || queue.is_a?(Symbol)
386
+ uri << "Queue = '#{queue.to_s}'"
387
+ end
388
+ end
389
+
390
+ def add_custom_fields!(uri, options) #:nodoc:
391
+ return false unless options
392
+ options.each do |field, value|
393
+ if value.is_a?(Array)
394
+ valpart = Array.new
395
+ for val in value
396
+ valpart << "'CF.{#{field}}' = '#{val.to_s}'"
397
+ end
398
+ uri << '( ' + valpart.join(" OR ") + ' )'
399
+ elsif value.is_a?(String)
400
+ uri << "'CF.{#{field}}' = '#{value.to_s}'"
401
+ end
402
+ end
403
+ end
404
+
405
+ def add_status!(uri, options) #:nodoc:
406
+ return false unless options
407
+ parts = Array.new
408
+ if options.is_a?(Array)
409
+ statpart = Array.new
410
+ for status in options
411
+ statpart << "Status = '#{status.to_s}'"
412
+ end
413
+ parts << '( ' + statpart.join(" OR ") + ' )'
414
+ elsif options.is_a?(String) || options.is_a?(Symbol)
415
+ parts << "Status = '#{options.to_s}'"
416
+ end
417
+ uri << parts
418
+ end
419
+
420
+ def add_searches!(uri, options) #:nodoc:
421
+ search_fields = %w( subject content content_type file_name owner requestors cc admin_cc)
422
+ options.each do |key, value|
423
+ if search_fields.include?(key.to_s)
424
+ key = key.to_s.camelize
425
+ parts = Array.new
426
+ if value.is_a?(Array)
427
+ value.each do |v|
428
+ parts << "#{key} LIKE '#{v}'"
429
+ end
430
+ uri << '( ' + parts.join(" AND ") + ' )'
431
+ elsif value.is_a?(String)
432
+ uri << "#{key} LIKE '#{value}'"
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ def add_dates!(uri, options) #:nodoc:
439
+ date_field = %w( created started resolved told last_updated starts due updated )
440
+ options.each do |key, value|
441
+ if date_field.include?(key.to_s)
442
+ key = key.to_s.camelize
443
+ parts = Array.new
444
+ if value.is_a?(Range) or value.is_a?(Array)
445
+ parts << "#{key} > '#{value.first.is_a?(Time) ? value.first.strftime("%Y-%m-%d %H:%M:%S") : value.first.to_s}'"
446
+ parts << "#{key} < '#{value.last.is_a?(Time) ? value.last.strftime("%Y-%m-%d %H:%M:%S") : value.last.to_s}'"
447
+ elsif value.is_a?(String)
448
+ parts << "#{key} > '#{value.to_s}'"
449
+ elsif value.is_a?(Time)
450
+ parts << "#{key} > '#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
451
+ end
452
+ uri << '( ' + parts.join(" AND ") + ' )'
453
+ end
454
+ end
455
+ end
456
+
457
+ end
458
+
459
+ end
460
+
461
+ end