axtro-roart 0.1.8

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