roart 0.1.4 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/README.rdoc +23 -19
- data/lib/roart/connection.rb +32 -30
- data/lib/roart/connection_adapter.rb +21 -0
- data/lib/roart/connection_adapters/mechanize_adapter.rb +30 -0
- data/lib/roart/core/string.rb +5 -0
- data/lib/roart/roart.rb +9 -0
- data/lib/roart/ticket.rb +91 -60
- data/lib/roart/validations.rb +221 -0
- data/lib/roart.rb +1 -1
- data/roart.gemspec +3 -3
- data/spec/roart/connection_adapter_spec.rb +11 -0
- data/spec/roart/connection_spec.rb +43 -0
- data/spec/roart/ticket_spec.rb +7 -4
- metadata +6 -2
data/History.txt
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
==0.1.5 / 2010-01-19
|
2
|
+
Can add transactions ticket using #comment method. (via btucker)
|
3
|
+
Can authenticate again after the Ticket class is declared. (via newellista)
|
4
|
+
More search fields are now avaliable. (via threetee)
|
5
|
+
|
1
6
|
==0.1.4 / 2009-08-19
|
2
7
|
Implemented callbacks, you can now hook into the ticket life-cycle with before and after create and update.
|
3
8
|
Saving works like ActiveRecord now, with save returning true if successful and false otherwise, and save! raising an error if unsuccessful.
|
data/README.rdoc
CHANGED
@@ -1,61 +1,61 @@
|
|
1
1
|
== Roart
|
2
|
-
|
2
|
+
|
3
3
|
\___\__o/
|
4
4
|
/ \
|
5
5
|
|
6
|
-
|
6
|
+
|
7
7
|
by PJ Davis
|
8
8
|
http://github.com/pjdavis/roart
|
9
9
|
|
10
10
|
== DESCRIPTION:
|
11
|
-
If you are using Best Practical's Request Tracker (RT) and you need to interact with tickets from other applications, Roart provides an interface that is slightly reminiscent of ActiveRecord.
|
11
|
+
If you are using Best Practical's Request Tracker (RT) and you need to interact with tickets from other applications, Roart provides an interface that is slightly reminiscent of ActiveRecord.
|
12
12
|
|
13
13
|
|
14
14
|
== FEATURES/PROBLEMS:
|
15
15
|
|
16
16
|
* Access RT Tickets through an ActiveRecord like API
|
17
|
-
|
18
|
-
* This has only been tested against RT 3.6. Changes to the REST interface in later versions of RT may break stuff.
|
17
|
+
|
18
|
+
* This has only been tested against RT 3.6. Changes to the REST interface in later versions of RT may break stuff.
|
19
19
|
|
20
20
|
== SYNOPSIS:
|
21
21
|
|
22
22
|
* Create a class to interact with your ticket system
|
23
|
-
|
23
|
+
|
24
24
|
require 'rubygems'
|
25
25
|
require 'roart'
|
26
|
-
|
26
|
+
|
27
27
|
class Ticket < Roart::Ticket
|
28
28
|
connection :server => 'http://my.rt.server.com', :user => 'myuser', :pass => 'mypass'
|
29
|
-
|
29
|
+
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
* Search for tickets
|
33
33
|
|
34
34
|
my_tickets = Ticket.find(:all, :queue => 'Scutters', :status => [:new, :open])
|
35
35
|
my_tickets.each do |ticket|
|
36
36
|
puts ticket.subject
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
#-> New John Wayne packages
|
40
40
|
#-> Medi-lab training
|
41
|
-
|
41
|
+
|
42
42
|
* See all info for a ticket
|
43
|
-
|
43
|
+
|
44
44
|
my_ticket = Ticket.find(:first, :queue => 'Issues', :status => :new)
|
45
45
|
ticket.creator #-> rimmer@reddwarf.com
|
46
46
|
ticket.subject #-> Where is the Bomb?
|
47
|
-
|
47
|
+
|
48
48
|
* Get history for a ticket
|
49
|
-
|
49
|
+
|
50
50
|
my_ticket.histories #-> Returns an array of history objects
|
51
|
-
|
51
|
+
|
52
52
|
* Create a new ticket
|
53
53
|
|
54
54
|
issue = Ticket.new(:queue => 'some_queue', :subject => 'This is not working for me')
|
55
55
|
issue.id #-> 'ticket/new'
|
56
56
|
issue.save
|
57
57
|
issue.id #-> 23423
|
58
|
-
|
58
|
+
|
59
59
|
* Update a ticket
|
60
60
|
|
61
61
|
ticket = Ticket.find(23452)
|
@@ -63,20 +63,24 @@ If you are using Best Practical's Request Tracker (RT) and you need to interact
|
|
63
63
|
ticket.subject #-> "Smoke me a kipper, I'll be back for breakfast."
|
64
64
|
ticket.save
|
65
65
|
ticket.subject #->"Smoke me a kipper, I'll be back for breakfast."
|
66
|
-
|
66
|
+
|
67
|
+
* Comment on a Ticket
|
68
|
+
ticket = Ticket.find(23452)
|
69
|
+
ticket.comment("This is a lovely Ticket", :time_worked => 45, :cc => 'someone@example.com'))
|
70
|
+
|
67
71
|
|
68
72
|
== REQUIREMENTS:
|
69
73
|
|
70
74
|
* mechanize
|
71
75
|
* A working RT3 install.
|
72
|
-
|
76
|
+
|
73
77
|
== INSTALL:
|
74
78
|
|
75
79
|
$ gem sources -a http://gems.github.com
|
76
80
|
$ sudo gem install pjdavis-roart
|
77
81
|
|
78
82
|
== LICENSE:
|
79
|
-
|
83
|
+
|
80
84
|
(C) PJ Davis <pj.davis@gmail.com>
|
81
85
|
|
82
86
|
This program is free software; you can redistribute it and/or modify it under
|
data/lib/roart/connection.rb
CHANGED
@@ -2,53 +2,55 @@ require 'yaml'
|
|
2
2
|
require 'mechanize'
|
3
3
|
|
4
4
|
module Roart
|
5
|
-
|
5
|
+
|
6
6
|
module Connections
|
7
|
-
RequiredConfig = %w(server
|
8
|
-
|
7
|
+
RequiredConfig = %w(server adapter)
|
8
|
+
RequiredToLogin = %w( user pass )
|
9
|
+
|
9
10
|
end
|
10
|
-
|
11
|
+
|
11
12
|
class Connection
|
12
|
-
|
13
|
+
|
13
14
|
attr_reader :agent
|
14
|
-
|
15
|
+
attr_reader :conf
|
16
|
+
|
15
17
|
def initialize(conf)
|
16
|
-
|
17
18
|
if conf.is_a?(String)
|
18
19
|
raise "Loading Config File not yet implemented"
|
19
20
|
elsif conf.is_a?(Hash)
|
20
|
-
Roart::check_keys!(conf, Roart::Connections::RequiredConfig)
|
21
21
|
@conf = conf
|
22
22
|
end
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
if Roart::check_keys(conf, Roart::Connections::RequiredConfig)
|
24
|
+
@agent = @conf[:login]
|
25
|
+
add_methods!
|
26
|
+
@connection = ConnectionAdapter.new(@conf)
|
27
|
+
else
|
28
|
+
raise "Configuration Error"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def authenticate(conf)
|
33
|
+
if Roart::check_keys(conf, Roart::Connections::RequiredToLogin)
|
34
|
+
@connection.authenticate(conf)
|
35
|
+
self
|
36
|
+
end
|
26
37
|
end
|
27
|
-
|
38
|
+
|
28
39
|
def rest_path
|
29
40
|
self.server + '/REST/1.0/'
|
30
41
|
end
|
31
|
-
|
42
|
+
|
32
43
|
def get(uri)
|
33
|
-
@
|
44
|
+
@connection.get(uri)
|
34
45
|
end
|
35
|
-
|
46
|
+
|
36
47
|
def post(uri, payload)
|
37
|
-
@
|
48
|
+
@connection.post(uri, payload)
|
38
49
|
end
|
39
|
-
|
50
|
+
|
40
51
|
protected
|
41
|
-
|
42
|
-
|
43
|
-
agent = WWW::Mechanize.new
|
44
|
-
page = agent.get(@conf[:server])
|
45
|
-
form = page.form('login')
|
46
|
-
form.user = @conf[:user]
|
47
|
-
form.pass = @conf[:pass]
|
48
|
-
page = agent.submit form
|
49
|
-
agent
|
50
|
-
end
|
51
|
-
|
52
|
+
|
53
|
+
|
52
54
|
def add_methods!
|
53
55
|
@conf.each do |key, value|
|
54
56
|
(class << self; self; end).send :define_method, key do
|
@@ -56,7 +58,7 @@ module Roart
|
|
56
58
|
end
|
57
59
|
end
|
58
60
|
end
|
59
|
-
|
61
|
+
|
60
62
|
end
|
61
|
-
|
63
|
+
|
62
64
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Roart
|
4
|
+
|
5
|
+
class ConnectionAdapter
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@adapter = Roart::ConnectionAdapters.const_get(config[:adapter].capitalize).new(config)
|
10
|
+
@adapter.login(config) if config[:user] && config[:pass]
|
11
|
+
end
|
12
|
+
|
13
|
+
def authenticate(config)
|
14
|
+
@adapter.login(config)
|
15
|
+
end
|
16
|
+
|
17
|
+
def_delegators :@adapter, :get, :post
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Roart
|
2
|
+
module ConnectionAdapters
|
3
|
+
class Mechanize
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@conf = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def login(config)
|
10
|
+
@conf.merge!(config)
|
11
|
+
agent = WWW::Mechanize.new
|
12
|
+
page = agent.get(@conf[:server])
|
13
|
+
form = page.form('login')
|
14
|
+
form.user = @conf[:user]
|
15
|
+
form.pass = @conf[:pass]
|
16
|
+
page = agent.submit form
|
17
|
+
@agent = agent
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(uri)
|
21
|
+
@agent.get(uri).body
|
22
|
+
end
|
23
|
+
|
24
|
+
def post(uri, payload)
|
25
|
+
@agent.post(uri, payload).body
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/roart/core/string.rb
CHANGED
data/lib/roart/roart.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
module Roart
|
2
2
|
|
3
3
|
def self.check_keys!(hash, required)
|
4
|
+
puts hash
|
4
5
|
unless required.inject(true) do |inc, attr|
|
5
6
|
inc ? hash.keys.include?(attr.to_sym) : nil
|
6
7
|
end
|
@@ -8,6 +9,14 @@ module Roart
|
|
8
9
|
end
|
9
10
|
end
|
10
11
|
|
12
|
+
def self.check_keys(hash, required)
|
13
|
+
unless required.inject(true) do |inc, attr|
|
14
|
+
inc ? hash.keys.include?(attr.to_sym) : nil
|
15
|
+
end
|
16
|
+
return false
|
17
|
+
end
|
18
|
+
return true
|
19
|
+
end
|
11
20
|
|
12
21
|
module MethodFunctions
|
13
22
|
|
data/lib/roart/ticket.rb
CHANGED
@@ -1,38 +1,40 @@
|
|
1
1
|
module Roart
|
2
|
-
|
2
|
+
|
3
3
|
module Tickets
|
4
|
-
|
4
|
+
|
5
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
6
|
RequiredAttributes = %w(queue subject)
|
7
|
-
|
7
|
+
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
class Ticket
|
11
|
-
|
11
|
+
|
12
12
|
include Roart::MethodFunctions
|
13
13
|
include Roart::Callbacks
|
14
|
-
|
14
|
+
require File.join(File.dirname(__FILE__), %w[ validations.rb ])
|
15
|
+
include Roart::Validations
|
16
|
+
|
15
17
|
attr_reader :full, :history, :saved
|
16
|
-
|
18
|
+
|
17
19
|
# Creates a new ticket. Attributes queue and subject are required. Expects a hash with the attributes of the ticket.
|
18
20
|
#
|
19
21
|
# ticket = MyTicket.new(:queue => "Some Queue", :subject => "The System is Down.")
|
20
22
|
# ticket.id #-> This will be the ID of the ticket in the RT System.
|
21
23
|
#
|
22
|
-
def initialize(attributes)
|
23
|
-
|
24
|
-
if attributes.is_a?(Hash)
|
24
|
+
def initialize(attributes=nil)
|
25
|
+
if attributes
|
25
26
|
@attributes = Roart::Tickets::DefaultAttributes.to_hash.merge(attributes)
|
26
|
-
@attributes.update(:id => 'ticket/new')
|
27
|
-
@saved = false
|
28
27
|
else
|
29
|
-
|
28
|
+
@attributes = Roart::Tickets::DefaultAttributes.to_hash
|
30
29
|
end
|
30
|
+
@attributes.update(:id => 'ticket/new')
|
31
|
+
@saved = false
|
31
32
|
@history = false
|
33
|
+
@new_record = true
|
32
34
|
add_methods!
|
33
35
|
end
|
34
|
-
|
35
|
-
# Loads all information for a ticket from RT and lets full to true.
|
36
|
+
|
37
|
+
# Loads all information for a ticket from RT and lets full to true.
|
36
38
|
# This changes the ticket object and adds methods for all the fields on the ticket.
|
37
39
|
# Custom fields will be prefixed with 'cf' so a custom field of 'phone'
|
38
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
|
@@ -44,13 +46,13 @@ module Roart
|
|
44
46
|
add_methods!
|
45
47
|
end
|
46
48
|
end
|
47
|
-
|
49
|
+
|
48
50
|
#loads the ticket history from rt
|
49
51
|
#
|
50
52
|
def histories
|
51
53
|
@histories ||= Roart::History.default(:ticket => self)
|
52
54
|
end
|
53
|
-
|
55
|
+
|
54
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
|
55
57
|
#
|
56
58
|
def save
|
@@ -88,16 +90,20 @@ module Roart
|
|
88
90
|
raise "Ticket Comment Failed" unless resp.first.include?("200")
|
89
91
|
!!resp[2].match(/^# Message recorded/)
|
90
92
|
end
|
91
|
-
|
93
|
+
|
92
94
|
# works just like save, but if the save fails, it raises an exception instead of silently returning false
|
93
95
|
#
|
94
96
|
def save!
|
95
97
|
raise "Ticket Create Failed" unless self.save
|
96
98
|
true
|
97
99
|
end
|
98
|
-
|
100
|
+
|
101
|
+
def new_record?
|
102
|
+
return @new_record
|
103
|
+
end
|
104
|
+
|
99
105
|
protected
|
100
|
-
|
106
|
+
|
101
107
|
def create #:nodoc:
|
102
108
|
self.before_create
|
103
109
|
uri = "#{self.class.connection.server}/REST/1.0/ticket/new"
|
@@ -108,19 +114,20 @@ module Roart
|
|
108
114
|
if tid = resp[2].match(/^# Ticket (\d+) created./)
|
109
115
|
@attributes[:id] = tid[1].to_i
|
110
116
|
self.after_create
|
117
|
+
@new_record = false
|
111
118
|
true
|
112
119
|
else
|
113
120
|
false
|
114
|
-
end
|
121
|
+
end
|
115
122
|
end
|
116
|
-
|
123
|
+
|
117
124
|
def create! #:nodoc:
|
118
125
|
raise "Ticket Create Failed" unless self.create
|
119
126
|
true
|
120
127
|
end
|
121
|
-
|
128
|
+
|
122
129
|
class << self #class methods
|
123
|
-
|
130
|
+
|
124
131
|
# Searches for a ticket or group of tickets with an active record like interface.
|
125
132
|
#
|
126
133
|
# Find has 3 different ways to search for tickets
|
@@ -133,15 +140,15 @@ module Roart
|
|
133
140
|
#
|
134
141
|
# ====Parameters
|
135
142
|
# * <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.
|
143
|
+
# * <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
144
|
# * <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
145
|
# * <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.
|
146
|
+
# * <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
147
|
#
|
141
148
|
# ==== Examples
|
142
149
|
#
|
143
150
|
# # find first
|
144
|
-
# MyTicket.find(:first)
|
151
|
+
# MyTicket.find(:first)
|
145
152
|
# MyTicket.find(:first, :queue => 'My Queue')
|
146
153
|
# MyTicket.find(:first, :status => [:new, :open])
|
147
154
|
# MyTicket.find(:first, :queue => 'My Queue', :status => :resolved)
|
@@ -163,24 +170,46 @@ module Roart
|
|
163
170
|
else find_by_ids(args, options)
|
164
171
|
end
|
165
172
|
end
|
166
|
-
|
167
173
|
|
168
|
-
|
169
|
-
# Accepts
|
170
|
-
#
|
174
|
+
|
175
|
+
# Accepts parameters for connecting to an RT server.
|
176
|
+
# Required:
|
177
|
+
# :server sets the URL for the rt server, :ie http://rt.server.com/
|
178
|
+
# Optional:
|
179
|
+
# :user sets the username to connect to RT
|
180
|
+
# :pass sets the password for the user to connect with
|
181
|
+
# :adapter is the connection adapter to connect with. Defaults to Mechanize
|
182
|
+
#
|
171
183
|
# class Ticket < Roart::Ticket
|
172
184
|
# connection :server => 'server', :user => 'user', :pass => 'pass'
|
173
185
|
# end
|
174
186
|
#
|
175
187
|
def connection(options=nil)
|
176
188
|
if options
|
177
|
-
@connection = Roart::Connection.new(options)
|
189
|
+
@connection = Roart::Connection.new({:adapter => "mechanize"}.merge(options))
|
178
190
|
else
|
179
191
|
defined?(@connection) ? @connection : nil
|
180
192
|
end
|
181
193
|
end
|
182
|
-
|
183
|
-
#
|
194
|
+
|
195
|
+
# Sets the username and password used to connect to the RT server
|
196
|
+
# Required:
|
197
|
+
# :user sets the username to connect to RT
|
198
|
+
# :pass sets the password for the user to connect with
|
199
|
+
# This can be used to change a connection once the Ticket class has
|
200
|
+
# been initialized. Not required if you sepecify :user and :pass in
|
201
|
+
# the connection method
|
202
|
+
#
|
203
|
+
# class Ticket < Roart::Ticket
|
204
|
+
# connection :server => 'server'
|
205
|
+
# authenticate :user => 'user', :pass => 'pass'
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
def authenticate(options)
|
209
|
+
@connection.authenticate(options)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Adds a default queue to search each time. This is overridden by
|
184
213
|
# specifically including a :queue option in your find method. This can
|
185
214
|
# be an array of queue names or a string with a single queue name.
|
186
215
|
#
|
@@ -191,16 +220,16 @@ module Roart
|
|
191
220
|
defined?(@default_queue) ? @default_queue : nil
|
192
221
|
end
|
193
222
|
end
|
194
|
-
|
223
|
+
|
195
224
|
# creates a new ticket object and immediately saves it to the database.
|
196
225
|
def create(options)
|
197
226
|
ticket = self.new(options)
|
198
227
|
ticket.save
|
199
228
|
ticket
|
200
229
|
end
|
201
|
-
|
230
|
+
|
202
231
|
protected
|
203
|
-
|
232
|
+
|
204
233
|
def instantiate(attrs) #:nodoc:
|
205
234
|
object = nil
|
206
235
|
if attrs.is_a?(Array)
|
@@ -218,43 +247,45 @@ module Roart
|
|
218
247
|
object.send("add_methods!")
|
219
248
|
end
|
220
249
|
object.instance_variable_set("@history", false)
|
250
|
+
object.instance_variable_set("@new_record", false)
|
221
251
|
object
|
222
252
|
end
|
223
|
-
|
253
|
+
|
224
254
|
def find_initial(options={}) #:nodoc:
|
225
255
|
options.update(:limit => 1)
|
226
256
|
find_all(options).first
|
227
257
|
end
|
228
|
-
|
258
|
+
|
229
259
|
def find_all(options) #:nodoc:
|
230
260
|
uri = construct_search_uri(options)
|
231
261
|
tickets = get_tickets_from_search_uri(uri)
|
232
262
|
end
|
233
|
-
|
263
|
+
|
234
264
|
def find_by_ids(args, options) #:nodoc:
|
265
|
+
raise "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)
|
235
266
|
get_ticket_by_id(args.first)
|
236
267
|
end
|
237
|
-
|
268
|
+
|
238
269
|
def page_array(uri) #:nodoc:
|
239
270
|
page = self.connection.get(uri)
|
240
271
|
raise TicketSystemError, "Can't get ticket." unless page
|
241
|
-
page = page.split("\n")
|
272
|
+
page = page.split("\n")
|
242
273
|
status = page.delete_at(0)
|
243
|
-
if status.include?("200")
|
244
|
-
page.delete_if{|x| !x.include?(":")}
|
274
|
+
if status.include?("200")
|
275
|
+
page.delete_if{|x| !x.include?(":")}
|
245
276
|
page
|
246
277
|
else
|
247
278
|
raise TicketSystemInterfaceError, "Error Getting Ticket: #{status}"
|
248
279
|
end
|
249
280
|
end
|
250
|
-
|
281
|
+
|
251
282
|
def get_tickets_from_search_uri(uri) #:nodoc:
|
252
283
|
page = page_array(uri)
|
253
284
|
page.extend(Roart::TicketPage)
|
254
285
|
page = page.to_search_array
|
255
286
|
self.instantiate(page)
|
256
287
|
end
|
257
|
-
|
288
|
+
|
258
289
|
def get_ticket_from_uri(uri) #:nodoc:
|
259
290
|
page = page_array(uri)
|
260
291
|
page.extend(Roart::TicketPage)
|
@@ -263,20 +294,20 @@ module Roart
|
|
263
294
|
ticket.instance_variable_set("@full", true)
|
264
295
|
ticket
|
265
296
|
end
|
266
|
-
|
297
|
+
|
267
298
|
def get_ticket_by_id(id) #:nodoc:
|
268
299
|
uri = "#{self.connection.server}/REST/1.0/ticket/"
|
269
300
|
uri << id.to_s
|
270
301
|
get_ticket_from_uri(uri)
|
271
302
|
end
|
272
|
-
|
303
|
+
|
273
304
|
def construct_search_uri(options={}) #:nodoc:
|
274
305
|
uri = "#{self.connection.server}/REST/1.0/search/ticket?"
|
275
306
|
uri << 'orderby=-Created&' if options.delete(:order)
|
276
307
|
unless options.empty? && default_queue.nil?
|
277
308
|
uri << 'query= '
|
278
309
|
query = Array.new
|
279
|
-
|
310
|
+
|
280
311
|
if options[:queues] || options[:queue]
|
281
312
|
add_queue!(query, options[:queues] || options[:queue])
|
282
313
|
else
|
@@ -286,14 +317,14 @@ module Roart
|
|
286
317
|
add_searches!(query, options)
|
287
318
|
add_status!(query, options[:status])
|
288
319
|
add_custom_fields!(query, options[:custom_fields])
|
289
|
-
|
320
|
+
|
290
321
|
query << options[:conditions].to_s.chomp if options[:conditions]
|
291
|
-
|
322
|
+
|
292
323
|
uri << query.join(" AND ")
|
293
324
|
end
|
294
325
|
uri
|
295
326
|
end
|
296
|
-
|
327
|
+
|
297
328
|
def add_queue!(uri, queue) #:nodoc:
|
298
329
|
return false unless queue
|
299
330
|
if queue.is_a?(Array)
|
@@ -306,7 +337,7 @@ module Roart
|
|
306
337
|
uri << "Queue = '#{queue.to_s}'"
|
307
338
|
end
|
308
339
|
end
|
309
|
-
|
340
|
+
|
310
341
|
def add_custom_fields!(uri, options) #:nodoc:
|
311
342
|
return false unless options
|
312
343
|
options.each do |field, value|
|
@@ -321,7 +352,7 @@ module Roart
|
|
321
352
|
end
|
322
353
|
end
|
323
354
|
end
|
324
|
-
|
355
|
+
|
325
356
|
def add_status!(uri, options) #:nodoc:
|
326
357
|
return false unless options
|
327
358
|
parts = Array.new
|
@@ -336,9 +367,9 @@ module Roart
|
|
336
367
|
end
|
337
368
|
uri << parts
|
338
369
|
end
|
339
|
-
|
370
|
+
|
340
371
|
def add_searches!(uri, options) #:nodoc:
|
341
|
-
|
372
|
+
search_fields = %w( subject content content_type file_name owner requestors cc admin_cc)
|
342
373
|
options.each do |key, value|
|
343
374
|
if search_fields.include?(key.to_s)
|
344
375
|
key = key.to_s.camelize
|
@@ -354,7 +385,7 @@ module Roart
|
|
354
385
|
end
|
355
386
|
end
|
356
387
|
end
|
357
|
-
|
388
|
+
|
358
389
|
def add_dates!(uri, options) #:nodoc:
|
359
390
|
date_field = %w( created started resolved told last_updated starts due updated )
|
360
391
|
options.each do |key, value|
|
@@ -373,9 +404,9 @@ module Roart
|
|
373
404
|
end
|
374
405
|
end
|
375
406
|
end
|
376
|
-
|
407
|
+
|
377
408
|
end
|
378
|
-
|
409
|
+
|
379
410
|
end
|
380
|
-
|
411
|
+
|
381
412
|
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
module Roart
|
2
|
+
|
3
|
+
class Errors
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(obj)
|
7
|
+
@base, @errors = obj, {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_to_base(msg)
|
11
|
+
add(:base, msg)
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(field, message)
|
15
|
+
@errors[field.to_sym] ||= []
|
16
|
+
@errors[field.to_sym] << message
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_base
|
20
|
+
on(:base)
|
21
|
+
end
|
22
|
+
|
23
|
+
def on(field)
|
24
|
+
errors = @errors[field.to_sym]
|
25
|
+
return nil if errors.nil?
|
26
|
+
errors
|
27
|
+
end
|
28
|
+
|
29
|
+
alias :[] :on
|
30
|
+
|
31
|
+
def each
|
32
|
+
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns true if no errors have been added.
|
36
|
+
def empty?
|
37
|
+
@errors.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Removes all errors that have been added.
|
41
|
+
def clear
|
42
|
+
@errors = {}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such.
|
46
|
+
def size
|
47
|
+
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
|
48
|
+
end
|
49
|
+
|
50
|
+
alias_method :count, :size
|
51
|
+
alias_method :length, :size
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
module Validations
|
56
|
+
|
57
|
+
def self.included(model)
|
58
|
+
model.extend ClassMethods
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
class Validators
|
63
|
+
|
64
|
+
def initialize
|
65
|
+
@validators = []
|
66
|
+
end
|
67
|
+
|
68
|
+
def add(validator)
|
69
|
+
@validators << validator
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate(obj)
|
73
|
+
@validators.each{|validator| validator.call(obj)}
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
module ClassMethods
|
79
|
+
|
80
|
+
ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :min, :maximum, :max ].freeze
|
81
|
+
ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=',
|
82
|
+
:equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
|
83
|
+
:odd => 'odd?', :even => 'even?' }.freeze
|
84
|
+
|
85
|
+
def validator
|
86
|
+
@validator ||= Validators.new
|
87
|
+
end
|
88
|
+
|
89
|
+
def validates_presence_of(*args)
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
def validates_format_of(*args)
|
94
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
95
|
+
args.each do |field|
|
96
|
+
validator_proc = lambda do |obj|
|
97
|
+
unless obj.send(field.to_sym).match(options[:format])
|
98
|
+
obj.errors.add(field.to_sym, "Wrong Format")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
self.validator.add(validator_proc)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def validates_length_of(*args)
|
106
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
107
|
+
# Ensure that one and only one range option is specified.
|
108
|
+
range_options = ALL_RANGE_OPTIONS & options.keys
|
109
|
+
case range_options.size
|
110
|
+
when 0
|
111
|
+
raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
|
112
|
+
when 1
|
113
|
+
# Valid number of options; do nothing.
|
114
|
+
else
|
115
|
+
raise ArgumentError, 'Too many range options specified. Choose only one.'
|
116
|
+
end
|
117
|
+
|
118
|
+
option = range_options.first
|
119
|
+
option_value = options[range_options.first]
|
120
|
+
key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option]
|
121
|
+
custom_message = options[:message] || options[key]
|
122
|
+
|
123
|
+
args.each do |field|
|
124
|
+
case option
|
125
|
+
when :within, :in
|
126
|
+
raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
|
127
|
+
validator_proc = lambda do |obj|
|
128
|
+
if obj.send(field.to_sym).length < option_value.begin
|
129
|
+
obj.errors.add(field.to_sym, "Must be more than #{option_value.begin} characters.")
|
130
|
+
end
|
131
|
+
if obj.send(field.to_sym).length > option_value.end
|
132
|
+
obj.errors.add(field.to_sym, "Must be less than #{option_value.end} characters")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
self.validator.add(validator_proc)
|
136
|
+
when :min, :minium
|
137
|
+
raise ArgumentError, ":#{option} must be an Integer" unless option_value.is_a?(Integer)
|
138
|
+
validator_proc = lambda do |obj|
|
139
|
+
if obj.send(field.to_sym).length < option_value
|
140
|
+
obj.errors.add(field.to_sym, "Must be more than #{option_value} characters.")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
self.validator.add(validator_proc)
|
144
|
+
when :max, :maxium
|
145
|
+
raise ArgumentError, ":#{option} must be an Integer" unless option_value.is_a?(Integer)
|
146
|
+
validator_proc = lambda do |obj|
|
147
|
+
if obj.send(field.to_sym).length > option_value
|
148
|
+
obj.errors.add(field.to_sym, "Must be less than #{option_value} characters.")
|
149
|
+
end
|
150
|
+
end
|
151
|
+
self.validator.add(validator_proc)
|
152
|
+
when :is
|
153
|
+
raise ArgumentError, ":#{option} must be an Integer" unless option_value.is_a?(Integer)
|
154
|
+
validator_proc = lambda do |obj|
|
155
|
+
puts "#{field} is #{option_value}"
|
156
|
+
unless obj.send(field.to_sym).length == option_value
|
157
|
+
obj.errors.add(field.to_sym, "Must be #{option_value} characters.")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
self.validator.add(validator_proc)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
alias_method :validates_size_of, :validates_length_of
|
166
|
+
|
167
|
+
def validates_numericality_of(*args)
|
168
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
169
|
+
numericality_options = ALL_NUMERICALITY_CHECKS & options.keys
|
170
|
+
|
171
|
+
case numericality_options
|
172
|
+
when 0
|
173
|
+
raise ArgumentError, "Options Unspecified. Specify an option to use."
|
174
|
+
when 1
|
175
|
+
#continue
|
176
|
+
else
|
177
|
+
raise ArgumentError, "Too many options specified"
|
178
|
+
end
|
179
|
+
|
180
|
+
option = numericality_options.first
|
181
|
+
option_value = options[numericality_options.first]
|
182
|
+
key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option]
|
183
|
+
custom_message = options[:message] || options[key]
|
184
|
+
|
185
|
+
args.each do |field|
|
186
|
+
numericality_options.each do |option|
|
187
|
+
case option
|
188
|
+
when :odd, :even
|
189
|
+
unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
|
190
|
+
record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
|
191
|
+
end
|
192
|
+
else
|
193
|
+
record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
def validator
|
203
|
+
self.class.validator
|
204
|
+
end
|
205
|
+
|
206
|
+
def valid?
|
207
|
+
validator.validate self
|
208
|
+
self.errors.size == 0
|
209
|
+
end
|
210
|
+
|
211
|
+
def invalid?
|
212
|
+
!valid?
|
213
|
+
end
|
214
|
+
|
215
|
+
def errors
|
216
|
+
@errors ||= Errors.new(self)
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
data/lib/roart.rb
CHANGED
data/roart.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{roart}
|
5
|
-
s.version = "0.1.
|
5
|
+
s.version = "0.1.5"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["PJ Davis"]
|
9
|
-
s.date = %q{
|
9
|
+
s.date = %q{2010-01-12}
|
10
10
|
s.description = %q{Interface for working with Request Tracker (RT) tickets inspired by ActiveRecord.}
|
11
11
|
s.email = %q{pj.davis@gmail.com}
|
12
12
|
s.extra_rdoc_files = ["History.txt", "README.rdoc", "spec/test_data/full_history.txt", "spec/test_data/single_history.txt"]
|
@@ -34,4 +34,4 @@ Gem::Specification.new do |s|
|
|
34
34
|
s.add_dependency(%q<mechanize>, [">= 0.9.0"])
|
35
35
|
s.add_dependency(%q<bones>, [">= 2.5.1"])
|
36
36
|
end
|
37
|
-
end
|
37
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
|
2
|
+
require File.join(File.dirname(__FILE__), %w[ .. spec_helper])
|
3
|
+
|
4
|
+
describe "ConnectionAdapter" do
|
5
|
+
|
6
|
+
it 'should give us back a connection' do
|
7
|
+
Roart::ConnectionAdapters::Mechanize.should_receive(:new).with(:adapter => 'mechanize').and_return(mock('mechanize'))
|
8
|
+
Roart::ConnectionAdapter.new(:adapter => 'mechanize')
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
@@ -2,6 +2,49 @@
|
|
2
2
|
require File.join(File.dirname(__FILE__), %w[ .. spec_helper])
|
3
3
|
|
4
4
|
describe "Connection" do
|
5
|
+
|
6
|
+
describe 'get and post' do
|
7
|
+
|
8
|
+
before do
|
9
|
+
@agent = mock("agent", :null_object => true)
|
10
|
+
@options = {:server => 'server', :user => 'user', :pass => 'pass', :adapter => 'whatev'}
|
11
|
+
Roart::ConnectionAdapter.should_receive(:new).and_return(@agent)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should respond to get' do
|
15
|
+
@agent.should_receive(:get).with('some_uri').and_return('body')
|
16
|
+
connection = Roart::Connection.new(@options)
|
17
|
+
connection.get("some_uri").should == 'body'
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should respond to post' do
|
21
|
+
@agent.should_receive(:post).with('some_uri', 'a payload').and_return('body')
|
22
|
+
connection = Roart::Connection.new(@options)
|
23
|
+
connection.post("some_uri", "a payload").should == 'body'
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should raise an exception if it doesnt have all the options' do
|
29
|
+
lambda{Roart::Connection.new(:user => 'bad')}.should raise_error
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should give us the rest path' do
|
33
|
+
@agent = mock("agent", :null_object => true)
|
34
|
+
@options = {:server => 'server', :user => 'user', :pass => 'pass', :adapter => 'whatev'}
|
35
|
+
Roart::ConnectionAdapter.should_receive(:new).and_return(@agent)
|
36
|
+
connection = Roart::Connection.new(@options)
|
37
|
+
connection.rest_path.should == 'server/REST/1.0/'
|
38
|
+
end
|
5
39
|
|
40
|
+
it 'should give us back the whole thing' do
|
41
|
+
mock_mech = mock('mech')
|
42
|
+
@options = {:server => 'server', :user => 'user', :pass => 'pass', :adapter => 'mechanize'}
|
43
|
+
Roart::ConnectionAdapters::Mechanize.should_receive(:new).with(@options).and_return(mock_mech)
|
44
|
+
mock_mech.should_receive(:login).with(@options)
|
45
|
+
mock_mech.should_receive(:get).with('uri').and_return('body')
|
46
|
+
connection = Roart::Connection.new(@options)
|
47
|
+
connection.get('uri').should == 'body'
|
48
|
+
end
|
6
49
|
|
7
50
|
end
|
data/spec/roart/ticket_spec.rb
CHANGED
@@ -3,8 +3,8 @@ require File.join(File.dirname(__FILE__), %w[ .. spec_helper])
|
|
3
3
|
describe "Ticket" do
|
4
4
|
|
5
5
|
it "should have a connection" do
|
6
|
-
Roart::Connection.should_receive(:new).with('some options').and_return(true)
|
7
|
-
Roart::Ticket.connection('some options').should == true
|
6
|
+
Roart::Connection.should_receive(:new).with(:some => 'some options', :adapter => "www").and_return(true)
|
7
|
+
Roart::Ticket.connection(:some => 'some options', :adapter => "www").should == true
|
8
8
|
end
|
9
9
|
|
10
10
|
it "should find the first ticket" do
|
@@ -341,7 +341,9 @@ describe "Ticket" do
|
|
341
341
|
|
342
342
|
it 'should be able to save a new ticket' do
|
343
343
|
ticket = Roart::Ticket.new(@payload)
|
344
|
+
ticket.new_record?.should be_true
|
344
345
|
ticket.save
|
346
|
+
ticket.new_record?.should be_false
|
345
347
|
end
|
346
348
|
|
347
349
|
it 'should be able to create a ticket' do
|
@@ -360,8 +362,9 @@ describe "Ticket" do
|
|
360
362
|
describe 'updating tickets' do
|
361
363
|
|
362
364
|
before do
|
363
|
-
@
|
364
|
-
@post_data
|
365
|
+
@payload = {:queue => 'My Queue', :subject => 'An Old Ticket'}
|
366
|
+
@post_data = {:queue => 'My Queue', :subject => 'An Old Ticket'}
|
367
|
+
@payload[:subject] = 'A New Ticket'
|
365
368
|
@post_data = to_content_format(@post_data)
|
366
369
|
@mock_connection = mock('connection')
|
367
370
|
@mock_connection.should_receive(:server).and_return('uri')
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: roart
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- PJ Davis
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2010-01-19 00:00:00 -06:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -50,6 +50,8 @@ files:
|
|
50
50
|
- lib/roart.rb
|
51
51
|
- lib/roart/callbacks.rb
|
52
52
|
- lib/roart/connection.rb
|
53
|
+
- lib/roart/connection_adapter.rb
|
54
|
+
- lib/roart/connection_adapters/mechanize_adapter.rb
|
53
55
|
- lib/roart/core/array.rb
|
54
56
|
- lib/roart/core/hash.rb
|
55
57
|
- lib/roart/core/string.rb
|
@@ -58,8 +60,10 @@ files:
|
|
58
60
|
- lib/roart/roart.rb
|
59
61
|
- lib/roart/ticket.rb
|
60
62
|
- lib/roart/ticket_page.rb
|
63
|
+
- lib/roart/validations.rb
|
61
64
|
- roart.gemspec
|
62
65
|
- spec/roart/callbacks_spec.rb
|
66
|
+
- spec/roart/connection_adapter_spec.rb
|
63
67
|
- spec/roart/connection_spec.rb
|
64
68
|
- spec/roart/core/array_spec.rb
|
65
69
|
- spec/roart/core/hash_spec.rb
|