fanfeedrb 0.0.1 → 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/fanfeedrb.gemspec +1 -1
- data/lib/fanfeedrb/fanfeedr/conference.rb +15 -0
- data/lib/fanfeedrb/fanfeedr/content.rb +15 -0
- data/lib/fanfeedrb/fanfeedr/event.rb +29 -0
- data/lib/fanfeedrb/fanfeedr/geo.rb +15 -0
- data/lib/fanfeedrb/fanfeedr/league.rb +31 -0
- data/lib/fanfeedrb/fanfeedr/team.rb +15 -0
- data/lib/fanfeedrb/fanfeedr.rb +130 -0
- data/lib/fanfeedrb/runner.rb +527 -0
- data/lib/fanfeedrb/version.rb +1 -1
- data/lib/fanfeedrb.rb +220 -66
- metadata +15 -7
data/fanfeedrb.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
|
|
12
12
|
s.summary = %q{Ruby implementation of the FanFeedr api}
|
13
13
|
s.description = %q{Ruby implementation of the FanFeedr api}
|
14
14
|
s.add_dependency('rake')
|
15
|
-
s.add_dependency('
|
15
|
+
s.add_dependency('crack')
|
16
16
|
s.rubyforge_project = "fanfeedrb"
|
17
17
|
|
18
18
|
s.files = `git ls-files`.split("\n")
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Fanfeedrb
|
2
|
+
class Fanfeedr
|
3
|
+
class Event < Abstract
|
4
|
+
attr_reader :name
|
5
|
+
#reader :description, :author, :position, :complete
|
6
|
+
date_reader :date
|
7
|
+
|
8
|
+
def when
|
9
|
+
date && Date.new(date.year, date.mon, date.day)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(event, attributes = {})
|
13
|
+
@event = event
|
14
|
+
super(attributes)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_xml
|
18
|
+
Fanfeedrb.hash_to_xml(:task, @attributes)
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"#<#{self.class.inspect}:#{id.inspect}, event_id: #{event.id.inspect}, date: #{when.inspect} >"
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Fanfeedrb
|
2
|
+
class Fanfeedr
|
3
|
+
class League < Abstract
|
4
|
+
attr_reader :fanfeedr
|
5
|
+
reader :gender, :levels, :sport, :name
|
6
|
+
|
7
|
+
def initialize(fanfeedr, attributes = {})
|
8
|
+
p "league attr: #{attributes}"
|
9
|
+
@fanfeedr = fanfeedr
|
10
|
+
super(attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
def conference(conference_id)
|
14
|
+
raise Error, "No conference id given" if conference_id.to_s.empty?
|
15
|
+
Conference.new(self,fanfeedr.get_json("/conferences/#{conference_id}"))
|
16
|
+
end
|
17
|
+
def conferences(*args)
|
18
|
+
path = "/leagues/#{id}/conferences"
|
19
|
+
#path << "?api_key=#{CGI.escape(Fanfeedrb.config['api_token'])}"
|
20
|
+
#if filter
|
21
|
+
response = fanfeedr.get_json(path)
|
22
|
+
[response].flatten.compact.map {|s| Conference.new(self,s)}
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'crack/json'
|
3
|
+
|
4
|
+
class Fanfeedrb
|
5
|
+
class Fanfeedr
|
6
|
+
#/silver/api/leagues/20f0857f-3c43-5f50-acfc-879f838ee853/events/4dd1704b-a712-511c-b947-8c8f03ea3200?api_key=vbxctn5sn8x7jz644evkrhtc
|
7
|
+
ADDRESS = "ffapi.fanfeedr.com"
|
8
|
+
BASE_PATH = "/#{Fanfeedrb.config['api_plan']}/api/"
|
9
|
+
#SEARCH_KEYS = %w(label type state requester owner mywork id includedone)
|
10
|
+
class Error < Fanfeedrb::Error; end
|
11
|
+
|
12
|
+
attr_reader :token,:plan
|
13
|
+
|
14
|
+
def initialize(token,plan, ssl = false)
|
15
|
+
@token = token
|
16
|
+
@plan = plan
|
17
|
+
@ssl = ssl
|
18
|
+
end
|
19
|
+
|
20
|
+
def ssl?
|
21
|
+
@ssl
|
22
|
+
end
|
23
|
+
def http
|
24
|
+
unless @http
|
25
|
+
if ssl?
|
26
|
+
require 'net/https'
|
27
|
+
@http = Net::HTTP.new(ADDRESS, Net::HTTP.https_default_port)
|
28
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
29
|
+
@http.use_ssl = true
|
30
|
+
else
|
31
|
+
require 'net/http'
|
32
|
+
@http = Net::HTTP.new(ADDRESS)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@http
|
36
|
+
end
|
37
|
+
|
38
|
+
def request(method, path, *args)
|
39
|
+
headers = {
|
40
|
+
#"api_key" => @token,
|
41
|
+
"Accept" => "application/json",
|
42
|
+
"Content-type" => "application/json"
|
43
|
+
}
|
44
|
+
http # trigger require of 'net/http'
|
45
|
+
klass = Net::HTTP.const_get(method.to_s.capitalize)
|
46
|
+
p klass
|
47
|
+
path << "?api_key=#{CGI.escape(@token)}"
|
48
|
+
p path
|
49
|
+
http.request(klass.new("#{BASE_PATH}#{path}", headers), *args)
|
50
|
+
end
|
51
|
+
def request_json(method, path, *args)
|
52
|
+
response = request(method,path,*args)
|
53
|
+
raise response.inspect if response["Content-type"].split(/; */).first != "application/json"
|
54
|
+
hash = Crack::JSON.parse(response.body)
|
55
|
+
if hash.class == Hash && hash["message"] && (response.code.to_i >= 400 || hash["success"] == "false")
|
56
|
+
raise Error, hash["message"], caller
|
57
|
+
end
|
58
|
+
hash
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_json(path)
|
62
|
+
p path
|
63
|
+
request_json(:get, path)
|
64
|
+
end
|
65
|
+
|
66
|
+
def league(id)
|
67
|
+
League.new(self,get_json("/leagues/#{id}"))
|
68
|
+
end
|
69
|
+
#def conference(id)
|
70
|
+
#Conference.new(self,get_json("/conferences/#{id}"))
|
71
|
+
#end
|
72
|
+
|
73
|
+
class Abstract
|
74
|
+
def initialize(attributes = {})
|
75
|
+
@attributes = {}
|
76
|
+
(attributes || {}).each do |k,v|
|
77
|
+
if respond_to?("#{k}=")
|
78
|
+
send("#{k}=", v)
|
79
|
+
else
|
80
|
+
@attributes[k.to_s] = v
|
81
|
+
end
|
82
|
+
end
|
83
|
+
yield self if block_given?
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.reader(*methods)
|
87
|
+
methods.each do |method|
|
88
|
+
define_method(method) { @attributes[method.to_s] }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.date_reader(*methods)
|
93
|
+
methods.each do |method|
|
94
|
+
define_method(method) do
|
95
|
+
value = @attributes[method.to_s]
|
96
|
+
value.kind_of?(String) ? Date.parse(value) : value
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.accessor(*methods)
|
102
|
+
reader(*methods)
|
103
|
+
methods.each do |method|
|
104
|
+
define_method("#{method}=") { |v| @attributes[method.to_s] = v }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def id
|
109
|
+
id = @attributes['id'] and Integer(id)
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_xml
|
113
|
+
Fanfeedrb.hash_to_xml(self.class.name.split('::').last.downcase, @attributes)
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
#require 'fanfeedrb/fanfeedr/league'
|
122
|
+
#require 'fanfeedrb/fanfeedr/events'
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
|
128
|
+
|
129
|
+
|
130
|
+
|
@@ -0,0 +1,527 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class Fanfeedrb
|
4
|
+
class Runner
|
5
|
+
|
6
|
+
class Base
|
7
|
+
attr_reader :argv
|
8
|
+
|
9
|
+
def initialize(argv)
|
10
|
+
@argv = argv
|
11
|
+
@tty = $stdout.tty?
|
12
|
+
@opts = OptionParser.new
|
13
|
+
@opts.version = "0.0"
|
14
|
+
@opts.banner = "Usage: fanfeedrb #{self.class.command_name} #{self.class.banner_arguments}"
|
15
|
+
@opts.base.long["help"] = OptionParser::Switch::NoArgument.new do
|
16
|
+
help = @opts.help.chomp.chomp + "\n"
|
17
|
+
help += "\n#{self.class.description}" if self.class.description
|
18
|
+
puts help
|
19
|
+
@exit = 0
|
20
|
+
end
|
21
|
+
@opts.separator("")
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.options
|
25
|
+
@options ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.on(*args, &block)
|
29
|
+
options << args
|
30
|
+
define_method("option_#{args.object_id}", &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.banner_arguments(value = nil)
|
34
|
+
if value
|
35
|
+
@banner_arguments = value
|
36
|
+
else
|
37
|
+
@banner_arguments || (arity.zero? ? "" : "...")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.summary(value = nil)
|
42
|
+
if value
|
43
|
+
@summary = value
|
44
|
+
else
|
45
|
+
@summary
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.description(value = nil)
|
50
|
+
if value
|
51
|
+
@description = value
|
52
|
+
else
|
53
|
+
@description || "#@summary."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.command_name
|
58
|
+
name.split('::').last.gsub(/(.)([A-Z])/) {"#$1-#$2"}.downcase
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.method_name
|
62
|
+
command_name.gsub('-','_')
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.process(&block)
|
66
|
+
define_method(:process, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.arity
|
70
|
+
instance_method(:process).arity
|
71
|
+
end
|
72
|
+
|
73
|
+
def arity
|
74
|
+
self.class.arity
|
75
|
+
end
|
76
|
+
|
77
|
+
def fanfeedrb
|
78
|
+
@fanfeedrb ||= Fanfeedrb.new(Dir.getwd)
|
79
|
+
end
|
80
|
+
|
81
|
+
def abort(message)
|
82
|
+
raise Error, message
|
83
|
+
end
|
84
|
+
|
85
|
+
def too_many
|
86
|
+
abort "too many arguments"
|
87
|
+
end
|
88
|
+
|
89
|
+
def run
|
90
|
+
self.class.options.each do |arguments|
|
91
|
+
@opts.on(*arguments, &method("option_#{arguments.object_id}"))
|
92
|
+
end
|
93
|
+
begin
|
94
|
+
@opts.parse!(@argv)
|
95
|
+
rescue OptionParser::InvalidOption
|
96
|
+
abort $!.message
|
97
|
+
end
|
98
|
+
return @exit if @exit
|
99
|
+
minimum = arity < 0 ? -1 - arity : arity
|
100
|
+
if arity >= 0 && arity < @argv.size
|
101
|
+
too_many
|
102
|
+
elsif minimum > @argv.size
|
103
|
+
abort "not enough arguments"
|
104
|
+
end
|
105
|
+
process(*@argv)
|
106
|
+
end
|
107
|
+
|
108
|
+
def process(*argv)
|
109
|
+
fanfeedrb.send(self.class.method_name,*argv)
|
110
|
+
end
|
111
|
+
|
112
|
+
def color?
|
113
|
+
case fanfeedrb.config["color"]
|
114
|
+
when "always" then true
|
115
|
+
when "never" then false
|
116
|
+
else
|
117
|
+
@tty && RUBY_PLATFORM !~ /mswin|mingw/
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def colorize(code, string)
|
122
|
+
if color?
|
123
|
+
"\e[#{code}m#{string}\e[00m"
|
124
|
+
else
|
125
|
+
string.to_s
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def puts_summary(story)
|
130
|
+
summary = "%6d " % story.id
|
131
|
+
type = story.estimate || TYPE_SYMBOLS[story.story_type]
|
132
|
+
state = STATE_SYMBOLS[story.current_state]
|
133
|
+
summary << colorize("3#{STATE_COLORS[story.current_state]}", state) << ' '
|
134
|
+
summary << colorize("01;3#{TYPE_COLORS[story.story_type]}", type) << ' '
|
135
|
+
summary << story.name
|
136
|
+
puts summary
|
137
|
+
end
|
138
|
+
|
139
|
+
def puts_full(story)
|
140
|
+
puts colorize("01;3#{TYPE_COLORS[story.story_type]}", story.name)
|
141
|
+
puts "Type: #{story.story_type}".rstrip
|
142
|
+
if story.story_type == "release"
|
143
|
+
puts "Deadline: #{story.deadline}".rstrip
|
144
|
+
else
|
145
|
+
puts "Estimate: #{story.estimate}".rstrip
|
146
|
+
end
|
147
|
+
puts "State: #{story.current_state}".rstrip
|
148
|
+
puts "Labels: #{story.labels.join(', ')}".rstrip
|
149
|
+
puts "Requester: #{story.requested_by}".rstrip
|
150
|
+
puts "Owner: #{story.owned_by}".rstrip
|
151
|
+
puts "URL: #{story.url}".rstrip
|
152
|
+
puts unless story.description =~ /^\s*$/
|
153
|
+
story.description_lines.each do |line|
|
154
|
+
puts " #{line}".rstrip
|
155
|
+
end
|
156
|
+
story.notes.each do |note|
|
157
|
+
puts
|
158
|
+
puts " #{colorize('01', note.author)} (#{note.date})"
|
159
|
+
puts(*note.lines(72).map {|l| " #{l}".rstrip})
|
160
|
+
end
|
161
|
+
story.tasks.each do |task|
|
162
|
+
# puts
|
163
|
+
# puts " #{colorize('01', note.author)} (#{note.date})"
|
164
|
+
# puts(*note.lines(72).map {|l| " #{l}".rstrip})
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
def paginated_output
|
170
|
+
stdout = $stdout
|
171
|
+
if @tty && pager = fanfeedrb.config["pager"]
|
172
|
+
# Modeled after git
|
173
|
+
ENV["LESS"] ||= "FRSX"
|
174
|
+
IO.popen(pager,"w") do |io|
|
175
|
+
$stdout = io
|
176
|
+
yield
|
177
|
+
end
|
178
|
+
else
|
179
|
+
yield
|
180
|
+
end
|
181
|
+
rescue Errno::EPIPE
|
182
|
+
ensure
|
183
|
+
$stdout = stdout
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
def self.[](command)
|
189
|
+
klass_name = command.to_s.capitalize.gsub(/[-_](.)/) { $1.upcase }
|
190
|
+
if klass_name =~ /^[A-Z]\w*$/ && const_defined?(klass_name)
|
191
|
+
klass = const_get(klass_name)
|
192
|
+
if Class === klass && klass < Base
|
193
|
+
return klass
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.commands
|
199
|
+
constants.map {|c| Runner.const_get(c)}.select {|c| Class === c && c < Runner::Base}.sort_by {|r| r.command_name}.uniq
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.command(name, &block)
|
203
|
+
const_set(name.to_s.capitalize.gsub(/[-_](.)/) { $1.upcase },Class.new(Base,&block))
|
204
|
+
end
|
205
|
+
|
206
|
+
command :show do
|
207
|
+
banner_arguments "<story>"
|
208
|
+
summary "Show details for a story"
|
209
|
+
|
210
|
+
on "--full", "default format" do |full|
|
211
|
+
@format = :full
|
212
|
+
end
|
213
|
+
|
214
|
+
on "--raw", "same as the .feature" do |raw|
|
215
|
+
@format = :raw
|
216
|
+
end
|
217
|
+
|
218
|
+
process do |*args|
|
219
|
+
case args.size
|
220
|
+
when 0
|
221
|
+
puts "#{fanfeedrb.project_id} #{fanfeedrb.project.name}"
|
222
|
+
when 1
|
223
|
+
feature = fanfeedrb.feature(args.first)
|
224
|
+
story = feature.story
|
225
|
+
case @format
|
226
|
+
when :raw
|
227
|
+
puts feature.story.to_s(fanfeedrb.format) if feature.story
|
228
|
+
else
|
229
|
+
paginated_output do
|
230
|
+
puts_full feature.story
|
231
|
+
end
|
232
|
+
end
|
233
|
+
else
|
234
|
+
too_many
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
command :search do
|
240
|
+
banner_arguments "[query]"
|
241
|
+
summary "List all stories matching a query"
|
242
|
+
|
243
|
+
def modifications
|
244
|
+
@modifications ||= {}
|
245
|
+
end
|
246
|
+
[:label, :type, :state].each do |o|
|
247
|
+
on "--#{o} #{o.to_s.upcase}" do |value|
|
248
|
+
modifications[o] = value
|
249
|
+
end
|
250
|
+
end
|
251
|
+
[:requester, :owner, :mywork].each do |o|
|
252
|
+
on "--#{o}[=USERNAME]" do |value|
|
253
|
+
modifications[o] = value || fanfeedrb.real_name
|
254
|
+
end
|
255
|
+
end
|
256
|
+
on "--[no-]includedone", "include accepted stories" do |value|
|
257
|
+
modifications[:includedone] = value
|
258
|
+
@iterations ||= []
|
259
|
+
@iterations << :done?
|
260
|
+
end
|
261
|
+
|
262
|
+
on "-b", "--backlog", "filter results to future iterations" do |c|
|
263
|
+
@iterations ||= []
|
264
|
+
@iterations << :backlog?
|
265
|
+
end
|
266
|
+
|
267
|
+
on "-c", "--current", "filter results to current iteration" do |b|
|
268
|
+
@iterations ||= []
|
269
|
+
@iterations << :current?
|
270
|
+
end
|
271
|
+
|
272
|
+
on "--[no-]full", "show full story, not a summary line" do |b|
|
273
|
+
@full = b
|
274
|
+
end
|
275
|
+
|
276
|
+
process do |*argv|
|
277
|
+
argv << modifications unless modifications.empty?
|
278
|
+
if argv == [{:includedone => true}]
|
279
|
+
# Bypass the 200 search results limitation
|
280
|
+
stories = fanfeedrb.project.stories
|
281
|
+
else
|
282
|
+
stories = fanfeedrb.project.stories(*argv)
|
283
|
+
end
|
284
|
+
if @iterations && @iterations != [:done?]
|
285
|
+
stories.reject! {|s| !@iterations.any? {|i| s.send(i)}}
|
286
|
+
end
|
287
|
+
paginated_output do
|
288
|
+
first = true
|
289
|
+
stories.each do |story|
|
290
|
+
if @full
|
291
|
+
puts unless first
|
292
|
+
puts_full story
|
293
|
+
else
|
294
|
+
puts_summary story
|
295
|
+
end
|
296
|
+
first = false
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
command :push do
|
303
|
+
banner_arguments "[story] ..."
|
304
|
+
summary "Upload stories"
|
305
|
+
description <<-EOF
|
306
|
+
Upload the given story or all features with a tracker url in a comment on the
|
307
|
+
first line. Features with a blank comment in the first line will created as
|
308
|
+
new stories.
|
309
|
+
EOF
|
310
|
+
|
311
|
+
process do |*args|
|
312
|
+
args.replace(fanfeedrb.local_features) if args.empty?
|
313
|
+
args.each do |arg|
|
314
|
+
fanfeedrb.feature(arg).push
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
command :pull do
|
320
|
+
banner_arguments "[story] ..."
|
321
|
+
summary "Download stories"
|
322
|
+
description <<-EOF
|
323
|
+
Download the given story story to the features/ directory. With no arguments,
|
324
|
+
downloads all stories that look like Cucumber features (with "Scenario:" and
|
325
|
+
valid Cucumber syntax). The version without arguments is mainly useful for
|
326
|
+
syncing existing features.
|
327
|
+
EOF
|
328
|
+
|
329
|
+
process do |*args|
|
330
|
+
args.replace(fanfeedrb.scenario_features) if args.empty?
|
331
|
+
args.each do |arg|
|
332
|
+
fanfeedrb.feature(arg).pull
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
command :start do
|
338
|
+
banner_arguments "<story> [basename]"
|
339
|
+
summary "Pull a story and mark it started"
|
340
|
+
description <<-EOF
|
341
|
+
Pull a given story and change its state to started. If basename is given
|
342
|
+
and no local file exists, features/basename.feature will be created. Give a
|
343
|
+
basename of "-" to use a downcased, underscored version of the story name as
|
344
|
+
the basename.
|
345
|
+
EOF
|
346
|
+
|
347
|
+
process do |story, *args|
|
348
|
+
fanfeedrb.feature(story).start(args.first)
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
command :finish do
|
353
|
+
banner_arguments "<story>"
|
354
|
+
summary "Push a story and mark it finished"
|
355
|
+
|
356
|
+
process do |story|
|
357
|
+
fanfeedrb.feature(story).finish
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
command :deliver do
|
362
|
+
banner_arguments "[story] ..."
|
363
|
+
summary "Mark stories delivered"
|
364
|
+
on "--all-finished", "deliver all finished stories" do
|
365
|
+
@all = true
|
366
|
+
end
|
367
|
+
process do |*args|
|
368
|
+
if @all
|
369
|
+
fanfeedrb.deliver_all_finished_stories
|
370
|
+
end
|
371
|
+
args.each do |arg|
|
372
|
+
fanfeedrb.story(arg).transition!('delivered')
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
command :unstart do
|
378
|
+
banner_arguments "[story] ..."
|
379
|
+
summary "Mark stories unstarted"
|
380
|
+
on "--all-started", "unstart all started stories" do
|
381
|
+
@all = true
|
382
|
+
end
|
383
|
+
process do |*args|
|
384
|
+
if @all
|
385
|
+
fanfeedrb.project.stories(:state => "started").each do |story|
|
386
|
+
story.transition!('unstarted')
|
387
|
+
end
|
388
|
+
end
|
389
|
+
args.each do |arg|
|
390
|
+
fanfeedrb.story(arg).transition!('unstarted')
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
command :unschedule do
|
396
|
+
banner_arguments "[story] ..."
|
397
|
+
summary "Move stories to icebox"
|
398
|
+
process do |*args|
|
399
|
+
args.each do |arg|
|
400
|
+
fanfeedrb.story(arg).transition!('unscheduled')
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
command :browse do
|
406
|
+
banner_arguments "[story]"
|
407
|
+
summary "Open a story in the web browser"
|
408
|
+
description <<-EOF
|
409
|
+
Open project or a story in the web browser.
|
410
|
+
|
411
|
+
Requires launchy (gem install launchy).
|
412
|
+
EOF
|
413
|
+
|
414
|
+
on "--dashboard" do
|
415
|
+
@special = "dashboard"
|
416
|
+
end
|
417
|
+
on "--faq" do
|
418
|
+
@special = "help"
|
419
|
+
end
|
420
|
+
on "--profile", "get your API Token here" do
|
421
|
+
@special = "profile"
|
422
|
+
end
|
423
|
+
on "--time", "not publicly available" do
|
424
|
+
@special = "time_shifts?project=#{fanfeedrb.project_id}"
|
425
|
+
end
|
426
|
+
|
427
|
+
process do |*args|
|
428
|
+
too_many if args.size > 1 || @special && args.first
|
429
|
+
if args.first
|
430
|
+
url = fanfeedrb.story(args.first).url
|
431
|
+
elsif @special
|
432
|
+
url = "http://www.pivotaltracker.com/#@special"
|
433
|
+
else
|
434
|
+
url = "http://www.pivotaltracker.com/projects/#{fanfeedrb.project_id}/stories"
|
435
|
+
end
|
436
|
+
require 'launchy'
|
437
|
+
Launchy.open(url)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
command :comment do
|
442
|
+
banner_arguments "<story> <paragraph> ..."
|
443
|
+
summary "Post a comment to a story"
|
444
|
+
|
445
|
+
process do |story, *paragraphs|
|
446
|
+
fanfeedrb.story(story).comment!(paragraphs.join("\n\n"))
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
def initialize(argv)
|
451
|
+
@argv = argv
|
452
|
+
end
|
453
|
+
|
454
|
+
COLORS = {
|
455
|
+
:black => 0,
|
456
|
+
:red => 1,
|
457
|
+
:green => 2,
|
458
|
+
:yellow => 3,
|
459
|
+
:blue => 4,
|
460
|
+
:magenta => 5,
|
461
|
+
:cyan => 6,
|
462
|
+
:white => 7
|
463
|
+
}
|
464
|
+
|
465
|
+
STATE_COLORS = {
|
466
|
+
nil => COLORS[:black],
|
467
|
+
"rejected" => COLORS[:red],
|
468
|
+
"accepted" => COLORS[:green],
|
469
|
+
"delivered" => COLORS[:yellow],
|
470
|
+
"unscheduled" => COLORS[:white],
|
471
|
+
"started" => COLORS[:magenta],
|
472
|
+
"finished" => COLORS[:cyan],
|
473
|
+
"unstarted" => COLORS[:blue]
|
474
|
+
}
|
475
|
+
|
476
|
+
STATE_SYMBOLS = {
|
477
|
+
"unscheduled" => " ",
|
478
|
+
"unstarted" => ":|",
|
479
|
+
"started" => ":/",
|
480
|
+
"finished" => ":)",
|
481
|
+
"delivered" => ";)",
|
482
|
+
"rejected" => ":(",
|
483
|
+
"accepted" => ":D"
|
484
|
+
}
|
485
|
+
|
486
|
+
TYPE_COLORS = {
|
487
|
+
'chore' => COLORS[:blue],
|
488
|
+
'feature' => COLORS[:magenta],
|
489
|
+
'bug' => COLORS[:red],
|
490
|
+
'release' => COLORS[:cyan]
|
491
|
+
}
|
492
|
+
|
493
|
+
TYPE_SYMBOLS = {
|
494
|
+
"feature" => "*",
|
495
|
+
"chore" => "%",
|
496
|
+
"release" => "!",
|
497
|
+
"bug" => "/"
|
498
|
+
}
|
499
|
+
|
500
|
+
def run
|
501
|
+
command = @argv.shift
|
502
|
+
if klass = self.class[command]
|
503
|
+
result = klass.new(@argv).run
|
504
|
+
exit result.respond_to?(:to_int) ? result.to_int : 0
|
505
|
+
elsif ['help', '--help', '-h', '', nil].include?(command)
|
506
|
+
puts "usage: fanfeedrb <command> [options] [arguments]"
|
507
|
+
puts
|
508
|
+
puts "Commands:"
|
509
|
+
self.class.commands.each do |command|
|
510
|
+
puts " %-19s %s" % [command.command_name, command.summary]
|
511
|
+
end
|
512
|
+
puts
|
513
|
+
puts "Run fanfeedrb <command> --help for help with a given command"
|
514
|
+
else
|
515
|
+
raise Error, "Unknown fanfeedrb command #{command}"
|
516
|
+
end
|
517
|
+
rescue fanfeedrb::Error
|
518
|
+
$stderr.puts "#$!"
|
519
|
+
exit 1
|
520
|
+
rescue Interrupt
|
521
|
+
$stderr.puts "Interrupted!"
|
522
|
+
exit 130
|
523
|
+
end
|
524
|
+
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
data/lib/fanfeedrb/version.rb
CHANGED
data/lib/fanfeedrb.rb
CHANGED
@@ -1,85 +1,239 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
class Fanfeedrb
|
2
|
+
class Error < RuntimeError
|
3
|
+
end
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
5
|
+
autoload :Runner, 'fanfeedrb/runner'
|
6
|
+
|
7
|
+
def self.config
|
8
|
+
@config ||= {'api_token' => ENV["FANFEEDR_API_TOKEN"]}.merge(
|
9
|
+
if File.exist?(path = File.expand_path('~/.fanfeedr.yml'))
|
10
|
+
YAML.load_file(path)
|
11
|
+
end || {}
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.hash_to_xml(root, attributes)
|
16
|
+
require 'cgi'
|
17
|
+
xml = "<#{root}>"
|
18
|
+
attributes.each do |k,v|
|
19
|
+
if v.kind_of?(Hash)
|
20
|
+
xml << hash_to_xml(k, v)
|
20
21
|
else
|
21
|
-
|
22
|
+
xml << "<#{k}>#{CGI.escapeHTML(v.to_s)}</#{k}>"
|
22
23
|
end
|
23
24
|
end
|
24
|
-
|
25
|
-
def execute(method)
|
26
|
-
p @proxy.url
|
27
|
-
res = FanfeedrbResponse.construct self.class.send(method,@proxy.url,@opts)
|
28
|
-
@proxy = FanfeedrbProxy.new
|
29
|
-
res
|
30
|
-
end
|
25
|
+
xml << "</#{root}>"
|
31
26
|
end
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
27
|
+
|
28
|
+
def self.run(argv)
|
29
|
+
Runner.new(argv).run
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :directory
|
33
|
+
|
34
|
+
def initialize(path = '.')
|
35
|
+
@lang = 'en'
|
36
|
+
@directory = File.expand_path(path)
|
37
|
+
#until File.directory?(File.join(@directory,'features'))
|
38
|
+
#if @directory == File.dirname(@directory)
|
39
|
+
#raise Error, 'not found. Make sure you have a features/ directory.', caller
|
40
|
+
#end
|
41
|
+
#@directory = File.dirname(@directory)
|
42
|
+
#end
|
43
|
+
end
|
44
|
+
|
45
|
+
def feedr_path(*subdirs)
|
46
|
+
File.join(@directory,'fanfeedr',*subdirs)
|
47
|
+
end
|
48
|
+
|
49
|
+
def config_file
|
50
|
+
feedr_path('fanfeedr.yml')
|
51
|
+
end
|
52
|
+
|
53
|
+
def config
|
54
|
+
@config ||= File.exist?(config_file) && YAML.load_file(config_file) || {}
|
55
|
+
self.class.config.merge(@config)
|
56
|
+
end
|
57
|
+
|
58
|
+
def new_story(attributes = {}, &block)
|
59
|
+
attributes = attributes.inject('requested_by' => real_name) do |h,(k,v)|
|
60
|
+
h.update(k.to_s => v)
|
43
61
|
end
|
62
|
+
.new_story(attributes, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def stories(*args)
|
66
|
+
project.stories(*args)
|
67
|
+
end
|
68
|
+
|
69
|
+
def name
|
70
|
+
league.name
|
71
|
+
end
|
72
|
+
|
73
|
+
def iteration_length
|
74
|
+
project.iteration_length
|
75
|
+
end
|
76
|
+
|
77
|
+
def point_scale
|
78
|
+
project.point_scale
|
79
|
+
end
|
80
|
+
|
81
|
+
def week_start_day
|
82
|
+
project.week_start_day
|
83
|
+
end
|
84
|
+
|
85
|
+
def deliver_all_finished_stories
|
86
|
+
project.deliver_all_finished_stories
|
87
|
+
end
|
88
|
+
|
89
|
+
#def parse(story)
|
90
|
+
#require 'cucumber'
|
91
|
+
#Cucumber::FeatureFile.new(story.url, story.to_s).parse(
|
92
|
+
#Cucumber::Cli::Options.new,
|
93
|
+
#{}
|
94
|
+
#)
|
95
|
+
#end
|
96
|
+
|
97
|
+
def league_id
|
98
|
+
config["league_id"] || (self.class.config["leagues"]||{})[File.basename(@directory)]
|
99
|
+
end
|
100
|
+
|
101
|
+
def leagues
|
44
102
|
|
45
|
-
|
46
|
-
|
103
|
+
end
|
104
|
+
|
105
|
+
def league
|
106
|
+
@league ||= Dir.chdir(@directory) do
|
107
|
+
unless token = config['api_token']
|
108
|
+
raise Error, 'echo api_token: ... > ~/.fanfeedr.yml'
|
109
|
+
end
|
110
|
+
unless plan = config['api_plan']
|
111
|
+
raise Error, 'echo api_plan: ... > ~/.fanfeedr.yml'
|
112
|
+
end
|
113
|
+
unless id = league_id
|
114
|
+
raise Error, 'echo league_id: ... > fanfeedr/fanfeedr.yml'
|
115
|
+
end
|
116
|
+
ssl = config['ssl']
|
117
|
+
Fanfeedr.new(token, plan, ssl).league(id)
|
47
118
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
119
|
+
end
|
120
|
+
|
121
|
+
def scenario_word
|
122
|
+
#require 'cucumber'
|
123
|
+
#Gherkin::I18n::LANGUAGES[@lang]['scenario']
|
124
|
+
if @lang == 'en'
|
125
|
+
'Scenario'
|
126
|
+
else
|
127
|
+
raise Error, 'Sorry, no internationalization support (yet)'
|
52
128
|
end
|
53
129
|
|
54
130
|
end
|
55
131
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
def initialize(hash)
|
60
|
-
hresp = []
|
61
|
-
if hash.parsed_response.nil? || hash.parsed_response.blank?
|
62
|
-
hresp << hash
|
63
|
-
else
|
64
|
-
hresp << hash.parsed_response
|
65
|
-
end
|
66
|
-
@hresp = hresp.flatten if !hresp.blank?
|
132
|
+
def format
|
133
|
+
(config['format'] || :tag).to_sym
|
134
|
+
end
|
67
135
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
136
|
+
def local_features
|
137
|
+
Dir[features_path('**','*.feature')].map {|f|feature(f)}.select {|f|f.pushable?}
|
138
|
+
end
|
139
|
+
|
140
|
+
def scenario_features (excluded_states = %w()) #(excluded_states = %w(unscheduled unstarted))
|
141
|
+
project.stories(scenario_word, :includedone => true).reject do |s|
|
142
|
+
Array(excluded_states).map {|state| state.to_s}.include?(s.current_state)
|
143
|
+
end.select do |s|
|
144
|
+
s.to_s =~ /^\s*#{Regexp.escape(scenario_word)}:/
|
72
145
|
end
|
73
146
|
end
|
147
|
+
|
148
|
+
def feature(string)
|
149
|
+
string.kind_of?(Feature) ? string : Feature.new(self,string)
|
150
|
+
end
|
151
|
+
|
152
|
+
def story(string)
|
153
|
+
feature(string).story
|
154
|
+
end
|
155
|
+
|
156
|
+
protected
|
157
|
+
|
158
|
+
|
74
159
|
end
|
75
160
|
|
76
|
-
#
|
77
|
-
#
|
78
|
-
|
79
|
-
|
161
|
+
#class Client
|
162
|
+
#def initialize(key,type)
|
163
|
+
#@auth = {:key => key, :type => type}
|
164
|
+
#@proxy = FanfeedrbProxy.new
|
165
|
+
#end
|
166
|
+
|
167
|
+
#def method_missing(method, *args, &block)
|
168
|
+
#@proxy.append(method, args[0])
|
169
|
+
#@opts = {:query => @proxy.options}
|
170
|
+
#if args.size > 0 && !method.to_s.eql?("post")
|
171
|
+
#execute("get")
|
172
|
+
#elsif method.to_s.match /\bget\b|\bpost\b/
|
173
|
+
#execute(method)
|
174
|
+
#else
|
175
|
+
#execute("get")
|
176
|
+
#end
|
177
|
+
#end
|
178
|
+
|
179
|
+
#def execute(method)
|
180
|
+
#p @proxy.url
|
181
|
+
#res = FanfeedrbResponse.construct self.class.send(method,@proxy.url,@opts)
|
182
|
+
#@proxy = FanfeedrbProxy.new
|
183
|
+
#res
|
184
|
+
#end
|
185
|
+
#end
|
186
|
+
|
187
|
+
#class FanfeedrbProxy
|
188
|
+
#attr_reader :options
|
189
|
+
|
190
|
+
#def initialize
|
191
|
+
#@keys = []; @options = {}
|
192
|
+
#end
|
193
|
+
|
194
|
+
#def append(key,options)
|
195
|
+
##@options.merge!(self.default_json_options)
|
196
|
+
#@keys << key; @options.merge!(options) if options
|
197
|
+
#end
|
198
|
+
|
199
|
+
#def url
|
200
|
+
#@url = "http://ffapi.fanfeedr.com/#{ENV['FANFEEDR_TYPE']}/api/" + @keys.join("/")
|
201
|
+
#end
|
202
|
+
#protected
|
203
|
+
|
204
|
+
#def default_json_options
|
205
|
+
#{:api_key => @auth[:key]}
|
206
|
+
#end
|
207
|
+
|
208
|
+
#end
|
209
|
+
|
210
|
+
#class FanfeedrbResponse
|
211
|
+
#attr_reader :errors
|
212
|
+
#attr_reader :hresp
|
213
|
+
#def initialize(hash)
|
214
|
+
#hresp = []
|
215
|
+
#if hash.parsed_response.nil? || hash.parsed_response.blank?
|
216
|
+
#hresp << hash
|
217
|
+
#else
|
218
|
+
#hresp << hash.parsed_response
|
219
|
+
#end
|
220
|
+
#@hresp = hresp.flatten if !hresp.blank?
|
221
|
+
|
222
|
+
#end
|
223
|
+
|
224
|
+
#def self.construct(res)
|
225
|
+
#return res.class == Array ? res.collect { |item| FanfeedrbResponse.new(item) } : FanfeedrbResponse.new(res)
|
226
|
+
#end
|
227
|
+
#end
|
228
|
+
#end
|
80
229
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
230
|
+
##Examples
|
231
|
+
##client = Fanfeedrb::Client.new(ENV['FANFEEDR_KEY'],ENV['FANFEEDR_TYPE'])
|
232
|
+
##leagues = client.statuses.show(:id => "13400589015")
|
233
|
+
##p status.errors ? status.errors : status.text
|
234
|
+
#http://ffapi.fanfeedr.com/silver/api/leagues/20f0857f-3c43-5f50-acfc-879f838ee853/events/4dd1704b-a712-511c-b947-8c8f03ea3200?api_key=vbxctn5sn8x7jz644evkrhtc
|
235
|
+
## More Examples
|
236
|
+
##user = client.users.lookup(:screen_name => "gregosuri")
|
237
|
+
##client.statuses.update.post(:status=>"Ruby Metaprogramming Rocks")
|
238
|
+
## Your code goes here...
|
85
239
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fanfeedrb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,12 +9,12 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-06-
|
12
|
+
date: 2011-06-12 00:00:00.000000000 -04:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rake
|
17
|
-
requirement: &
|
17
|
+
requirement: &2164394580 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ! '>='
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: '0'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *2164394580
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
28
|
-
requirement: &
|
27
|
+
name: crack
|
28
|
+
requirement: &2164394160 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ! '>='
|
@@ -33,7 +33,7 @@ dependencies:
|
|
33
33
|
version: '0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *2164394160
|
37
37
|
description: Ruby implementation of the FanFeedr api
|
38
38
|
email:
|
39
39
|
- mjording@opengotham.com
|
@@ -46,6 +46,14 @@ files:
|
|
46
46
|
- Rakefile
|
47
47
|
- fanfeedrb.gemspec
|
48
48
|
- lib/fanfeedrb.rb
|
49
|
+
- lib/fanfeedrb/fanfeedr.rb
|
50
|
+
- lib/fanfeedrb/fanfeedr/conference.rb
|
51
|
+
- lib/fanfeedrb/fanfeedr/content.rb
|
52
|
+
- lib/fanfeedrb/fanfeedr/event.rb
|
53
|
+
- lib/fanfeedrb/fanfeedr/geo.rb
|
54
|
+
- lib/fanfeedrb/fanfeedr/league.rb
|
55
|
+
- lib/fanfeedrb/fanfeedr/team.rb
|
56
|
+
- lib/fanfeedrb/runner.rb
|
49
57
|
- lib/fanfeedrb/version.rb
|
50
58
|
has_rdoc: true
|
51
59
|
homepage: http://iequalsi.com
|