active_pivot 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +78 -0
- data/Rakefile +7 -0
- data/active_pivot.gemspec +37 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/active_pivot/activity.rb +55 -0
- data/lib/active_pivot/api/activity.rb +24 -0
- data/lib/active_pivot/api/config.rb +9 -0
- data/lib/active_pivot/api/epic.rb +16 -0
- data/lib/active_pivot/api/filter.rb +34 -0
- data/lib/active_pivot/api/paginated_collection.rb +44 -0
- data/lib/active_pivot/api/project.rb +11 -0
- data/lib/active_pivot/api/request.rb +37 -0
- data/lib/active_pivot/api/response.rb +38 -0
- data/lib/active_pivot/api/story.rb +24 -0
- data/lib/active_pivot/epic.rb +11 -0
- data/lib/active_pivot/epic_story.rb +8 -0
- data/lib/active_pivot/importer.rb +84 -0
- data/lib/active_pivot/pivotal.rb +7 -0
- data/lib/active_pivot/project.rb +20 -0
- data/lib/active_pivot/railtie.rb +9 -0
- data/lib/active_pivot/story.rb +147 -0
- data/lib/active_pivot/version.rb +3 -0
- data/lib/active_pivot.rb +24 -0
- data/lib/generators/active_pivot/migrations_generator.rb +12 -0
- data/lib/tasks/import.rake +27 -0
- metadata +150 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ddfd8ecabf7eb02b0c27dbe9dee4379f809abba9
|
4
|
+
data.tar.gz: 87c5999311a37760f3aa71905151831b1233f02f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1245ceaa173eca33e982179b910c063ed179016aa99bdc5244979090bebba3362c1a1715fdb1dbbbf93c2d5a4ff5bc03ecfe08db1ffc4c1b13d2c58ac0213a01
|
7
|
+
data.tar.gz: 367fb78a74ff099c4a9d5e507e533634ad55f0b2eba1f7adfcb90c2262f2eca99bcc4987fe0a10f091be7fa6037da636101f67697c2993e43133d3c6930a2130
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
active_pivot
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.2
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Allan McLelland
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# ActivePivot
|
2
|
+
|
3
|
+
An easy way to store your Pivotal Tracker projects, stories, and epics
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'active_pivot'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install active_pivot
|
20
|
+
|
21
|
+
Next, generate the migrations by running:
|
22
|
+
|
23
|
+
$ rails g active_pivot:migrations
|
24
|
+
|
25
|
+
Open up the "create_pivotal_stories" and change the line:
|
26
|
+
|
27
|
+
t.text[] :tags
|
28
|
+
|
29
|
+
to:
|
30
|
+
|
31
|
+
t.text :tags, array: true, default: []
|
32
|
+
|
33
|
+
Now, you can review the migrations and then run
|
34
|
+
|
35
|
+
$ rake db:migrate
|
36
|
+
|
37
|
+
and the requisite tables will be created.
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
Add your Pivotal Tracker API token to your secrets.yml:
|
42
|
+
|
43
|
+
`tracker_api_token: <%= ENV["PIVOTAL_TRACKER_API_TOKEN"] %>`
|
44
|
+
|
45
|
+
Now you can import your projects and stories using the following:
|
46
|
+
|
47
|
+
`rake active_pivot:import:pivotal_initial` for all activity up to 3 years ago
|
48
|
+
|
49
|
+
`rake active_pivot:import:pivotal_date[]` for all activity since a particular date
|
50
|
+
example: `rake active_pivot:import:date['August 12, 2015']`
|
51
|
+
|
52
|
+
`rake active_pivot:import:pivotal_update[]` for all activity since X minutes ago
|
53
|
+
example: `rake active_pivot:import:update[15]`
|
54
|
+
|
55
|
+
This gem will create the following models:
|
56
|
+
- [ActivePivot::Activity](lib/active_pivot/activity.rb)
|
57
|
+
- [ActivePivot::Epic](lib/active_pivot/epic.rb)
|
58
|
+
- [ActivePivot::EpicStory](lib/active_pivot/epic_story.rb)
|
59
|
+
- [ActivePivot::Project](lib/active_pivot/project.rb)
|
60
|
+
- [ActivePivot::Story](lib/active_pivot/story.rb)
|
61
|
+
|
62
|
+
You can subclass these models in your project to customize behavior.
|
63
|
+
|
64
|
+
|
65
|
+
## Development
|
66
|
+
|
67
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
68
|
+
|
69
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
70
|
+
|
71
|
+
## Contributing
|
72
|
+
|
73
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/foraker/active_pivot.
|
74
|
+
|
75
|
+
|
76
|
+
## License
|
77
|
+
|
78
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_pivot/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "active_pivot"
|
8
|
+
spec.version = ActivePivot::VERSION
|
9
|
+
spec.authors = ["Allan McLelland", "Jon Evans"]
|
10
|
+
spec.email = ["awm@foraker.com", "jle@foraker.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Import all of your Pivotal projects and stories easily}
|
13
|
+
#spec.description = %q{TODO: Write a longer description or delete this line.}
|
14
|
+
spec.homepage = "http://www.foraker.com"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "rails", "> 3"
|
31
|
+
spec.add_dependency "httparty"
|
32
|
+
|
33
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
34
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
35
|
+
spec.add_development_dependency "rspec"
|
36
|
+
|
37
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "active_pivot"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
class Activity
|
3
|
+
attr_accessor :remote_activity
|
4
|
+
|
5
|
+
def initialize(remote_activity)
|
6
|
+
self.remote_activity = remote_activity
|
7
|
+
end
|
8
|
+
|
9
|
+
def store
|
10
|
+
story.update_attribute(:started_at, updated_at) if store?
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def store?
|
16
|
+
started? && started_before?(updated_at)
|
17
|
+
end
|
18
|
+
|
19
|
+
def started_before?(updated_at)
|
20
|
+
story.started_at.nil? || story.started_at < updated_at
|
21
|
+
end
|
22
|
+
|
23
|
+
def story
|
24
|
+
@story ||= ActivePivot::Story.find_by_pivotal_id(story_id)
|
25
|
+
end
|
26
|
+
|
27
|
+
def started?
|
28
|
+
current_state.present? && current_state == 'started'
|
29
|
+
end
|
30
|
+
|
31
|
+
def kind
|
32
|
+
@kind ||= primary_resource['kind']
|
33
|
+
end
|
34
|
+
|
35
|
+
def story_id
|
36
|
+
@story_id ||= primary_resource['id']
|
37
|
+
end
|
38
|
+
|
39
|
+
def current_state
|
40
|
+
new_values.present? ? new_values['current_state'] : 'unstarted'
|
41
|
+
end
|
42
|
+
|
43
|
+
def updated_at
|
44
|
+
@updated_at ||= new_values['updated_at']
|
45
|
+
end
|
46
|
+
|
47
|
+
def primary_resource
|
48
|
+
remote_activity.primary_resources[0]
|
49
|
+
end
|
50
|
+
|
51
|
+
def new_values
|
52
|
+
remote_activity.changes[0]['new_values']
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
module Api
|
3
|
+
class Activity < OpenStruct
|
4
|
+
STATES = %w{ accepted delivered finished started rejected unstarted unscheduled }
|
5
|
+
|
6
|
+
def self.default_filter
|
7
|
+
Filter.new({
|
8
|
+
# state: STATES,
|
9
|
+
# includedone: true
|
10
|
+
})
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.for_project(project_id, story_id, params = {})
|
14
|
+
collection(project_id, story_id).all
|
15
|
+
.map { |story| self.new(story) rescue nil }
|
16
|
+
.compact
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.collection(project_id, story_id, params = {})
|
20
|
+
PaginatedCollection.new("/services/v5/projects/#{project_id}/stories/#{story_id}/activity.json")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
module Api
|
3
|
+
class Epic < OpenStruct
|
4
|
+
def self.for_project(project_id, params = {})
|
5
|
+
collection(project_id, params).all
|
6
|
+
.reject { |response| response["kind"] == "error" }
|
7
|
+
.map { |epic| self.new(epic) rescue nil }
|
8
|
+
.compact
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.collection(project_id, params = {})
|
12
|
+
PaginatedCollection.new("/services/v5/projects/#{project_id}/epics.json", params.as_json)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
module Api
|
3
|
+
class Filter
|
4
|
+
def initialize(params = {})
|
5
|
+
@params = params
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_params
|
9
|
+
{filter: filter_string}
|
10
|
+
end
|
11
|
+
|
12
|
+
def merge(new_params)
|
13
|
+
@params = @params.merge(new_params)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def filter_string
|
20
|
+
@params.map do |key, value|
|
21
|
+
[key, sanitize_value(value)].join(":")
|
22
|
+
end.join(" ")
|
23
|
+
end
|
24
|
+
|
25
|
+
def sanitize_value(value)
|
26
|
+
case value
|
27
|
+
when Date, Time then value.iso8601
|
28
|
+
when Array then value.join(",")
|
29
|
+
else value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
module Api
|
3
|
+
class PaginatedCollection
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_reader :endpoint, :params
|
7
|
+
|
8
|
+
delegate :total_pages, :limit, to: :first_page
|
9
|
+
delegate :each, to: :all
|
10
|
+
|
11
|
+
def initialize(endpoint, params = {})
|
12
|
+
@endpoint = endpoint
|
13
|
+
@params = params
|
14
|
+
end
|
15
|
+
|
16
|
+
def all
|
17
|
+
pages.flat_map(&:parsed_response)
|
18
|
+
end
|
19
|
+
|
20
|
+
def pages
|
21
|
+
[first_page] + subsequent_pages
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def subsequent_pages
|
27
|
+
return [] unless multiple_pages?
|
28
|
+
|
29
|
+
(2..total_pages).map do |page|
|
30
|
+
offset = limit * (page - 1)
|
31
|
+
Request.get(endpoint, params.merge(offset: offset))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def multiple_pages?
|
36
|
+
total_pages > 1
|
37
|
+
end
|
38
|
+
|
39
|
+
def first_page
|
40
|
+
@first_page ||= Request.get(endpoint, params)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module ActivePivot
|
4
|
+
module Api
|
5
|
+
class Request
|
6
|
+
HOST = "https://www.pivotaltracker.com"
|
7
|
+
|
8
|
+
attr_accessor :path, :params
|
9
|
+
|
10
|
+
def self.get(path, params = {})
|
11
|
+
self.new(path, params).get
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(path, params = {})
|
15
|
+
@path = path
|
16
|
+
@params = params
|
17
|
+
end
|
18
|
+
|
19
|
+
def get
|
20
|
+
Response.new(HTTParty.get(HOST + path, options))
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def options
|
26
|
+
{
|
27
|
+
headers: {"X-TrackerToken" => api_token},
|
28
|
+
query: params
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def api_token
|
33
|
+
Api::Config.api_token
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
module Api
|
3
|
+
class Response < Struct.new(:response)
|
4
|
+
delegate :parsed_response, :headers, to: :response
|
5
|
+
delegate :each, to: :parsed_response
|
6
|
+
|
7
|
+
def next_page?
|
8
|
+
(offset * limit + returned) < total
|
9
|
+
end
|
10
|
+
|
11
|
+
def total_pages
|
12
|
+
(total / limit.to_f).ceil rescue 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def success?
|
16
|
+
[200, 201].include?(response.code)
|
17
|
+
end
|
18
|
+
|
19
|
+
def limit
|
20
|
+
headers["X-Tracker-Pagination-Limit"].to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def offset
|
26
|
+
headers["X-Tracker-Pagination-Offset"].to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
def total
|
30
|
+
headers["X-Tracker-Pagination-Total"].to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
def returned
|
34
|
+
headers["X-Tracker-Pagination-Returned"].to_i
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
module Api
|
3
|
+
class Story < OpenStruct
|
4
|
+
STATES = %w{ accepted delivered finished started rejected unstarted unscheduled }
|
5
|
+
|
6
|
+
def self.default_filter
|
7
|
+
Filter.new({
|
8
|
+
state: STATES,
|
9
|
+
includedone: true
|
10
|
+
})
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.for_project(project_id, story_id, params = {})
|
14
|
+
collection(project_id, story_id, default_filter.merge(params).to_params).all
|
15
|
+
.map { |story| self.new(story) rescue nil }
|
16
|
+
.compact
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.collection(project_id, story_id, params = {})
|
20
|
+
PaginatedCollection.new("/services/v5/projects/#{project_id}/stories.json", params.as_json)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
class Epic < ActiveRecord::Base
|
3
|
+
self.table_name = "pivotal_epics"
|
4
|
+
|
5
|
+
has_many :epic_stories
|
6
|
+
has_many :stories, through: :epic_stories
|
7
|
+
has_many :time_entries, through: :stories
|
8
|
+
belongs_to :project, primary_key: :pivotal_id,
|
9
|
+
foreign_key: :pivotal_project_id
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
class Importer < Struct.new(:params)
|
3
|
+
def self.run(updated_after = 5.minutes.ago)
|
4
|
+
self.new({updated_after: updated_after}).run
|
5
|
+
end
|
6
|
+
|
7
|
+
def run
|
8
|
+
import_projects
|
9
|
+
import_epics
|
10
|
+
import_stories
|
11
|
+
import_activities
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def import_projects
|
17
|
+
Api::Project.all.each do |remote_project|
|
18
|
+
ActivePivot::Project.where(pivotal_id: remote_project.id)
|
19
|
+
.first_or_initialize
|
20
|
+
.update_attributes!({
|
21
|
+
name: remote_project.name,
|
22
|
+
point_scale: remote_project.point_scale
|
23
|
+
})
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def import_epics
|
28
|
+
ActivePivot::Project.all.each do |project|
|
29
|
+
project.remote_epics(params.except(:updated_after)).each do |remote_epic|
|
30
|
+
import_epic(remote_epic)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def import_stories
|
36
|
+
Project.all.each do |project|
|
37
|
+
project.remote_stories(params).each do |remote_story|
|
38
|
+
import_story(remote_story)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def import_activities
|
44
|
+
Story.all.each do |story|
|
45
|
+
story.remote_activities(params).each do |remote_activity|
|
46
|
+
import_activity(remote_activity)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def import_epic(remote_epic)
|
52
|
+
ActivePivot::Epic.where(pivotal_id: remote_epic.id.to_s)
|
53
|
+
.first_or_initialize
|
54
|
+
.update_attributes({
|
55
|
+
project_id: remote_epic.project_id,
|
56
|
+
name: remote_epic.name,
|
57
|
+
url: remote_epic.url,
|
58
|
+
label_id: remote_epic.label.try(:[], "id")
|
59
|
+
})
|
60
|
+
end
|
61
|
+
|
62
|
+
def import_story(remote_story)
|
63
|
+
ActivePivot::Story.where(pivotal_id: remote_story.id.to_s)
|
64
|
+
.first_or_initialize
|
65
|
+
.update_attributes!({
|
66
|
+
project_id: remote_story.project_id,
|
67
|
+
pivotal_id: remote_story.id,
|
68
|
+
name: remote_story.name,
|
69
|
+
description: remote_story.description,
|
70
|
+
kind: remote_story.kind,
|
71
|
+
story_type: remote_story.story_type,
|
72
|
+
labels: remote_story.labels,
|
73
|
+
current_state: remote_story.current_state,
|
74
|
+
estimate: remote_story.estimate,
|
75
|
+
accepted_at: remote_story.accepted_at,
|
76
|
+
url: remote_story.url
|
77
|
+
})
|
78
|
+
end
|
79
|
+
|
80
|
+
def import_activity(remote_activity)
|
81
|
+
ActivePivot::Activity.new(remote_activity).store
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
class Project < ActiveRecord::Base
|
3
|
+
self.table_name = "pivotal_projects"
|
4
|
+
|
5
|
+
has_many :stories, primary_key: :pivotal_id, foreign_key: :project_id
|
6
|
+
has_many :epics, primary_key: :pivotal_id, foreign_key: :pivotal_project_id
|
7
|
+
|
8
|
+
def self.alphabetized
|
9
|
+
order("lower(#{table_name}.name)")
|
10
|
+
end
|
11
|
+
|
12
|
+
def remote_stories(params = {})
|
13
|
+
Api::Story.for_project(pivotal_id, params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def remote_epics(params = {})
|
17
|
+
Api::Epic.for_project(pivotal_id, params)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
class Story < ActiveRecord::Base
|
3
|
+
self.table_name = "pivotal_stories"
|
4
|
+
|
5
|
+
belongs_to :project, primary_key: :pivotal_id, foreign_key: :project_id
|
6
|
+
|
7
|
+
has_many :time_entries, foreign_key: :project_number, primary_key: :pivotal_id
|
8
|
+
has_many :project_types, through: :time_entries
|
9
|
+
|
10
|
+
has_many :epic_stories
|
11
|
+
has_many :epics, through: :epic_stories
|
12
|
+
|
13
|
+
delegate :name, to: :project, prefix: true
|
14
|
+
|
15
|
+
serialize :labels, Array
|
16
|
+
|
17
|
+
def remote_activities(params = {})
|
18
|
+
Api::Activity.for_project(project_id, pivotal_id, params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.for_project(project_id)
|
22
|
+
where(project_id: project_id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.with_status(status)
|
26
|
+
where(current_state: status)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.with_estimate(estimate)
|
30
|
+
where(estimate: estimate)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.with_number(number)
|
34
|
+
where(pivotal_id: number)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.order_by_project_name
|
38
|
+
joins(:project).merge(Project.alphabetized)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.subselect(scope, attribute = :id)
|
42
|
+
where("#{table_name}.id IN (#{scope.select(attribute).to_sql})")
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.worked_on_before(date)
|
46
|
+
subselect Story.unscoped.joins(:time_entries).merge(TimeEntry.worked_on_before(date))
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.worked_on_after(date)
|
50
|
+
subselect Story.unscoped.joins(:time_entries).merge(TimeEntry.worked_on_after(date))
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.unique_statuses
|
54
|
+
group(:current_state).pluck(:current_state)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.unique_estimates
|
58
|
+
group(:estimate).pluck(:estimate).reject(&:blank?)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.select_all
|
62
|
+
select("#{table_name}.*")
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.accepted
|
66
|
+
where.not(accepted_at: nil)
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.accepted_after(date)
|
70
|
+
where("#{table_name}.accepted_at >= ?", date)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.accepted_before(date)
|
74
|
+
where("#{table_name}.accepted_at <= ?", date)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.select_points(as = :accepted_points)
|
78
|
+
select("SUM(#{table_name}.estimate) as #{as}")
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.with_tags(*tags)
|
82
|
+
where("#{table_name}.tags @> ARRAY[?]", tags.join(","))
|
83
|
+
end
|
84
|
+
|
85
|
+
def labels=(labels)
|
86
|
+
super(Array.wrap(labels))
|
87
|
+
|
88
|
+
self.epics = extract_epics_for_labels(labels)
|
89
|
+
self.tags = extract_tags_for_labels(labels)
|
90
|
+
end
|
91
|
+
|
92
|
+
def state
|
93
|
+
current_state
|
94
|
+
end
|
95
|
+
|
96
|
+
def human_state
|
97
|
+
state.titleize
|
98
|
+
end
|
99
|
+
|
100
|
+
def human_type
|
101
|
+
story_type.titleize
|
102
|
+
end
|
103
|
+
|
104
|
+
def hours
|
105
|
+
read_attribute(:minutes) ? (minutes / 60.0) : time_entries.map(&:hours).sum
|
106
|
+
end
|
107
|
+
|
108
|
+
def invoice_hours
|
109
|
+
read_attribute(:invoice_minutes) ? (invoice_minutes / 60.0) : time_entries.map(&:invoice_hours).sum
|
110
|
+
end
|
111
|
+
|
112
|
+
def billed_amount
|
113
|
+
(read_attribute(:billed_amount) || time_entries.map(&:billed_amount).sum).to_f
|
114
|
+
end
|
115
|
+
|
116
|
+
def billed_amount_per_point
|
117
|
+
estimate.to_i.zero? ? 0 : billed_amount / estimate.to_i
|
118
|
+
end
|
119
|
+
|
120
|
+
def hours_per_point
|
121
|
+
estimate.to_i.zero? ? 0 : hours / estimate.to_i
|
122
|
+
end
|
123
|
+
|
124
|
+
def invoice_hours_per_point
|
125
|
+
estimate.to_i.zero? ? 0 : invoice_hours / estimate.to_i
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_param
|
129
|
+
pivotal_id
|
130
|
+
end
|
131
|
+
|
132
|
+
def accepted?
|
133
|
+
accepted_at?
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def extract_tags_for_labels(labels)
|
139
|
+
Array.wrap(labels).reject(&:blank?).map { |label| label['name'] }
|
140
|
+
end
|
141
|
+
|
142
|
+
def extract_epics_for_labels(labels)
|
143
|
+
label_ids = Array.wrap(labels).reject(&:blank?).map { |label| label['id'] }
|
144
|
+
Epic.where.not(label_id: nil).where(label_id: label_ids)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/active_pivot.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
require "rails"
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
require "active_pivot/version"
|
6
|
+
require "active_pivot/api/paginated_collection"
|
7
|
+
require "active_pivot/api/config"
|
8
|
+
require "active_pivot/api/epic"
|
9
|
+
require "active_pivot/api/filter"
|
10
|
+
require "active_pivot/api/project"
|
11
|
+
require "active_pivot/api/request"
|
12
|
+
require "active_pivot/api/response"
|
13
|
+
require "active_pivot/api/activity"
|
14
|
+
require "active_pivot/api/story"
|
15
|
+
require "active_pivot/epic"
|
16
|
+
require "active_pivot/epic_story"
|
17
|
+
require "active_pivot/importer"
|
18
|
+
require "active_pivot/project"
|
19
|
+
require "active_pivot/story"
|
20
|
+
require "active_pivot/activity"
|
21
|
+
|
22
|
+
require "rake"
|
23
|
+
require "active_pivot/railtie" if defined?(Rails)
|
24
|
+
load "tasks/import.rake"
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ActivePivot
|
2
|
+
module Generators
|
3
|
+
class MigrationsGenerator < ::Rails::Generators::Base
|
4
|
+
def create_all
|
5
|
+
generate "migration", "create_pivotal_projects pivotal_id:integer name:text point_scale:text updated_at:datetime created_at:datetime"
|
6
|
+
generate "migration", "create_pivotal_epics project_id:integer:index pivotal_id:integer label_id:integer name:string url:string updated_at:datetime created_at:datetime"
|
7
|
+
generate "migration", "create_pivotal_stories pivotal_id:integer project_id:integer:index started_at:datetime accepted_at:datetime url:string estimate:integer name:text description:text kind:string story_type:string labels:text current_state:string tags:text[] updated_at:datetime created_at:datetime"
|
8
|
+
generate "migration", "create_pivotal_epic_stories story_id:integer:index epic_id:integer:index updated_at:datetime created_at:datetime"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
namespace :active_pivot do
|
2
|
+
namespace :import do
|
3
|
+
|
4
|
+
task :pivotal_update, [:interval] => :environment do |t, args|
|
5
|
+
updated_after = args[:interval].to_i.minutes.ago
|
6
|
+
puts "Updating since #{updated_after}"
|
7
|
+
ActivePivot::Importer.run(updated_after)
|
8
|
+
end
|
9
|
+
|
10
|
+
task pivotal_initial: :environment do
|
11
|
+
interval = 3.years.ago
|
12
|
+
ActivePivot::Importer.run(interval)
|
13
|
+
end
|
14
|
+
|
15
|
+
task :pivotal_date, [:interval] => :environment do |t, args|
|
16
|
+
begin
|
17
|
+
updated_after = DateTime.parse args[:interval]
|
18
|
+
puts "Updating since #{updated_after}"
|
19
|
+
ActivePivot::Importer.run(updated_after)
|
20
|
+
rescue
|
21
|
+
puts "Could not parse your start date"
|
22
|
+
puts "Enter a date such as 'August 12, 2015'"
|
23
|
+
puts "Example: rake active_import:import:pivotal_date['August 12, 2015']"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_pivot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Allan McLelland
|
8
|
+
- Jon Evans
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-08-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '3'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '3'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: httparty
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: bundler
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '1.10'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '1.10'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rake
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '10.0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '10.0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rspec
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
description:
|
85
|
+
email:
|
86
|
+
- awm@foraker.com
|
87
|
+
- jle@foraker.com
|
88
|
+
executables: []
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- ".gitignore"
|
93
|
+
- ".rspec"
|
94
|
+
- ".ruby-gemset"
|
95
|
+
- ".ruby-version"
|
96
|
+
- ".travis.yml"
|
97
|
+
- Gemfile
|
98
|
+
- LICENSE.txt
|
99
|
+
- README.md
|
100
|
+
- Rakefile
|
101
|
+
- active_pivot.gemspec
|
102
|
+
- bin/console
|
103
|
+
- bin/setup
|
104
|
+
- lib/active_pivot.rb
|
105
|
+
- lib/active_pivot/activity.rb
|
106
|
+
- lib/active_pivot/api/activity.rb
|
107
|
+
- lib/active_pivot/api/config.rb
|
108
|
+
- lib/active_pivot/api/epic.rb
|
109
|
+
- lib/active_pivot/api/filter.rb
|
110
|
+
- lib/active_pivot/api/paginated_collection.rb
|
111
|
+
- lib/active_pivot/api/project.rb
|
112
|
+
- lib/active_pivot/api/request.rb
|
113
|
+
- lib/active_pivot/api/response.rb
|
114
|
+
- lib/active_pivot/api/story.rb
|
115
|
+
- lib/active_pivot/epic.rb
|
116
|
+
- lib/active_pivot/epic_story.rb
|
117
|
+
- lib/active_pivot/importer.rb
|
118
|
+
- lib/active_pivot/pivotal.rb
|
119
|
+
- lib/active_pivot/project.rb
|
120
|
+
- lib/active_pivot/railtie.rb
|
121
|
+
- lib/active_pivot/story.rb
|
122
|
+
- lib/active_pivot/version.rb
|
123
|
+
- lib/generators/active_pivot/migrations_generator.rb
|
124
|
+
- lib/tasks/import.rake
|
125
|
+
homepage: http://www.foraker.com
|
126
|
+
licenses:
|
127
|
+
- MIT
|
128
|
+
metadata:
|
129
|
+
allowed_push_host: https://rubygems.org
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
requirements: []
|
145
|
+
rubyforge_project:
|
146
|
+
rubygems_version: 2.4.8
|
147
|
+
signing_key:
|
148
|
+
specification_version: 4
|
149
|
+
summary: Import all of your Pivotal projects and stories easily
|
150
|
+
test_files: []
|