rallytastic 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +51 -0
- data/LICENSE +20 -0
- data/README.rdoc +85 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/lib/iteration.rb +52 -0
- data/lib/parsers/base.rb +23 -0
- data/lib/parsers/madison.rb +23 -0
- data/lib/project.rb +46 -0
- data/lib/rally/parsing_helpers.rb +70 -0
- data/lib/rally/rally_api.rb +102 -0
- data/lib/rallytastic.rb +30 -0
- data/lib/revision.rb +18 -0
- data/lib/revision_history.rb +19 -0
- data/lib/story.rb +101 -0
- data/lib/tasks/docs.thor +31 -0
- data/lib/tasks/scraper.thor +94 -0
- data/spec/fixtures/child_project.txt +1 -0
- data/spec/fixtures/iteration.txt +1 -0
- data/spec/fixtures/project.txt +1 -0
- data/spec/fixtures/story.txt +1 -0
- data/spec/fixtures/story_query.txt +1 -0
- data/spec/iteration_spec.rb +76 -0
- data/spec/project_spec.rb +64 -0
- data/spec/rally/rally_api_spec.rb +114 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/story_spec.rb +82 -0
- metadata +114 -0
data/.document
ADDED
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.0.0.rc)
|
5
|
+
activesupport (= 3.0.0.rc)
|
6
|
+
builder (~> 2.1.2)
|
7
|
+
i18n (~> 0.4.1)
|
8
|
+
activesupport (3.0.0.rc)
|
9
|
+
autotest (4.3.2)
|
10
|
+
bson (1.0.4)
|
11
|
+
bson_ext (1.0.4)
|
12
|
+
builder (2.1.2)
|
13
|
+
gemcutter (0.6.1)
|
14
|
+
git (1.2.5)
|
15
|
+
i18n (0.4.1)
|
16
|
+
jeweler (1.4.0)
|
17
|
+
gemcutter (>= 0.1.0)
|
18
|
+
git (>= 1.2.5)
|
19
|
+
rubyforge (>= 2.0.0)
|
20
|
+
json_pure (1.4.3)
|
21
|
+
mime-types (1.16)
|
22
|
+
mongo (1.0.6)
|
23
|
+
bson (>= 1.0.4)
|
24
|
+
mongoid (2.0.0.beta.15)
|
25
|
+
activemodel (= 3.0.0.rc)
|
26
|
+
bson (= 1.0.4)
|
27
|
+
mongo (= 1.0.6)
|
28
|
+
tzinfo (= 0.3.22)
|
29
|
+
will_paginate (~> 3.0.pre)
|
30
|
+
rake (0.8.7)
|
31
|
+
rest-client (1.6.0)
|
32
|
+
mime-types (>= 1.16)
|
33
|
+
rspec (1.3.0)
|
34
|
+
rubyforge (2.0.4)
|
35
|
+
json_pure (>= 1.1.7)
|
36
|
+
thor (0.14.0)
|
37
|
+
tzinfo (0.3.22)
|
38
|
+
will_paginate (3.0.pre2)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
autotest
|
45
|
+
bson_ext
|
46
|
+
jeweler
|
47
|
+
mongoid (= 2.0.0.beta.15)
|
48
|
+
rake
|
49
|
+
rest-client
|
50
|
+
rspec
|
51
|
+
thor
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Matt Clark
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
= rallytastic
|
2
|
+
== Description
|
3
|
+
|
4
|
+
Do you use Rally? Every day? Do you use it enough that you've run out of ways to hack the site to get that last ounce of customization from it? The reports, for example, while very good, aren't quite what you need to run your dev shop, are they? If you've taken a look at the RallyAPI longingly in the past, but written it off as too much work to do anything meaningful with, then Rallytastic is offering you a helping hand.
|
5
|
+
|
6
|
+
Rallytastic is a set of objects built on MongoDB and Mongoid, along with Thor tasks that pull data out of Rally and stuff it into those objects. Once there, Rallytastic tries to get out of your way and lets the data be your playground. Have at it with a platic shovel and Tonka Bulldozer if that'll get you up in the morning. You were born to get down with your bad Rally self. Be Awesome.
|
7
|
+
|
8
|
+
Rallytastic is intended to be the foundation of a web app that reports User Story data back to Rally users. It augments Rally proper primarily as an extention to the reporting capabilities. For example, if you are more concerned with the throughput of your dev teams than their velocity, Rallytastic is going to help you build custom fit measuring tools for that. It has built in helpers to walk through the revision logs of your stories and identify key events in their lifecycle. You can track work in progress(WIP) levels over time, measure how long stories sit prioritized on your backlog before dev starts, identify queues and bottlenecks anywhere between story definition time and release, and much more.
|
9
|
+
|
10
|
+
== Congiguration
|
11
|
+
|
12
|
+
In a file that is required from lib/rallytastic.rb, you can configure Rally authentication.
|
13
|
+
RallyAPI::configure do |config|
|
14
|
+
config.user = "username@AwesomeCompany.com"
|
15
|
+
config.password = "password"
|
16
|
+
end
|
17
|
+
|
18
|
+
You also need to have Mongo running, and let your app know what database to use. Either do it the Rails .yml way, or put something like this in a config file and include it:
|
19
|
+
Mongoid.configure do |config|
|
20
|
+
name = "rallytastic_dev"
|
21
|
+
host = "localhost"
|
22
|
+
config.master = Mongo::Connection.new.db(name)
|
23
|
+
config.persist_in_safe_mode = false
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
You should also set mongo to use a testing database in spec/spec_helper.rb
|
28
|
+
Mongoid.configure do |config|
|
29
|
+
name = "rallytastic_test"
|
30
|
+
host = "localhost"
|
31
|
+
config.master = Mongo::Connection.new.db(name)
|
32
|
+
config.persist_in_safe_mode = false
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
==Usage
|
37
|
+
|
38
|
+
=== Populating your MongoDB for the first time
|
39
|
+
> bundle exec thor scraper:projects
|
40
|
+
> bundle exec thor scraper:iterations
|
41
|
+
> bundle exec thor scraper:refresh
|
42
|
+
|
43
|
+
=== Revisions are pulled on request per iteration
|
44
|
+
> bundle exec thor scraper:revisions "Iteration Name"
|
45
|
+
|
46
|
+
=== On an ongoing basis, updating stories is quicker
|
47
|
+
> bundle exec thor scraper:refresh
|
48
|
+
# update the story revisions for the iteration you are interested in
|
49
|
+
> bundle exec thor scraper:revisions "Iteration Name"
|
50
|
+
|
51
|
+
=== Thor Tasks
|
52
|
+
==== scraper:help [TASK]
|
53
|
+
# Describe available tasks or one specific task
|
54
|
+
==== scraper:refresh
|
55
|
+
# Pulls down Rally stories since the last run, and creates Iterations and Projects as needed. This is the task you want to put in your job scheduler.
|
56
|
+
==== scraper:stories
|
57
|
+
# Walk Rally stories updated since last run
|
58
|
+
==== scraper:update_non_stories
|
59
|
+
# The scrape:stroies task creates shell iterations and projects for any that it doesn't know about yet. This task looks for those, and pull their information down from Rally
|
60
|
+
==== scraper:revisions iteration_name
|
61
|
+
# Get the revisions for stories hanging off an iteration
|
62
|
+
==== scraper:projects
|
63
|
+
# Bootstrapping method that is primarily useful when you are setting up Rallytastic for the first time. It will walk Rally and pull down all projects. Technically, you don't need to run this because using the scrape:stories; scrape:update_non_stories; will fill this information in for you. It may be a bit quicker though to run this first if you have a lot of projects because it does batch queries
|
64
|
+
==== scraper:iterations
|
65
|
+
# Bootstrapping method that is primarily useful when you are setting up Rallytastic for the first time. It will walk Rally and pull down all Iterations. Technically, you don't need to run this because using the scrape:stories; scrape:update_non_stories; will fill this information in for you. It may be a bit quicker though to run this first if you have a lot of iterations because it does batch queries
|
66
|
+
<End of Thor Tasks>
|
67
|
+
|
68
|
+
== TODO
|
69
|
+
* Add Releases
|
70
|
+
* Taging stories
|
71
|
+
* Reporting engine
|
72
|
+
|
73
|
+
== Note on Patches/Pull Requests
|
74
|
+
|
75
|
+
* Fork the project.
|
76
|
+
* Make your feature addition or bug fix.
|
77
|
+
* Add tests for it. This is important so I don't break it in a
|
78
|
+
future version unintentionally.
|
79
|
+
* Commit, do not mess with rakefile, version, or history.
|
80
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
81
|
+
* Send me a pull request. Bonus points for topic branches.
|
82
|
+
|
83
|
+
== Copyright
|
84
|
+
|
85
|
+
Copyright (c) 2010 Matt Clark. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "rallytastic"
|
8
|
+
gem.summary = %Q{Its a Rally story teleporter}
|
9
|
+
gem.description = %Q{longer description of your gem}
|
10
|
+
gem.email = "winescout@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/winescout/rallytastic"
|
12
|
+
gem.authors = ["Matt Clark"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'spec/rake/spectask'
|
22
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
23
|
+
spec.libs << 'lib' << 'spec'
|
24
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
25
|
+
end
|
26
|
+
|
27
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
28
|
+
spec.libs << 'lib' << 'spec'
|
29
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
30
|
+
spec.rcov = true
|
31
|
+
end
|
32
|
+
|
33
|
+
task :spec => :check_dependencies
|
34
|
+
|
35
|
+
task :default => :spec
|
36
|
+
|
37
|
+
require 'rake/rdoctask'
|
38
|
+
Rake::RDocTask.new do |rdoc|
|
39
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
40
|
+
|
41
|
+
rdoc.rdoc_dir = 'rdoc'
|
42
|
+
rdoc.title = "rallytastic #{version}"
|
43
|
+
rdoc.rdoc_files.include('README*')
|
44
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
45
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/lib/iteration.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
class Iteration
|
2
|
+
include Mongoid::Document
|
3
|
+
include Rally::ParsingHelpers
|
4
|
+
extend Rally::ParsingHelperClassMethods
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def rally_uri
|
8
|
+
"/iteration.js"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
field :name
|
13
|
+
field :rally_uri
|
14
|
+
field :created_on, :type => Date
|
15
|
+
field :description
|
16
|
+
field :notes
|
17
|
+
field :state
|
18
|
+
field :start_date, :type => Date
|
19
|
+
field :end_date, :type => Date
|
20
|
+
field :resources
|
21
|
+
field :theme
|
22
|
+
|
23
|
+
referenced_in :project
|
24
|
+
references_many :stories
|
25
|
+
|
26
|
+
def refresh hash_values=nil
|
27
|
+
@rally_hash = hash_values
|
28
|
+
from_rally :rally_uri, :_ref
|
29
|
+
from_rally :name
|
30
|
+
from_rally :theme
|
31
|
+
from_rally :state
|
32
|
+
from_rally :created_on, :_CreatedAt
|
33
|
+
from_rally :start_date, :StartDate
|
34
|
+
from_rally :end_date, :EndDate
|
35
|
+
from_rally :resources
|
36
|
+
|
37
|
+
self.save
|
38
|
+
rescue ArgumentError #getting some bad created_on dates
|
39
|
+
puts "Errored on #{self.name}"
|
40
|
+
self.save # save what you can
|
41
|
+
end
|
42
|
+
|
43
|
+
#has to be called after refresh, or with hash_values passed in
|
44
|
+
def associate hash_values=nil
|
45
|
+
@rally_hash = hash_values if hash_values
|
46
|
+
if @rally_hash.has_key?("Project")
|
47
|
+
project = Project.find_or_create_by(:rally_uri => @rally_hash["Project"]["_ref"])
|
48
|
+
project.iterations << self
|
49
|
+
end
|
50
|
+
self.save
|
51
|
+
end
|
52
|
+
end
|
data/lib/parsers/base.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Parser
|
2
|
+
class Base
|
3
|
+
attr_accessor :story
|
4
|
+
def initialize story
|
5
|
+
@story = story
|
6
|
+
end
|
7
|
+
|
8
|
+
def revisions
|
9
|
+
@story.revisions
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
def latest_revision_date_matching regexp
|
14
|
+
revision = revisions.desc(:created_on).select{|r| r.description =~ regexp}.first
|
15
|
+
revision.created_on if revision
|
16
|
+
end
|
17
|
+
|
18
|
+
def first_revision_date_matching regexp
|
19
|
+
revision = revisions.asc(:created_on).select{|r| r.description =~ regexp}.first
|
20
|
+
revision.created_on if revision
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Parser
|
2
|
+
class Madison < Base
|
3
|
+
def sized_on
|
4
|
+
first_revision_date_matching /TASK ESTIMATE TOTAL changed from \[0.0\]/
|
5
|
+
end
|
6
|
+
|
7
|
+
def prioritized_on
|
8
|
+
last_revision_date_matching /PROJECT changed from \[[^\]]*\] to \[Madison\]/
|
9
|
+
end
|
10
|
+
|
11
|
+
def started_on
|
12
|
+
last_revision_date_matching /ITERATION added/
|
13
|
+
end
|
14
|
+
|
15
|
+
def completed_on
|
16
|
+
last_revision_date_matching /SCHEDULE STATE changed from \[[^\]]*\] to [Complete]/
|
17
|
+
end
|
18
|
+
|
19
|
+
def accepted_on
|
20
|
+
last_revision_date_matching /SCHEDULE STATE changed from \[[^\]]*\] to [Accepted]/
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/project.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
class Project
|
2
|
+
include Mongoid::Document
|
3
|
+
include Rally::ParsingHelpers
|
4
|
+
extend Rally::ParsingHelperClassMethods
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def rally_uri
|
8
|
+
"/project.js"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
field :name
|
13
|
+
field :rally_uri
|
14
|
+
field :created_on
|
15
|
+
field :description
|
16
|
+
field :notes
|
17
|
+
field :state
|
18
|
+
|
19
|
+
field :revision_parser
|
20
|
+
|
21
|
+
referenced_in :parent, :class_name => "Project"
|
22
|
+
references_many :children, :class_name => "Project"
|
23
|
+
references_many :iterations
|
24
|
+
|
25
|
+
def refresh hash_values=nil
|
26
|
+
@rally_hash = hash_values if hash_values
|
27
|
+
from_rally :name
|
28
|
+
from_rally :description
|
29
|
+
from_rally :state
|
30
|
+
from_rally :notes
|
31
|
+
|
32
|
+
self.save
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
#must be called after refresh, or with has_values passed in
|
37
|
+
def associate hash_values=nil
|
38
|
+
@rally_hash = hash_values if hash_values
|
39
|
+
#TODO: associate with user when users are supported
|
40
|
+
if @rally_hash["Parent"]
|
41
|
+
parent = Project.find_or_create_by(:rally_uri => @rally_hash["Parent"]["_ref"])
|
42
|
+
self.parent = parent
|
43
|
+
end
|
44
|
+
self.save
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Rally
|
2
|
+
module ParsingHelperClassMethods
|
3
|
+
def node_name
|
4
|
+
if self.to_s == "Story"
|
5
|
+
"HierarchicalRequirement"
|
6
|
+
else
|
7
|
+
self.to_s
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def from_uri(uri)
|
12
|
+
obj = first(:conditions => {:rally_uri => uri}) || new(:rally_uri => uri)
|
13
|
+
obj.refresh
|
14
|
+
end
|
15
|
+
|
16
|
+
def rally_query options={}
|
17
|
+
RallyAPI.all(self, options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ParsingHelpers
|
22
|
+
def raw_json options = {}
|
23
|
+
options[:refresh] = options[:refresh] || false
|
24
|
+
|
25
|
+
unless self.rally_uri
|
26
|
+
raise "Could not fetch resource"
|
27
|
+
end
|
28
|
+
|
29
|
+
if @rally_hash
|
30
|
+
return @rally_hash
|
31
|
+
else
|
32
|
+
@rally_hash = get_fields_from_rally
|
33
|
+
end
|
34
|
+
|
35
|
+
@rally_hash
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def get_fields_from_rally
|
42
|
+
rally_hash = RallyAPI.get(self)
|
43
|
+
|
44
|
+
#special case for heirarchical_requirements
|
45
|
+
if rally_hash.has_key? "HierarchicalRequirement"
|
46
|
+
rally_hash = rally_hash["HierarchicalRequirement"]
|
47
|
+
end
|
48
|
+
|
49
|
+
rally_hash
|
50
|
+
end
|
51
|
+
|
52
|
+
#next 3 are for mapping from rally to mongo
|
53
|
+
def from_rally attr, key=nil
|
54
|
+
key = key || attr.capitalize
|
55
|
+
self.send("#{attr.to_s}=", raw_json[key.to_s])
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_refs key, rally_objects
|
59
|
+
if self.respond_to?(key) && !rally_objects.nil?
|
60
|
+
self.send("#{key.to_s}=", rally_objects.collect{|i| i["_ref"]})
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_ref key, rally_object
|
65
|
+
if self.respond_to?(key) && !rally_object.nil?
|
66
|
+
self.send("#{key.to_s}=", rally_object["_ref"])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|