threetee-roart 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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