austinmoody-fogbugz-api 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/LICENSE +22 -0
  2. data/README.rdoc +28 -0
  3. data/TODO +4 -0
  4. data/fogbugz-api.rb +1 -0
  5. data/lib/fogbugz-api.rb +375 -0
  6. metadata +71 -0
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2008 Austin Moody
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,28 @@
1
+ = Ruby FogBugz API Wrapper
2
+
3
+ My attempt at creating a wrapper for the FogBugz API in Ruby.
4
+
5
+ More about FogBugz: http://www.fogbugz.com
6
+ More about FogBugz API: http://www.fogcreek.com/FogBugz/docs/60/topics/advanced/API.html
7
+
8
+ This is very much a work in progress. Any questions, concerns, or if you want to help out please e-mail me: austin.moody@gmail.com
9
+
10
+ == Installation
11
+
12
+ GEM coming soon hopefully. For now nab the fogbugz-api.rb file and require in your Ruby script.
13
+
14
+ == Requirements
15
+
16
+ * Hpricot (http://code.whytheluckystiff.net/hpricot/)
17
+ * net/https
18
+ * A login to a FogBugz server
19
+
20
+ == Example Usage
21
+
22
+ fb = FogBugz.new("my.fogbugz.com",true)
23
+
24
+ fb.logon("me@email.com","mypassword")
25
+
26
+ case_search = fb.search("API errors")
27
+
28
+ fb.logoff
data/TODO ADDED
@@ -0,0 +1,4 @@
1
+ == FogBugz API Todo
2
+
3
+ * Finish it! At the moment it is very incomplete. Pretty much just the list functions, search, and newCase.
4
+ * Create tests and make sure they work.
data/fogbugz-api.rb ADDED
@@ -0,0 +1 @@
1
+ require "lib/fogbugz-api"
@@ -0,0 +1,375 @@
1
+ require 'rubygems' rescue nil
2
+ require 'hpricot'
3
+ require 'net/https'
4
+ require 'cgi'
5
+
6
+ class FogBugzError < StandardError; end
7
+
8
+ # FogBugz class
9
+ class FogBugz
10
+
11
+ # Version of the FogBuz API this was written for. If the minversion returned
12
+ # by FogBugz is greater than this value this library will not function.
13
+ # TODO
14
+ # 1. If API mismatch... destroy Object?
15
+ API_VERSION = 5
16
+
17
+ # This is an array of all possible values that can be returned on a case.
18
+ # For methods that ask for cols wanted for a case this array will be used if
19
+ # their is nothing else specified.
20
+ CASE_COLUMNS = %w(ixBug fOpen sTitle sLatestTextSummary ixBugEventLatestText
21
+ ixProject sProject ixArea sArea ixGroup ixPersonAssignedTo sPersonAssignedTo
22
+ sEmailAssignedTo ixPersonOpenedBy ixPersonResolvedBy ixPersonClosedBy
23
+ ixPersonLastEditedBy ixStatus sStatus ixPriority sPriority ixFixFor sFixFor
24
+ dtFixFor sVersion sComputer hrsOrigEst hrsCurrEst hrsElapsed c sCustomerEmail
25
+ ixMailbox ixCategory sCategory dtOpened dtResolved dtClosed ixBugEventLatest
26
+ dtLastUpdated fReplied fForwarded sTicket ixDiscussTopic dtDue sReleaseNotes
27
+ ixBugEventLastView dtLastView ixRelatedBugs sScoutDescription sScoutMessage
28
+ fScoutStopReporting fSubscribed events)
29
+
30
+ attr_reader :url, :token, :use_ssl, :api_version, :api_minversion, :api_url
31
+
32
+ # Creates an instance of the FogBugz class.
33
+ #
34
+ # * url: URL to your FogBugz installation. URL only as in my.fogbugz.com
35
+ # without the http or https.
36
+ # * use_ssl: Does this server use SSL? true/false
37
+ # * token: Already have a token for the server? You can provide that here.
38
+ #
39
+ # Connects to the specified FogBugz installation and grabs the api.xml file
40
+ # to get other information such as API version, API minimum version, and the
41
+ # API endpoint. Also sets http/https connection to the server and sets the
42
+ # token if provided. FogBugzError will be raise if the minimum API version
43
+ # returned by FogBugz is greater than API_VERSION of this class.
44
+ #
45
+ # Example Usage:
46
+ #
47
+ # fb = FogBugz.new("my.fogbugz.com",true)
48
+ #
49
+ def initialize(url,use_ssl=false,token=nil)
50
+ @url = url
51
+ @use_ssl = use_ssl
52
+ connect
53
+
54
+ # Attempt to grap api.xml file from the server specified by url. Will let
55
+ # us know API is functional and verion matches this class
56
+ result = Hpricot.XML(@connection.get("/api.xml").body)
57
+
58
+ @api_version = (result/"version").inner_html.to_i
59
+ @api_minversion = (result/"version").inner_html.to_i
60
+ @api_url = "/" + (result/"url").inner_html
61
+
62
+ # Make sure this class will work w/ API version
63
+ raise FogBugzError, "API version mismatch" if (API_VERSION < @api_minversion)
64
+
65
+ @token = token ? token : ""
66
+ end
67
+
68
+ # Validates a user with FogBugz. Saves the returned token for use with other
69
+ # commands.
70
+ #
71
+ # If a token was already specified with new it will be overwritten with the
72
+ # token picked up by a successful authentication.
73
+ def logon(email,password)
74
+ cmd = {"cmd" => "logon", "email" => email, "password" => password}
75
+
76
+ result = Hpricot.XML(@connection.post(@api_url, to_params(cmd)).body)
77
+
78
+ if (result/"error").length >= 1
79
+ # error code 1 = bad login
80
+ # error code 2 = ambiguous name
81
+ case (result/"error").first["code"]
82
+ when "1"
83
+ raise FogBugzError, (result/"error").inner_html
84
+ when "2"
85
+ ambiguous_users = []
86
+ (result/"person").each do |person|
87
+ ambiguous_users << CDATA_REGEXP.match(person.inner_html)[1]
88
+ end
89
+ raise FogBugzError, (result/"error").inner_html + " " + ambiguous_users.join(", ")
90
+ end # case
91
+ elsif (result/"token").length == 1
92
+ # successful login
93
+ @token = CDATA_REGEXP.match((result/"token").inner_html)[1]
94
+ end
95
+ end
96
+
97
+ def logoff
98
+ cmd = {"cmd" => "logoff", "token" => @token}
99
+ result = Hpricot.XML(@connection.post(@api_url, to_params(cmd)).body)
100
+ @token = ""
101
+ end
102
+
103
+ def filters
104
+ cmd = {"cmd" => "listFilters", "token" => @token}
105
+ result = Hpricot.XML(@connection.post(@api_url, to_params(cmd)).body)
106
+ return_value = Hash.new
107
+ (result/"filter").each do |filter|
108
+ # create hash for each new project
109
+ filter_name = filter.inner_html
110
+ return_value[filter_name] = Hash.new
111
+ return_value[filter_name]["name"] = filter_name
112
+ return_value[filter_name] = filter.attributes.merge(return_value[filter_name])
113
+ end
114
+ return_value
115
+ end
116
+
117
+ def projects(fWrite=false, ixProject=nil)
118
+ return_value = Hash.new
119
+ cmd = {"cmd" => "listProjects", "token" => @token}
120
+ {"fWrite"=>"1"}.merge(cmd) if fWrite
121
+ {"ixProject"=>ixProject}.merge(cmd) if ixProject
122
+ result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
123
+ return list_process(result,"project","sProject")
124
+ end
125
+
126
+ # Returns an integer, which is ixProject for the project created
127
+ # TODO - change to accept Has of parameters?
128
+ def new_project(sProject, ixPersonPrimaryContact, fAllowPublicSubmit, ixGroup, fInbox)
129
+
130
+ # I would have thought that the fAllowPublicSubmit would have been
131
+ # true/false... instead seems to need to be 0 or 1.
132
+ cmd = {
133
+ "cmd" => "newProject",
134
+ "token" => @token,
135
+ "sProject" => sProject,
136
+ "ixPersonPrimaryContact" => ixPersonPrimaryContact.to_s,
137
+ "fAllowPublicSubmit" => fAllowPublicSubmit.to_s,
138
+ "ixGroup" => ixGroup.to_s,
139
+ "fInbox" => fInbox.to_s
140
+ }
141
+ result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
142
+ return (result/"ixProject").inner_html.to_i
143
+ end
144
+
145
+ def areas(fWrite=false, ixProject=nil, ixArea=nil)
146
+ return_value = Hash.new
147
+ cmd = {"cmd" => "listAreas", "token" => @token}
148
+ cmd = {"fWrite"=>"1"}.merge(cmd) if fWrite
149
+ cmd = {"ixProject"=>ixProject}.merge(cmd) if ixProject
150
+ cmd = {"ixArea" => ixArea}.merge(cmd) if ixArea
151
+ result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
152
+ return list_process(result,"area","sArea")
153
+ end
154
+
155
+ def fix_fors(ixProject=nil,ixFixFor=nil)
156
+ return_value = Hash.new
157
+ cmd = {"cmd" => "listFixFors", "token" => @token}
158
+ {"ixProject"=>ixProject}.merge(cmd) if ixProject
159
+ {"ixFixFor" => ixFixFor}.merge(cmd) if ixFixFor
160
+ result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
161
+ return list_process(result,"fixfor","sFixFor")
162
+ end
163
+
164
+ def categories
165
+ cmd = {"cmd" => "listCategories", "token" => @token}
166
+ result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
167
+ return list_process(result,"category","sCategory")
168
+ end
169
+
170
+ def listPriorities
171
+ cmd = {"cmd" => "listPriorities", "token" => @token}
172
+ result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
173
+ return list_process(result,"priority","sPriority")
174
+ end
175
+
176
+ # Returns list of people in corresponding categories.
177
+ #
178
+ # * fIncludeNormal: Only include Normal users. If no options specified,
179
+ # fIncludeNormal=1 is assumed.
180
+ # * fIncludeCommunity: true/false Will include Community users in return.
181
+ # * fIncludeVirtual: true/false Will include Virtual users in return.
182
+ def people(fIncludeNormal="1", fIncludeCommunity=nil, fIncludeVirtual=nil)
183
+ cmd = {
184
+ "cmd" => "listPeople",
185
+ "token" => @token,
186
+ "fIncludeNormal" => fIncludeNormal
187
+ }
188
+ cmd = {"fIncludeCommunity" => "1"}.merge(cmd) if fIncludeCommunity
189
+ cmd = {"fIncludeVirtual" => "1"}.merge(cmd) if fIncludeVirtual
190
+ result = Hpricot.XML(@connection.post(@api_url, to_params(cmd)).body)
191
+ return list_process(result,"person","sFullName")
192
+ end
193
+
194
+ # Returns a list of statuses for a particular category.
195
+ #
196
+ # * ixCategory => category to return statuses for. If not specified, then all are returned.
197
+ # * fResolved => If = 1 then only resolved statuses are returned.
198
+ def statuses(ixCategory=nil,fResolved=nil)
199
+ cmd = {
200
+ "cmd" => "listStatuses",
201
+ "token" => @token
202
+ }
203
+ cmd = {"ixCategory"=>ixCategory}.merge(cmd) if ixCategory
204
+ cmd = {"fResolved"=>fResolved}.merge(cmd) if fResolved
205
+ result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
206
+ return list_process(result,"status","sStatus")
207
+ end
208
+
209
+ # Returns a list of mailboxes that you have access to.
210
+ def mailboxes
211
+ cmd = {
212
+ "cmd" => "listMailboxes",
213
+ "token" => @token
214
+ }
215
+ result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
216
+ # usually lists were keyed w/ a name field. Mailboxes just
217
+ # weren't working for me so I'm going with ixMailbox value
218
+ return list_process(result,"mailbox","ixMailbox")
219
+ end
220
+
221
+ # Searches for FogBugz cases
222
+ #
223
+ # * q: Query for searching. Should hopefully work just like the Search box
224
+ # within the FogBugz application.
225
+ # * cols: Columns of information to be returned for each case found. Consult
226
+ # FogBugz API documentation for a list. If this is not specified the
227
+ # CASE_COLUMNS will be used. This will request every possible datapoint (as
228
+ # of API version 5) for each case.
229
+ # * max: Maximum number of cases to be returned for your search. Will return
230
+ # all if not specified.
231
+ def search(q, cols=CASE_COLUMNS, max=nil)
232
+ # TODO - shoudl I worry about the "operations" returned
233
+ # in the <case>?
234
+
235
+ cmd = {
236
+ "cmd" => "search",
237
+ "token" => @token,
238
+ "q" => q,
239
+ "cols" => cols.join(",")
240
+ }
241
+ cmd = {"max" => max}.merge(cmd) if max
242
+ result = Hpricot.XML(@connection.post(@api_url,to_params(cmd)).body)
243
+ return_value = list_process(result,"case","ixBug")
244
+ # if one of the returned cols = events, then process
245
+ # this list and replace its spot in the Hash
246
+ # with this instead of a string of XML
247
+ return_value.each do |key,value|
248
+ return_value[key]["events"] = list_process(Hpricot.XML(return_value[key]["events"]),"event","ixBugEvent") if return_value[key].has_key?("events")
249
+ end
250
+ return_value
251
+ end
252
+
253
+ # Creates a FogBugz case.
254
+ #
255
+ # * params: must be a hash keyed with values from the FogBugz API docs.
256
+ # sTitle, ixProject (or sProject), etc...
257
+ # * cols: columns to be returned about the case which gets created. Ff not
258
+ # passed will use constant list (all) provided with Class
259
+ def new_case(params, cols=CASE_COLUMNS)
260
+ case_process("new",params,cols)
261
+ end
262
+
263
+ protected
264
+
265
+ CDATA_REGEXP = /<!\[CDATA\[(.*?)\]\]>/
266
+
267
+ # Makes connection to the FogBugz server
268
+ #
269
+ # Assumes port 443 for SSL connections and 80 for non-SSL connections.
270
+ # Possibly should provide a way to override this.
271
+ def connect
272
+ @connection = Net::HTTP.new(@url, @use_ssl ? 443 : 80)
273
+ @connection.use_ssl = @use_ssl
274
+ @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
275
+ end
276
+
277
+ def case_process(cmd,params,cols)
278
+ cmd = {
279
+ "cmd" => cmd,
280
+ "token" => @token,
281
+ "cols" => cols.join(",")
282
+ }.merge(params)
283
+ result = Hpricot.XML(@connection.post(@api_url, to_params(cmd)).body)
284
+ return_value = list_process(result,"case","ixBug")
285
+ # if one of the returned cols = events, then process
286
+ # this list and replace its spot in the Hash
287
+ # with this instead of a string of XML
288
+ return_value.each do |key,value|
289
+ return_value[key]["events"] = list_process(Hpricot.XML(return_value[key]["events"]),"event","ixBugEvent") if return_value[key].has_key?("events")
290
+ end
291
+ return_value[return_value.keys[0]]
292
+ end
293
+
294
+ # method used by other list methods to process the XML returned by FogBugz API.
295
+ #
296
+ # * xml => XML to process
297
+ # * element => individual elements within the XML to create Hashes for within the returned value
298
+ # * element_name => key for each individual Hash within the return value.
299
+ #
300
+ # EXAMPLE XML
301
+ #<response>
302
+ # <categories>
303
+ # <category>
304
+ # <ixCategory>1</ixCategory>
305
+ # <sCategory><![CDATA[Bug]]></sCategory>
306
+ # <sPlural><![CDATA[Bugs]]></sPlural>
307
+ # <ixStatusDefault>2</ixStatusDefault>
308
+ # <fIsScheduleItem>false</fIsScheduleItem>
309
+ # </category>
310
+ # <category>
311
+ # <ixCategory>2</ixCategory>
312
+ # <sCategory><![CDATA[Feature]]></sCategory>
313
+ # <sPlural><![CDATA[Features]]></sPlural>
314
+ # <ixStatusDefault>8</ixStatusDefault>
315
+ # <fIsScheduleItem>false</fIsScheduleItem>
316
+ # </category>
317
+ # </categories>
318
+ #</response>
319
+ #
320
+ # EXAMPLE CAll
321
+ # list_process(xml, "category", "sCategory")
322
+ #
323
+ # EXAMPLE HASH RETURN
324
+ #
325
+ # {
326
+ # "Bug" => {
327
+ # "ixCategory" => 1,
328
+ # "sCategory" => "Bug",
329
+ # "sPlural" => "Bugs",
330
+ # "ixStatusDefault" => 2,
331
+ # "fIsScheduleItem" => false
332
+ # },
333
+ # "Feature" => {
334
+ # "ixCategory" => 2,
335
+ # "sCategory" => "Feature",
336
+ # "sPlural" => "Features",
337
+ # "ixStatusDefault" => 2,
338
+ # "fIsScheduleItem" => false
339
+ # }
340
+ # }
341
+ def list_process(xml, element, element_name)
342
+ return_value = Hash.new
343
+ (xml/"#{element}").each do |item|
344
+ if element_name[0,1] == "s"
345
+ item_name = CDATA_REGEXP.match((item/"#{element_name}").inner_html)[1]
346
+ else
347
+ item_name = (item/"#{element_name}").inner_html
348
+ end
349
+ return_value[item_name] = Hash.new
350
+
351
+ item.each_child do |attribute|
352
+ return_value[item_name][attribute.name] = attribute.inner_html
353
+ # convert values to proper types
354
+ 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
355
+ 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")
356
+ return_value[item_name][attribute.name] = (return_value[item_name][attribute.name] == "true") ? true : false if attribute.name[0,1] == "f"
357
+
358
+ end
359
+ end
360
+
361
+ return_value
362
+ end
363
+
364
+ # Converts a hash such as
365
+ # {
366
+ # :cmd => "logon",
367
+ # :email => "austin.moody@gmail.com",
368
+ # :password => "yeahwhatever",
369
+ # }
370
+ # to
371
+ # "cmd=logon&email=austin.moody@gmail.com&password=yeahwhatever"
372
+ def to_params(hash)
373
+ hash.map{|key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&')
374
+ end
375
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: austinmoody-fogbugz-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Austin Moody
8
+ - Gregory McIntyre
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2008-07-03 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: hpricot
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.6"
24
+ version:
25
+ description:
26
+ email: austin.moody@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README.rdoc
33
+ - LICENSE
34
+ - TODO
35
+ files:
36
+ - README.rdoc
37
+ - LICENSE
38
+ - TODO
39
+ - fogbugz-api.rb
40
+ - lib
41
+ - lib/fogbugz-api.rb
42
+ has_rdoc: true
43
+ homepage: http://github.com/austinmoody/ruby-fogbugz-api/wikis
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --main
47
+ - README.rdoc
48
+ - --inline-source
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 1.2.0
67
+ signing_key:
68
+ specification_version: 2
69
+ summary: Ruby wrapper for FogBugz API
70
+ test_files: []
71
+