active_pivot 0.1.0
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.
- 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: []
|