foggy_bottom 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rvmrc
6
+ test.rb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in foggy_bottom.gemspec
4
+ gemspec
data/README ADDED
@@ -0,0 +1,4 @@
1
+ FoggyBottom is a Ruby wrapper around the FogBugz API. It's not a direct match, so you don't
2
+ need to know how to use the FogBugz API to use this library.
3
+
4
+ It's written in "normal" Ruby syntax and patterns, so no strange "port" of another library.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "foggy_bottom/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "foggy_bottom"
7
+ s.version = FoggyBottom::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Jon Moses"]
10
+ s.email = ["jon@burningbush.us"]
11
+ s.homepage = ""
12
+ s.summary = %q{Ruby API wrapper for FogBugz}
13
+ s.description = %q{It wraps the FogBugz API.}
14
+
15
+ #s.rubyforge_project = "foggy_bottom"
16
+ s.add_dependency('nokogiri')
17
+ s.add_dependency('activemodel')
18
+ s.add_dependency('hirb')
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+ end
@@ -0,0 +1,15 @@
1
+ require 'nokogiri'
2
+ require 'open-uri'
3
+ require 'digest/sha2'
4
+ require 'cgi'
5
+ require 'logger'
6
+ require 'delegate'
7
+
8
+ require 'active_support/core_ext/array'
9
+ require 'active_model'
10
+ require 'hirb'
11
+
12
+ require 'foggy_bottom/api'
13
+ require 'foggy_bottom/columns'
14
+ require 'foggy_bottom/case'
15
+ require 'foggy_bottom/console'
@@ -0,0 +1,76 @@
1
+ module FoggyBottom
2
+ class Api
3
+ attr_accessor :token, :url, :endpoint
4
+ attr_reader :logger
5
+
6
+ def initialize( url, username, password )
7
+ @logger = Logger.new(STDOUT).tap {|l| l.level = Logger::DEBUG }
8
+ @url = url[-1] == '/' ? url : "#{url}/"
9
+
10
+ token_filename = File.expand_path( "~/.foggy_bottom_token_" + Digest::SHA2.hexdigest("#{username}_#{password}") )
11
+
12
+ if File.exists?(token_filename)
13
+ load_details(token_filename)
14
+ else
15
+ fetch_endpoint
16
+ fetch_token(username, password, token_filename)
17
+ end
18
+ end
19
+
20
+ def exec(cmd, args = {})
21
+ get(build_url(cmd, args))
22
+ end
23
+
24
+ def find( issue_id )
25
+ Case.find(issue_id, self)
26
+ end
27
+ alias :[] :find
28
+
29
+ def search( terms )
30
+ Case.search(terms, self)
31
+ end
32
+
33
+ protected
34
+ def fetch_token(username, password, token_filename)
35
+ @token ||= begin
36
+ logger.debug("Fetching token")
37
+ get(build_url(:logon, :email => username, :password => password)).at_css("response token").content.tap do |token|
38
+ logger.debug(" done.")
39
+ save_token(token, token_filename)
40
+ end
41
+ end
42
+ end
43
+
44
+ def load_details(token_filename)
45
+ logger.debug("Loading cached credentials")
46
+
47
+ @endpoint, @token = File.read(token_filename).split("\n").map(&:strip)
48
+ end
49
+
50
+ def save_token(token, token_filename)
51
+ logger.debug("Caching token")
52
+
53
+ File.open(token_filename, 'w') do |out|
54
+ out << endpoint
55
+ out << "\n"
56
+ out << token
57
+ end
58
+ end
59
+
60
+ def fetch_endpoint
61
+ @endpoint ||= begin
62
+ logger.debug("Fetching endpoint")
63
+ Nokogiri.XML( open("#{url}/api.xml") {|f| f.read } ).at_css("response url").content
64
+ end
65
+ end
66
+
67
+ def get(path)
68
+ logger.debug("Attempting to access: #{path}")
69
+ Nokogiri.XML( open(path) {|f| f.read } )
70
+ end
71
+
72
+ def build_url(cmd, args)
73
+ url + endpoint + args.merge(:cmd => cmd.to_s, :token => token).inject([]) {|memo, (key, value)| memo << "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,83 @@
1
+ class FoggyBottom::Case
2
+ include FoggyBottom::Columns::Case
3
+
4
+ include ActiveModel::AttributeMethods
5
+ include ActiveModel::Dirty
6
+ include ActiveModel::Serialization
7
+
8
+ define_attribute_methods ALL_COLUMNS
9
+
10
+ attr_accessor :api
11
+
12
+ delegate :logger, :to => :api
13
+
14
+ class << self
15
+ def find( case_id, api )
16
+ details = api.exec(:search, :q => case_id, :cols => FoggyBottom::Columns::Case::ALL_COLUMNS.join(',') ).at_css("case")
17
+
18
+ create_from_xml(details, api) if details
19
+ end
20
+
21
+ def search( terms, api )
22
+ api.exec(:search, :q => terms, :cols => FoggyBottom::Columns::Case::ALL_COLUMNS.join(',')).css('case').collect do |details|
23
+ create_from_xml(details, api)
24
+ end
25
+ end
26
+
27
+ def create_from_xml(xml, api)
28
+ new( {}.tap do |attributes|
29
+ (FoggyBottom::Columns::Case::ALL_COLUMNS - %w(tags)).each do |col|
30
+ attributes[col] = xml.at_css(col).content
31
+ end
32
+
33
+ attributes['tags'] = [].tap do |tags|
34
+ xml.css("tag").each {|t| tags << t.content }
35
+ end
36
+ end).tap do |instance|
37
+ instance.api = api
38
+ end
39
+
40
+ end
41
+ end
42
+
43
+ def initialize(attributes = {})
44
+ @attributes = attributes.stringify_keys
45
+ end
46
+
47
+ def attributes=(new_attributes)
48
+ new_attributes.each_pair {|k,v| send("#{k}=", v) }
49
+ end
50
+
51
+ def save( comment = nil )
52
+ return unless changed?
53
+
54
+ save!(comment)
55
+ end
56
+
57
+ def resolve( comment = nil )
58
+ save!(comment, :resolve)
59
+ end
60
+
61
+ def to_s
62
+ "#{ixBug} - #{sTitle}"
63
+ end
64
+
65
+ protected
66
+ def attributes
67
+ @attributes
68
+ end
69
+
70
+ def save!(comment, action = :edit)
71
+ @previously_changed = changes
72
+
73
+ logger.debug("Saving changes for #{ixBug} columns #{changed.join(', ')} with action #{action}")
74
+ api.exec(action, parameters('sEvent' => comment))
75
+ logger.debug(" done")
76
+
77
+ @changed_attributes.clear
78
+ end
79
+
80
+ def parameters( extra = {} )
81
+ changed.inject({}) {|memo, key| memo.merge(key => send(key))}.merge(extra).merge('ixBug' => ixBug)
82
+ end
83
+ end
@@ -0,0 +1,80 @@
1
+ module FoggyBottom
2
+ module Columns
3
+ module Case
4
+ def self.included(base)
5
+ base.class_eval do
6
+ ALL_COLUMNS.each do |column|
7
+ define_method(column) do
8
+ @attributes[column]
9
+ end
10
+
11
+ define_method("#{column}=") do |arg|
12
+ send("#{column}_will_change!") unless send(column) == arg
13
+
14
+ @attributes[column] = arg
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ ALL_COLUMNS = %w(
21
+ ixBug
22
+ ixBugParent
23
+ ixBugChildren
24
+ tags
25
+ fOpen
26
+ sTitle
27
+ sOriginalTitle
28
+ sLatestTextSummary
29
+ ixBugEventLatestText
30
+ ixProject
31
+ sProject
32
+ ixArea
33
+ sArea
34
+ ixGroup
35
+ ixPersonAssignedTo
36
+ sPersonAssignedTo
37
+ sEmailAssignedTo
38
+ ixPersonOpenedBy
39
+ ixPersonResolvedBy
40
+ ixPersonClosedBy
41
+ ixPersonLastEditedBy
42
+ ixStatus
43
+ sStatus
44
+ ixPriority
45
+ sPriority
46
+ ixFixFor
47
+ sFixFor
48
+ dtFixFor
49
+ sVersion
50
+ sComputer
51
+ hrsOrigEst
52
+ hrsCurrEst
53
+ hrsElapsed
54
+ c
55
+ sCustomerEmail
56
+ ixMailbox
57
+ ixCategory
58
+ sCategory
59
+ dtOpened
60
+ dtResolved
61
+ dtClosed
62
+ ixBugEventLatest
63
+ dtLastUpdated
64
+ fReplied
65
+ fForwarded
66
+ sTicket
67
+ ixDiscussTopic
68
+ dtDue
69
+ sReleaseNotes
70
+ ixBugEventLastView
71
+ dtLastView
72
+ ixRelatedBugs
73
+ sScoutDescription
74
+ sScoutMessage
75
+ fScoutStopReporting
76
+ fSubscribed
77
+ )
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,59 @@
1
+ class FoggyBottom::Console
2
+ delegate :logger, :to => :api
3
+
4
+ attr_accessor :api
5
+ def initialize(api)
6
+ @api = api
7
+ end
8
+
9
+ def run
10
+ while( cmd = show_menu ) do
11
+ case cmd
12
+ when :search
13
+ search(get("Terms"))
14
+ when :resolve
15
+ resolve(get("Case ID"))
16
+ when :quit
17
+ exit 0
18
+ else
19
+ puts " Unknown choice"
20
+ end
21
+ end
22
+ end
23
+
24
+ protected
25
+ def show_menu
26
+ commands = %w( search resolve quit )
27
+ choice = (Hirb::Menu.render commands, :helper_class => false, :directions => false).first
28
+ choice ? choice.to_sym : :unknown
29
+ end
30
+
31
+ def choose( arguments, options = {})
32
+ Hirb::Menu.render arguments, options.merge(:helper_class => false)
33
+ end
34
+
35
+ def get(prompt)
36
+ STDOUT.write "#{prompt}: "
37
+ STDOUT.flush
38
+ STDIN.gets.chomp
39
+ end
40
+
41
+ def search(terms)
42
+ unless terms
43
+ puts "Search terms are required."
44
+ return
45
+ end
46
+
47
+ cases = api.search(terms)
48
+ choose(cases.map(&:to_s))
49
+ end
50
+
51
+ def resolve( case_id )
52
+ bug = api.find(case_id)
53
+ if bug
54
+ bug.resolve
55
+ else
56
+ puts "Bug #{case_id} not found"
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module FoggyBottom
2
+ VERSION = "0.0.2"
3
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: foggy_bottom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jon Moses
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-03 00:00:00.000000000 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: nokogiri
17
+ requirement: &2152537960 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *2152537960
26
+ - !ruby/object:Gem::Dependency
27
+ name: activemodel
28
+ requirement: &2152537540 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *2152537540
37
+ - !ruby/object:Gem::Dependency
38
+ name: hirb
39
+ requirement: &2152537120 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *2152537120
48
+ description: It wraps the FogBugz API.
49
+ email:
50
+ - jon@burningbush.us
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - Gemfile
57
+ - README
58
+ - Rakefile
59
+ - foggy_bottom.gemspec
60
+ - lib/foggy_bottom.rb
61
+ - lib/foggy_bottom/api.rb
62
+ - lib/foggy_bottom/case.rb
63
+ - lib/foggy_bottom/columns.rb
64
+ - lib/foggy_bottom/console.rb
65
+ - lib/foggy_bottom/version.rb
66
+ has_rdoc: true
67
+ homepage: ''
68
+ licenses: []
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 1.6.2
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Ruby API wrapper for FogBugz
91
+ test_files: []