axtro-roart 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ ==0.1.8 / 2010-03-16
2
+ Added validate presence of.
3
+ Updated specs to test validations.
4
+ No Longer depend on mechanize version 0.9.3 and get rid of those pesky deprecation warnings.
5
+ The connection adapter classes are now suffixed with "Adapter", so Mechanize is now MechanizeAdapter. You still specify it with :adapter => 'mechanize', however.
6
+
7
+ ==0.1.7 / 2010-03-11
8
+ Added some new error classes, add removed any extraneous raise 'text' (via jrbingham)
9
+ Removed debugging puts from the ticket file.
10
+
11
+ ==0.1.6 / 2010-03-09
12
+ Updated the search feature to pull all the ticket information at the same time, instead of only loading the id and subject.
13
+ Fixed tests that were failing because of out-of-order strings.
14
+ Returned to a sane version numbering scheme, sorry about that.
15
+
16
+ ==0.1.5.2 / 2010-01-20
17
+ New ticket objects now come with a default attribute :text that specifies the initial log of the ticket.
18
+
19
+ ==0.1.5.1 / 2010-01-20
20
+ Fixed a bug that caused custom fields not to show up in the ticket attributes
21
+
22
+ ==0.1.5 / 2010-01-19
23
+ Can add transactions ticket using #comment method. (via btucker)
24
+ Can authenticate again after the Ticket class is declared. (via newellista)
25
+ More search fields are now avaliable. (via threetee)
26
+
27
+ ==0.1.4 / 2009-08-19
28
+ Implemented callbacks, you can now hook into the ticket life-cycle with before and after create and update.
29
+ Saving works like ActiveRecord now, with save returning true if successful and false otherwise, and save! raising an error if unsuccessful.
30
+ Creating tickets is more ActiveRecord like now, with new not saving to the ticketing system automatically, you must call save or save!. There is a create function to do this now. Although this does break backward compatability, I'm not going to bump the minor because a) nobody has forked this yet, and b) i skipped 0.
31
+ You can now specify a default queue when you subclass Roart::Ticket. just add a default_queue 'queue name' to your class and that queue will automatically be searched unless you specify a queue.
32
+
33
+ ==0.1.3 / 2009-08-18
34
+ Can now update tickets with #save. Returns true if saved, raises an error if not.
35
+
36
+ ==0.1.2 / 2009-08-18
37
+ Added Create ticket functionality. Ticket.new will create a ticket in the RT system and return that ticket.
38
+
39
+ == 0.1.1 / 2009-08-17
40
+ Added History functionality. #histories now loads an array of history objects into memory and you can access them like you would a regular array. A few connivence methods (last, count) have also been added.
41
+
42
+ == 0.1.0 / 2009-08-13
43
+ Initial Commit. Base functionality added. Can search for tickets, and see all info on tickets. Ticket History not yet implemented.
@@ -0,0 +1,89 @@
1
+ == Roart
2
+
3
+ \___\__o/
4
+ / \
5
+
6
+
7
+ by PJ Davis
8
+ http://github.com/pjdavis/roart
9
+
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.
12
+
13
+
14
+ == FEATURES/PROBLEMS:
15
+
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.
19
+
20
+ == SYNOPSIS:
21
+
22
+ * Create a class to interact with your ticket system
23
+
24
+ require 'rubygems'
25
+ require 'roart'
26
+
27
+ class Ticket < Roart::Ticket
28
+ connection :server => 'http://my.rt.server.com', :user => 'myuser', :pass => 'mypass'
29
+
30
+ end
31
+
32
+ * Search for tickets
33
+
34
+ my_tickets = Ticket.find(:all, :queue => 'Scutters', :status => [:new, :open])
35
+ my_tickets.each do |ticket|
36
+ puts ticket.subject
37
+ end
38
+
39
+ #-> New John Wayne packages
40
+ #-> Medi-lab training
41
+
42
+ * See all info for a ticket
43
+
44
+ my_ticket = Ticket.find(:first, :queue => 'Issues', :status => :new)
45
+ ticket.creator #-> rimmer@reddwarf.com
46
+ ticket.subject #-> Where is the Bomb?
47
+
48
+ * Get history for a ticket
49
+
50
+ my_ticket.histories #-> Returns an array of history objects
51
+
52
+ * Create a new ticket
53
+
54
+ issue = Ticket.new(:queue => 'some_queue', :subject => 'This is not working for me')
55
+ issue.id #-> 'ticket/new'
56
+ issue.save
57
+ issue.id #-> 23423
58
+
59
+ * Update a ticket
60
+
61
+ ticket = Ticket.find(23452)
62
+ ticket.subject #-> "Some Subject for a ticket."
63
+ ticket.subject #-> "Smoke me a kipper, I'll be back for breakfast."
64
+ ticket.save
65
+ ticket.subject #->"Smoke me a kipper, I'll be back for breakfast."
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
+
71
+
72
+ == REQUIREMENTS:
73
+
74
+ * mechanize
75
+ * A working RT3 install.
76
+
77
+ == INSTALL:
78
+
79
+ $ gem sources -a http://gems.github.com
80
+ $ sudo gem install pjdavis-roart
81
+
82
+ == LICENSE:
83
+
84
+ (C) PJ Davis <pj.davis@gmail.com>
85
+
86
+ This program is free software; you can redistribute it and/or modify it under
87
+ the terms of the WTFPL, Version 2, as
88
+ published by Sam Hocevar. See http://sam.zoy.org/wtfpl/ for more details.
89
+
@@ -0,0 +1,35 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ Bones.setup
8
+ rescue LoadError
9
+ begin
10
+ load 'tasks/setup.rb'
11
+ rescue LoadError
12
+ raise RuntimeError, '### please install the "bones" gem ###'
13
+ end
14
+ end
15
+
16
+ ensure_in_path 'lib'
17
+ require 'roart'
18
+
19
+ task :default => 'spec:run'
20
+
21
+ PROJ.name = 'roart'
22
+ PROJ.ignore_file = '.gitignore'
23
+ PROJ.authors = 'PJ Davis'
24
+ PROJ.email = 'pj.davis@gmail.com'
25
+ PROJ.url = 'http://github.com/pjdavis/roart'
26
+ PROJ.version = Roart::VERSION
27
+ PROJ.rubyforge.name = 'roart'
28
+ PROJ.exclude = %w(.git pkg coverage)
29
+ PROJ.description = "Interface for working with Request Tracker (RT) tickets inspired by ActiveRecord."
30
+ PROJ.rdoc.main = 'README.rdoc'
31
+ depend_on 'mechanize' #this will go away when a stdlib adapter gets written
32
+
33
+ PROJ.spec.opts << '--color'
34
+
35
+ # EOF
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ module Roart
3
+
4
+ # :stopdoc:
5
+ VERSION = '0.1.8'
6
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
7
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
8
+ # :startdoc:
9
+
10
+ # Returns the version string for the library.
11
+ #
12
+ def self.version
13
+ VERSION
14
+ end
15
+
16
+ # Returns the library path for the module. If any arguments are given,
17
+ # they will be joined to the end of the libray path using
18
+ # <tt>File.join</tt>.
19
+ #
20
+ def self.libpath( *args )
21
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
22
+ end
23
+
24
+ # Returns the lpath for the module. If any arguments are given,
25
+ # they will be joined to the end of the path using
26
+ # <tt>File.join</tt>.
27
+ #
28
+ def self.path( *args )
29
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
30
+ end
31
+
32
+ # Utility method used to require all files ending in .rb that lie in the
33
+ # directory below this file that has the same name as the filename passed
34
+ # in. Optionally, a specific _directory_ name can be passed in such that
35
+ # the _filename_ does not have to be equivalent to the directory.
36
+ #
37
+ def self.require_all_libs_relative_to( fname, dir = nil )
38
+ dir ||= ::File.basename(fname, '.*')
39
+ search_me = ::File.expand_path(
40
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
41
+
42
+ Dir.glob(search_me).sort.each {|rb| require rb}
43
+ end
44
+
45
+ end # module Roart
46
+
47
+ Roart.require_all_libs_relative_to(__FILE__)
48
+
49
+ # EOF
@@ -0,0 +1,25 @@
1
+ module Roart
2
+
3
+ # Callbacks are implemented to do a bit of logic either before or after a part of the object life cycle. These can be overridden in your Ticket class and will be called at the approprate times.
4
+ #
5
+ module Callbacks
6
+
7
+ # called just before a ticket that has not been saved to the ticketing system is saved.
8
+ #
9
+ def before_create; end
10
+
11
+ # Called immediately a ticket that has not been saved is saved.
12
+ #
13
+ def before_update; end
14
+
15
+ # called just before a ticket that has been updated is saved to the ticketing system
16
+ #
17
+ def after_create; end
18
+
19
+ # called just after a ticket that has been updated is saved to the ticketing system
20
+ #
21
+ def after_update; end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,63 @@
1
+ require 'yaml'
2
+
3
+ module Roart
4
+
5
+ module Connections
6
+ RequiredConfig = %w(server adapter)
7
+ RequiredToLogin = %w( user pass )
8
+
9
+ end
10
+
11
+ class Connection
12
+
13
+ attr_reader :agent
14
+ attr_reader :conf
15
+
16
+ def initialize(conf)
17
+ if conf.is_a?(String)
18
+ raise RoartError, "Loading Config File not yet implemented"
19
+ elsif conf.class.name == Hash.name #TODO: Figure out why conf.is_a?(Hash) doesn't work
20
+ @conf = conf
21
+ end
22
+ if Roart::check_keys(conf, Roart::Connections::RequiredConfig)
23
+ @agent = @conf[:login]
24
+ add_methods!
25
+ @connection = ConnectionAdapter.new(@conf)
26
+ else
27
+ raise RoartError, "Configuration Error"
28
+ end
29
+ end
30
+
31
+ def authenticate(conf)
32
+ if Roart::check_keys(conf, Roart::Connections::RequiredToLogin)
33
+ @connection.authenticate(conf)
34
+ self
35
+ end
36
+ end
37
+
38
+ def rest_path
39
+ self.server + '/REST/1.0/'
40
+ end
41
+
42
+ def get(uri)
43
+ @connection.get(uri)
44
+ end
45
+
46
+ def post(uri, payload)
47
+ @connection.post(uri, payload)
48
+ end
49
+
50
+ protected
51
+
52
+
53
+ def add_methods!
54
+ @conf.each do |key, value|
55
+ (class << self; self; end).send :define_method, key do
56
+ return value
57
+ end
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ 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 + "Adapter").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,32 @@
1
+ require 'mechanize'
2
+
3
+ module Roart
4
+ module ConnectionAdapters
5
+ class MechanizeAdapter
6
+
7
+ def initialize(config)
8
+ @conf = config
9
+ end
10
+
11
+ def login(config)
12
+ @conf.merge!(config)
13
+ agent = Mechanize.new
14
+ page = agent.get(@conf[:server])
15
+ form = page.form('login')
16
+ form.user = @conf[:user]
17
+ form.pass = @conf[:pass]
18
+ page = agent.submit form
19
+ @agent = agent
20
+ end
21
+
22
+ def get(uri)
23
+ @agent.get(uri).body
24
+ end
25
+
26
+ def post(uri, payload)
27
+ @agent.post(uri, payload).body
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ class Array
2
+
3
+ def to_hash
4
+ h = HashWithIndifferentAccess.new
5
+ self.each do |element|
6
+ h.update(element => nil)
7
+ end
8
+ h
9
+ end
10
+
11
+ end
@@ -0,0 +1,22 @@
1
+ require "#{File.dirname(__FILE__)}/hash/indifferent_access.rb"
2
+ class Hash
3
+ def to_content_format
4
+ fields = self.map do |key,value|
5
+ unless value.nil?
6
+ if key.to_s.match(/^cf_.+/)
7
+ "CF-#{key.to_s[3..key.to_s.length].gsub(/_/, " ").camelize.humanize}: #{value}"
8
+ else
9
+ "#{key.to_s.camelize}: #{value}"
10
+ end
11
+ end
12
+ end
13
+ content = fields.compact.sort.join("\n")
14
+ end
15
+
16
+ def with_indifferent_access
17
+ hash = HashWithIndifferentAccess.new(self)
18
+ hash.default = self.default
19
+ hash
20
+ end
21
+
22
+ end
@@ -0,0 +1,135 @@
1
+ unless defined?(HashWithIndifferentAccess)
2
+ # used from ActiveSupport
3
+ # Copyright (c) 2005-2009 David Heinemeier Hansson
4
+
5
+ # This class has dubious semantics and we only have it so that
6
+ # people can write params[:key] instead of params['key']
7
+ # and they get the same value for both keys.
8
+
9
+ class HashWithIndifferentAccess < Hash
10
+ def initialize(constructor = {})
11
+ if constructor.is_a?(Hash)
12
+ super()
13
+ update(constructor)
14
+ else
15
+ super(constructor)
16
+ end
17
+ end
18
+
19
+ def default(key = nil)
20
+ if key.is_a?(Symbol) && include?(key = key.to_s)
21
+ self[key]
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
28
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
29
+
30
+ # Assigns a new value to the hash:
31
+ #
32
+ # hash = HashWithIndifferentAccess.new
33
+ # hash[:key] = "value"
34
+ #
35
+ def []=(key, value)
36
+ regular_writer(convert_key(key), convert_value(value))
37
+ end
38
+
39
+ # Updates the instantized hash with values from the second:
40
+ #
41
+ # hash_1 = HashWithIndifferentAccess.new
42
+ # hash_1[:key] = "value"
43
+ #
44
+ # hash_2 = HashWithIndifferentAccess.new
45
+ # hash_2[:key] = "New Value!"
46
+ #
47
+ # hash_1.update(hash_2) # => {"key"=>"New Value!"}
48
+ #
49
+ def update(other_hash)
50
+ other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
51
+ self
52
+ end
53
+
54
+ alias_method :merge!, :update
55
+
56
+ # Checks the hash for a key matching the argument passed in:
57
+ #
58
+ # hash = HashWithIndifferentAccess.new
59
+ # hash["key"] = "value"
60
+ # hash.key? :key # => true
61
+ # hash.key? "key" # => true
62
+ #
63
+ def key?(key)
64
+ super(convert_key(key))
65
+ end
66
+
67
+ alias_method :include?, :key?
68
+ alias_method :has_key?, :key?
69
+ alias_method :member?, :key?
70
+
71
+ # Fetches the value for the specified key, same as doing hash[key]
72
+ def fetch(key, *extras)
73
+ super(convert_key(key), *extras)
74
+ end
75
+
76
+ # Returns an array of the values at the specified indices:
77
+ #
78
+ # hash = HashWithIndifferentAccess.new
79
+ # hash[:a] = "x"
80
+ # hash[:b] = "y"
81
+ # hash.values_at("a", "b") # => ["x", "y"]
82
+ #
83
+ def values_at(*indices)
84
+ indices.collect {|key| self[convert_key(key)]}
85
+ end
86
+
87
+ # Returns an exact copy of the hash.
88
+ def dup
89
+ HashWithIndifferentAccess.new(self)
90
+ end
91
+
92
+ # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
93
+ # Does not overwrite the existing hash.
94
+ def merge(hash)
95
+ self.dup.update(hash)
96
+ end
97
+
98
+ # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
99
+ # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess.
100
+ def reverse_merge(other_hash)
101
+ super other_hash.with_indifferent_access
102
+ end
103
+
104
+ # Removes a specified key from the hash.
105
+ def delete(key)
106
+ super(convert_key(key))
107
+ end
108
+
109
+ def stringify_keys!; self end
110
+ def symbolize_keys!; self end
111
+ def to_options!; self end
112
+
113
+ # Convert to a Hash with String keys.
114
+ def to_hash
115
+ Hash.new(default).merge(self)
116
+ end
117
+
118
+ protected
119
+ def convert_key(key)
120
+ key.kind_of?(Symbol) ? key.to_s : key
121
+ end
122
+
123
+ def convert_value(value)
124
+ case value
125
+ when Hash
126
+ value.with_indifferent_access
127
+ when Array
128
+ value.collect { |e| e.is_a?(Hash) ? e.with_indifferent_access : e }
129
+ else
130
+ value
131
+ end
132
+ end
133
+ end
134
+
135
+ end