forked from austinmoody/ruby-fogbugz-api
/
fogbugz-api.rb
376 lines (339 loc) · 13.9 KB
/
fogbugz-api.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
require 'rubygems' rescue nil
require 'hpricot'
require 'net/https'
require 'cgi'
class FogBugzError < StandardError; end
# FogBugz class
class FogBugz
# Version of the FogBuz API this was written for. If the minversion returned
# by FogBugz is greater than this value this library will not function.
# TODO
# 1. If API mismatch... destroy Object?
API_VERSION = 5
# This is an array of all possible values that can be returned on a case.
# For methods that ask for cols wanted for a case this array will be used if
# their is nothing else specified.
CASE_COLUMNS = %w(ixBug fOpen sTitle sLatestTextSummary ixBugEventLatestText
ixProject sProject ixArea sArea ixGroup ixPersonAssignedTo sPersonAssignedTo
sEmailAssignedTo ixPersonOpenedBy ixPersonResolvedBy ixPersonClosedBy
ixPersonLastEditedBy ixStatus sStatus ixPriority sPriority ixFixFor sFixFor
dtFixFor sVersion sComputer hrsOrigEst hrsCurrEst hrsElapsed c sCustomerEmail
ixMailbox ixCategory sCategory dtOpened dtResolved dtClosed ixBugEventLatest
dtLastUpdated fReplied fForwarded sTicket ixDiscussTopic dtDue sReleaseNotes
ixBugEventLastView dtLastView ixRelatedBugs sScoutDescription sScoutMessage
fScoutStopReporting fSubscribed events)
attr_reader :url, :token, :use_ssl, :api_version, :api_minversion, :api_url
# Creates an instance of the FogBugz class.
#
# * url: URL to your FogBugz installation. URL only as in my.fogbugz.com
# without the http or https.
# * use_ssl: Does this server use SSL? true/false
# * token: Already have a token for the server? You can provide that here.
#
# Connects to the specified FogBugz installation and grabs the api.xml file
# to get other information such as API version, API minimum version, and the
# API endpoint. Also sets http/https connection to the server and sets the
# token if provided. FogBugzError will be raise if the minimum API version
# returned by FogBugz is greater than API_VERSION of this class.
#
# Example Usage:
#
# fb = FogBugz.new("my.fogbugz.com",true)
#
def initialize(url,use_ssl=false,token=nil)
@url = url
@use_ssl = use_ssl
connect
# Attempt to grap api.xml file from the server specified by url. Will let
# us know API is functional and verion matches this class
result = Hpricot.XML(@connection.get("/api.xml").body)
@api_version = (result/"version").inner_html.to_i
@api_minversion = (result/"version").inner_html.to_i
@api_url = "/" + (result/"url").inner_html
# Make sure this class will work w/ API version
raise FogBugzError, "API version mismatch" if (API_VERSION < @api_minversion)
@token = token ? token : ""
end
# Validates a user with FogBugz. Saves the returned token for use with other
# commands.
#
# If a token was already specified with new it will be overwritten with the
# token picked up by a successful authentication.
def logon(email,password)
cmd = {"cmd" => "logon", "email" => email, "password" => password}
result = Hpricot.XML(@connection.post(@api_url, to_params(cmd)).body)
if (result/"error").length >= 1
# error code 1 = bad login
# error code 2 = ambiguous name
case (result/"error").first["code"]
when "1"
raise FogBugzError, (result/"error").inner_html
when "2"
ambiguous_users = []
(result/"person").each do |person|
ambiguous_users << CDATA_REGEXP.match(person.inner_html)[1]
end
raise FogBugzError, (result/"error").inner_html + " " + ambiguous_users.join(", ")
end # case
elsif (result/"token").length == 1
# successful login
@token = CDATA_REGEXP.match((result/"token").inner_html)[1]
end
end
def logoff
cmd = {"cmd" => "logoff", "token" => @token}
result = Hpricot.XML(@connection.post(@api_url, to_params(cmd)).body)
@token = ""
end
def filters
cmd = {"cmd" => "listFilters", "token" => @token}
result = Hpricot.XML(@connection.post(@api_url, to_params(cmd)).body)
return_value = Hash.new
(result/"filter").each do |filter|
# create hash for each new project
filter_name = filter.inner_html
return_value[filter_name] = Hash.new
return_value[filter_name]["name"] = filter_name
return_value[filter_name] = filter.attributes.merge(return_value[filter_name])
end
return_value
end
def projects(fWrite=false, ixProject=nil)
return_value = Hash.new
cmd = {"cmd" => "listProjects", "token" => @token}
{"fWrite"=>"1"}.merge(cmd) if fWrite
{"ixProject"=>ixProject}.merge(cmd) if ixProject
result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
return list_process(result,"project","sProject")
end
# Returns an integer, which is ixProject for the project created
# TODO - change to accept Has of parameters?
def new_project(sProject, ixPersonPrimaryContact, fAllowPublicSubmit, ixGroup, fInbox)
# I would have thought that the fAllowPublicSubmit would have been
# true/false... instead seems to need to be 0 or 1.
cmd = {
"cmd" => "newProject",
"token" => @token,
"sProject" => sProject,
"ixPersonPrimaryContact" => ixPersonPrimaryContact.to_s,
"fAllowPublicSubmit" => fAllowPublicSubmit.to_s,
"ixGroup" => ixGroup.to_s,
"fInbox" => fInbox.to_s
}
result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
return (result/"ixProject").inner_html.to_i
end
def areas(fWrite=false, ixProject=nil, ixArea=nil)
return_value = Hash.new
cmd = {"cmd" => "listAreas", "token" => @token}
cmd = {"fWrite"=>"1"}.merge(cmd) if fWrite
cmd = {"ixProject"=>ixProject}.merge(cmd) if ixProject
cmd = {"ixArea" => ixArea}.merge(cmd) if ixArea
result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
return list_process(result,"area","sArea")
end
def fix_fors(ixProject=nil,ixFixFor=nil)
return_value = Hash.new
cmd = {"cmd" => "listFixFors", "token" => @token}
{"ixProject"=>ixProject}.merge(cmd) if ixProject
{"ixFixFor" => ixFixFor}.merge(cmd) if ixFixFor
result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
return list_process(result,"fixfor","sFixFor")
end
def categories
cmd = {"cmd" => "listCategories", "token" => @token}
result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
return list_process(result,"category","sCategory")
end
def priorities
cmd = {"cmd" => "listPriorities", "token" => @token}
result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
return list_process(result,"priority","sPriority")
end
# Returns list of people in corresponding categories.
#
# * fIncludeNormal: Only include Normal users. If no options specified,
# fIncludeNormal=1 is assumed.
# * fIncludeCommunity: true/false Will include Community users in return.
# * fIncludeVirtual: true/false Will include Virtual users in return.
def people(fIncludeNormal="1", fIncludeCommunity=nil, fIncludeVirtual=nil)
cmd = {
"cmd" => "listPeople",
"token" => @token,
"fIncludeNormal" => fIncludeNormal
}
cmd = {"fIncludeCommunity" => "1"}.merge(cmd) if fIncludeCommunity
cmd = {"fIncludeVirtual" => "1"}.merge(cmd) if fIncludeVirtual
result = Hpricot.XML(@connection.post(@api_url, to_params(cmd)).body)
return list_process(result,"person","sFullName")
end
# Returns a list of statuses for a particular category.
#
# * ixCategory => category to return statuses for. If not specified, then all are returned.
# * fResolved => If = 1 then only resolved statuses are returned.
def statuses(ixCategory=nil,fResolved=nil)
cmd = {
"cmd" => "listStatuses",
"token" => @token
}
cmd = {"ixCategory"=>ixCategory}.merge(cmd) if ixCategory
cmd = {"fResolved"=>fResolved}.merge(cmd) if fResolved
result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
return list_process(result,"status","sStatus")
end
# Returns a list of mailboxes that you have access to.
def mailboxes
cmd = {
"cmd" => "listMailboxes",
"token" => @token
}
result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
# usually lists were keyed w/ a name field. Mailboxes just
# weren't working for me so I'm going with ixMailbox value
return list_process(result,"mailbox","ixMailbox")
end
# Searches for FogBugz cases
#
# * q: Query for searching. Should hopefully work just like the Search box
# within the FogBugz application.
# * cols: Columns of information to be returned for each case found. Consult
# FogBugz API documentation for a list. If this is not specified the
# CASE_COLUMNS will be used. This will request every possible datapoint (as
# of API version 5) for each case.
# * max: Maximum number of cases to be returned for your search. Will return
# all if not specified.
def search(q, cols=CASE_COLUMNS, max=nil)
# TODO - shoudl I worry about the "operations" returned
# in the <case>?
cmd = {
"cmd" => "search",
"token" => @token,
"q" => q,
# ixBug is the key for the hash returned so I'm adding it to the cols array just in case
"cols" => (cols + ["ixBug"]).join(",")
}
cmd = {"max" => max}.merge(cmd) if max
result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
return_value = list_process(result,"case","ixBug")
# if one of the returned cols = events, then process
# this list and replace its spot in the Hash
# with this instead of a string of XML
return_value.each do |key,value|
return_value[key]["events"] = list_process(Hpricot.XML(return_value[key]["events"]),"event","ixBugEvent") if return_value[key].has_key?("events")
end
return_value
end
# Creates a FogBugz case.
#
# * params: must be a hash keyed with values from the FogBugz API docs.
# sTitle, ixProject (or sProject), etc...
# * cols: columns to be returned about the case which gets created. Ff not
# passed will use constant list (all) provided with Class
def new_case(params, cols=CASE_COLUMNS)
case_process("new",params,cols)
end
protected
CDATA_REGEXP = /<!\[CDATA\[(.*?)\]\]>/
# Makes connection to the FogBugz server
#
# Assumes port 443 for SSL connections and 80 for non-SSL connections.
# Possibly should provide a way to override this.
def connect
@connection = Net::HTTP.new(@url, @use_ssl ? 443 : 80)
@connection.use_ssl = @use_ssl
@connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
end
def case_process(cmd,params,cols)
cmd = {
"cmd" => cmd,
"token" => @token,
"cols" => cols.join(",")
}.merge(params)
result = Hpricot.XML(@connection.post(@api_url, to_params(cmd)).body)
return_value = list_process(result,"case","ixBug")
# if one of the returned cols = events, then process
# this list and replace its spot in the Hash
# with this instead of a string of XML
return_value.each do |key,value|
return_value[key]["events"] = list_process(Hpricot.XML(return_value[key]["events"]),"event","ixBugEvent") if return_value[key].has_key?("events")
end
return_value[return_value.keys[0]]
end
# method used by other list methods to process the XML returned by FogBugz API.
#
# * xml => XML to process
# * element => individual elements within the XML to create Hashes for within the returned value
# * element_name => key for each individual Hash within the return value.
#
# EXAMPLE XML
#<response>
# <categories>
# <category>
# <ixCategory>1</ixCategory>
# <sCategory><![CDATA[Bug]]></sCategory>
# <sPlural><![CDATA[Bugs]]></sPlural>
# <ixStatusDefault>2</ixStatusDefault>
# <fIsScheduleItem>false</fIsScheduleItem>
# </category>
# <category>
# <ixCategory>2</ixCategory>
# <sCategory><![CDATA[Feature]]></sCategory>
# <sPlural><![CDATA[Features]]></sPlural>
# <ixStatusDefault>8</ixStatusDefault>
# <fIsScheduleItem>false</fIsScheduleItem>
# </category>
# </categories>
#</response>
#
# EXAMPLE CAll
# list_process(xml, "category", "sCategory")
#
# EXAMPLE HASH RETURN
#
# {
# "Bug" => {
# "ixCategory" => 1,
# "sCategory" => "Bug",
# "sPlural" => "Bugs",
# "ixStatusDefault" => 2,
# "fIsScheduleItem" => false
# },
# "Feature" => {
# "ixCategory" => 2,
# "sCategory" => "Feature",
# "sPlural" => "Features",
# "ixStatusDefault" => 2,
# "fIsScheduleItem" => false
# }
# }
def list_process(xml, element, element_name)
return_value = Hash.new
(xml/"#{element}").each do |item|
if element_name[0,1] == "s"
item_name = CDATA_REGEXP.match((item/"#{element_name}").inner_html)[1]
else
item_name = (item/"#{element_name}").inner_html
end
return_value[item_name] = Hash.new
item.each_child do |attribute|
return_value[item_name][attribute.name] = attribute.inner_html
# convert values to proper types
return_value[item_name][attribute.name] = CDATA_REGEXP.match(attribute.inner_html)[1] if (attribute.name[0,1] == "s" or attribute.name[0,3] == "evt") and attribute.inner_html != "" and CDATA_REGEXP.match(attribute.inner_html) != nil
return_value[item_name][attribute.name] = return_value[item_name][attribute.name].to_i if (attribute.name[0,2] == "ix" or attribute.name[0,1] == "n")
return_value[item_name][attribute.name] = (return_value[item_name][attribute.name] == "true") ? true : false if attribute.name[0,1] == "f"
end
end
return_value
end
# Converts a hash such as
# {
# :cmd => "logon",
# :email => "austin.moody@gmail.com",
# :password => "yeahwhatever",
# }
# to
# "cmd=logon&email=austin.moody@gmail.com&password=yeahwhatever"
def to_params(hash)
hash.map{|key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&')
end
end