clubhouse2 0.0.1
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 +5 -0
- data/VERSION +1 -0
- data/clubhouse2.gemspec +31 -0
- data/lib/clubhouse2.rb +23 -0
- data/lib/clubhouse2/category.rb +9 -0
- data/lib/clubhouse2/client.rb +125 -0
- data/lib/clubhouse2/clubhouse_resource.rb +74 -0
- data/lib/clubhouse2/epic.rb +54 -0
- data/lib/clubhouse2/epic_comment.rb +15 -0
- data/lib/clubhouse2/exceptions.rb +55 -0
- data/lib/clubhouse2/file.rb +14 -0
- data/lib/clubhouse2/label.rb +15 -0
- data/lib/clubhouse2/linked_file.rb +14 -0
- data/lib/clubhouse2/member.rb +29 -0
- data/lib/clubhouse2/milestone.rb +22 -0
- data/lib/clubhouse2/ops-migration.rb +110 -0
- data/lib/clubhouse2/profile.rb +7 -0
- data/lib/clubhouse2/project.rb +28 -0
- data/lib/clubhouse2/repository.rb +11 -0
- data/lib/clubhouse2/state.rb +10 -0
- data/lib/clubhouse2/story.rb +112 -0
- data/lib/clubhouse2/story_comment.rb +15 -0
- data/lib/clubhouse2/story_link.rb +11 -0
- data/lib/clubhouse2/task.rb +18 -0
- data/lib/clubhouse2/team.rb +14 -0
- data/lib/clubhouse2/workflow.rb +26 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 55bfc748af3d711fd8f90a3594f1272d53b94544
|
4
|
+
data.tar.gz: 45a1626fc86b551ae3b53ebf230c64e099d49682
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0f3e1a806808f35df55b77564d5a82c041f6a1db89fa6781ec3506851a3fbb012cafd9192b1737e64ac1bb4746b4e0f07a7a858021196f5e4150e4dbc6610f61
|
7
|
+
data.tar.gz: a18e42aea643ef78213a2b8b4f035dba1cb5f13c62c8789746c6dc12132ca79dd0ad124b29a1bae2c3e518476bcbae0b3b16f5f0abd743e77aa672d910755f97
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/clubhouse2.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "clubhouse2"
|
7
|
+
spec.version = File.read(File.expand_path(File.dirname(__FILE__)) + '/VERSION')
|
8
|
+
spec.authors = ["James Denness"]
|
9
|
+
spec.email = ["jd@masabi.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{Clubhouse library for API version 2}
|
12
|
+
spec.description = %q{Does exactly what it says on the label}
|
13
|
+
spec.homepage = "https://www.masabi.com"
|
14
|
+
spec.license = "BSD"
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
17
|
+
# delete this section to allow pushing this gem to any host.
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
else
|
20
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
21
|
+
end
|
22
|
+
|
23
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
spec.bindir = 'bin'
|
25
|
+
spec.require_paths = [ 'lib' ]
|
26
|
+
|
27
|
+
spec.add_dependency 'http', '~> 1'
|
28
|
+
spec.add_dependency 'pry', '~> 0.10.4'
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
31
|
+
end
|
data/lib/clubhouse2.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'clubhouse2/clubhouse_resource.rb'
|
2
|
+
require 'clubhouse2/story_comment.rb'
|
3
|
+
require 'clubhouse2/epic_comment.rb'
|
4
|
+
require 'clubhouse2/linked_file.rb'
|
5
|
+
require 'clubhouse2/story_link.rb'
|
6
|
+
require 'clubhouse2/exceptions.rb'
|
7
|
+
require 'clubhouse2/milestone.rb'
|
8
|
+
require 'clubhouse2/category.rb'
|
9
|
+
require 'clubhouse2/workflow.rb'
|
10
|
+
require 'clubhouse2/project.rb'
|
11
|
+
require 'clubhouse2/profile.rb'
|
12
|
+
require 'clubhouse2/member.rb'
|
13
|
+
require 'clubhouse2/client.rb'
|
14
|
+
require 'clubhouse2/story.rb'
|
15
|
+
require 'clubhouse2/label.rb'
|
16
|
+
require 'clubhouse2/state.rb'
|
17
|
+
require 'clubhouse2/team.rb'
|
18
|
+
require 'clubhouse2/file.rb'
|
19
|
+
require 'clubhouse2/task.rb'
|
20
|
+
require 'clubhouse2/epic.rb'
|
21
|
+
|
22
|
+
module Clubhouse
|
23
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'http'
|
2
|
+
require 'uri'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Clubhouse
|
6
|
+
class Client
|
7
|
+
def initialize(api_key:, base_url: 'https://api.clubhouse.io/api/v2/')
|
8
|
+
@api_key = api_key
|
9
|
+
@base_url = base_url
|
10
|
+
@resources = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def url(endpoint)
|
14
|
+
URI.join(@base_url, endpoint,'?token=%s' % @api_key)
|
15
|
+
end
|
16
|
+
|
17
|
+
def api_request(method, *params)
|
18
|
+
puts [method, *params].join(', ')
|
19
|
+
response = HTTP.headers(content_type: 'application/json').send(method, *params)
|
20
|
+
case response.code
|
21
|
+
when 429
|
22
|
+
sleep 30
|
23
|
+
api_request(method, *params)
|
24
|
+
when 200
|
25
|
+
when 201
|
26
|
+
else
|
27
|
+
raise ClubhouseAPIError.new(response)
|
28
|
+
end
|
29
|
+
|
30
|
+
response
|
31
|
+
end
|
32
|
+
|
33
|
+
def flush(resource_class)
|
34
|
+
@resources[resource_class] = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def filter(object_array, args)
|
38
|
+
object_array.reject { |s| args.collect { |k, v| not [ *s.send(k) ].include? v }.reduce(:|) }
|
39
|
+
end
|
40
|
+
|
41
|
+
# or v.empty? if v.respond_to?(:empty?)
|
42
|
+
def create_object(resource_class, args)
|
43
|
+
this_class = Clubhouse::ClubhouseResource.subclass(resource_class)
|
44
|
+
this_class.validate(args)
|
45
|
+
flush(this_class)
|
46
|
+
new_params = args.compact.reject { |k, v| this_class.property_filter_create.include? k.to_sym }
|
47
|
+
response = api_request(:post, url(this_class.api_url), :json => new_params)
|
48
|
+
JSON.parse(response.to_s)
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_objects(resource_class, args = {})
|
52
|
+
this_class = Clubhouse::ClubhouseResource.subclass(resource_class)
|
53
|
+
unless @resources[this_class]
|
54
|
+
response = api_request(:get, url(this_class.api_url))
|
55
|
+
@resources[this_class] = JSON.parse(response.to_s).collect do |resource|
|
56
|
+
this_class.new(client: self, object: resource)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
filter(@resources[this_class], args)
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_object(resource_class, args = {})
|
64
|
+
get_objects(resource_class, args).first
|
65
|
+
end
|
66
|
+
|
67
|
+
# ---
|
68
|
+
|
69
|
+
def create_milestone(**args); create_object(:milestone, args); end
|
70
|
+
def milestones(**args); get_objects(:milestone, args); end
|
71
|
+
def milestone(**args); get_object(:milestone, args); end
|
72
|
+
|
73
|
+
def create_project(**args); create_object(:project, args); end
|
74
|
+
def projects(**args); get_objects(:project, args); end
|
75
|
+
def project(**args); get_object(:project, args); end
|
76
|
+
|
77
|
+
def create_story(**args); create_object(:story, args); end
|
78
|
+
def stories(**args)
|
79
|
+
filter(get_objects(:project).collect(&:stories).flatten, args)
|
80
|
+
end
|
81
|
+
|
82
|
+
def story(**args); stories(**args).first; end
|
83
|
+
|
84
|
+
def create_story_link(**args); create_object(:storylink, args); end
|
85
|
+
def story_links(**args)
|
86
|
+
filter(stories.collect(&:story_links).flatten, args)
|
87
|
+
end
|
88
|
+
|
89
|
+
def story_link(**args); story_links(**args).first; end
|
90
|
+
|
91
|
+
def create_member(**args); create_object(:member, args); end
|
92
|
+
def members(**args); get_objects(:member, args); end
|
93
|
+
def member(**args); get_object(:member, args); end
|
94
|
+
|
95
|
+
def create_team(**args); create_object(:team, args); end
|
96
|
+
def teams(**args); get_objects(:team, args); end
|
97
|
+
def team(**args); get_object(:team, args); end
|
98
|
+
|
99
|
+
def create_epic(**args); create_object(:epic, args); end
|
100
|
+
def epics(**args); get_objects(:epic, args); end
|
101
|
+
def epic(**args); get_object(:epic, args); end
|
102
|
+
|
103
|
+
def create_category(**args); create_object(:category, args); end
|
104
|
+
def categories(**args); get_objects(:category, args); end
|
105
|
+
def category(**args); get_object(:category, args); end
|
106
|
+
|
107
|
+
def create_label(**args); create_object(:label, args); end
|
108
|
+
def update_label(**args); update_object(:label, args); end
|
109
|
+
def labels(**args); get_objects(:label, args); end
|
110
|
+
def label(**args); get_object(:label, args); end
|
111
|
+
|
112
|
+
def create_file(**args); create_object(:file, args); end
|
113
|
+
def files(**args); get_objects(:file, args); end
|
114
|
+
def file(**args); get_object(:file, args); end
|
115
|
+
|
116
|
+
def create_linked_file(**args); create_object(:linkedfile, args); end
|
117
|
+
def linked_files(**args); get_objects(:linkedfile, args); end
|
118
|
+
def linked_file(**args); get_object(:linkedfile, args); end
|
119
|
+
|
120
|
+
def create_workflow(**args); create_object(:workflow, args); end
|
121
|
+
def workflows(**args); get_objects(:workflow, args); end
|
122
|
+
def workflow(**args); get_object(:workflow, args); end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class ClubhouseResource
|
3
|
+
@@subclasses = []
|
4
|
+
|
5
|
+
def self.inherited(other)
|
6
|
+
@@subclasses << other
|
7
|
+
end
|
8
|
+
|
9
|
+
# A list of properties to exlude from any create request
|
10
|
+
def self.property_filter_create
|
11
|
+
[
|
12
|
+
:archived, :days_to_thermometer, :entity_type, :id, :show_thermometer, :stats, :created_at, :updated_at,
|
13
|
+
:started_at, :completed_at, :comments, :position, :started, :project_ids, :completed, :blocker, :moved_at,
|
14
|
+
:task_ids, :files, :comment_ids, :workflow_state_id, :story_links, :mention_ids, :file_ids, :linked_file_ids
|
15
|
+
]
|
16
|
+
end
|
17
|
+
|
18
|
+
# A list of properties to exlude from any update request
|
19
|
+
def self.property_filter_update
|
20
|
+
self.property_filter_create
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.subclass(sub_class)
|
24
|
+
@@subclasses.find { |s| s.name == 'Clubhouse::%s' % sub_class.capitalize }
|
25
|
+
end
|
26
|
+
|
27
|
+
def api_url
|
28
|
+
self.class.api_url + "/#{@id}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.validate(args); end
|
32
|
+
|
33
|
+
def initialize(client:, object:)
|
34
|
+
@client = client
|
35
|
+
|
36
|
+
self.class.properties.each do |this_property|
|
37
|
+
self.class.class_eval { attr_accessor(this_property.to_sym) }
|
38
|
+
self.class.send(:define_method, (this_property.to_s + '=').to_sym) do |value|
|
39
|
+
update({ this_property => value })
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
set_properties(object)
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def set_properties(object)
|
48
|
+
object.each_pair do |k, v|
|
49
|
+
instance_variable_set('@' + k.to_s, v)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Empties resource cache
|
54
|
+
def flush
|
55
|
+
@client.flush(self.class)
|
56
|
+
end
|
57
|
+
|
58
|
+
def update(args = {})
|
59
|
+
new_params = to_h.merge(args).reject { |k, v| self.class.property_filter_update.include? k.to_sym }
|
60
|
+
validate(new_params)
|
61
|
+
flush
|
62
|
+
@client.api_request(:put, @client.url(api_url), :json => new_params)
|
63
|
+
end
|
64
|
+
|
65
|
+
def delete!
|
66
|
+
flush
|
67
|
+
@client.api_request(:delete, @client.url(api_url))
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_h
|
71
|
+
Hash[ (self.class.properties - self.class.property_filter_create).map { |name| [ name, instance_variable_get('@' + name.to_s) ] } ].compact
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Epic < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[
|
5
|
+
:archived, :comments, :completed, :completed_at, :completed_at_override, :created_at, :deadline, :description,
|
6
|
+
:entity_type, :external_id, :follower_ids, :id, :labels, :milestone_id, :name, :owner_ids, :position, :project_ids,
|
7
|
+
:started, :started_at, :started_at_override, :state, :stats, :updated_at
|
8
|
+
]
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.api_url
|
12
|
+
'epics'
|
13
|
+
end
|
14
|
+
|
15
|
+
def stories
|
16
|
+
@client.projects.collect(&:stories).reduce(:+).select { |s| s.epic_id == @id }
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate(args)
|
20
|
+
raise NoSuchTeam.NoSuchTeam(args[:team_id]) unless @client.get_team(id: args[:team_id])
|
21
|
+
|
22
|
+
(args[:follower_ids] || []).each do |this_member|
|
23
|
+
raise NoSuchMember.NoSuchMember(this_member) unless @client.get_member(id: this_member)
|
24
|
+
end
|
25
|
+
|
26
|
+
(args[:owner_ids] || []).each do |this_member|
|
27
|
+
raise NoSuchMember.NoSuchMember(this_member) unless @client.get_member(id: this_member)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def comments(**args)
|
32
|
+
# The API is missing a parent epic ID property, so we need to fake it here
|
33
|
+
args[:epic_id] = @id
|
34
|
+
@comments ||= JSON.parse(@client.api_request(:get, @client.url("#{api_url}/#{Epiccomment.api_url}"))).collect { |task| Epiccomment.new(client: @client, object: comment) }
|
35
|
+
@comments.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_h
|
39
|
+
super.merge({
|
40
|
+
comments: @comments.collect(&:to_h)
|
41
|
+
})
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_comment(**args)
|
45
|
+
Task.validate(args)
|
46
|
+
response = JSON.parse(@client.api_request(:post, @client.url("#{api_url}/#{Epiccomment.api_url}"), :json => args))
|
47
|
+
raise ClubhouseAPIError.new(response) unless response.code == 201
|
48
|
+
@comments = nil
|
49
|
+
response
|
50
|
+
end
|
51
|
+
|
52
|
+
def comment(**args); comments(args).first; end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Epiccomment < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[ :author_id, :comments, :created_at, :entity_type, :external_id, :id, :mention_ids, :position, :text, :updated_at, :epic_id ]
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.api_url
|
8
|
+
'comments'
|
9
|
+
end
|
10
|
+
|
11
|
+
def api_url
|
12
|
+
"#{self.api_url}/#{@epic_id}/#{id}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class ClubhouseValidationError < StandardError
|
3
|
+
def initialize(message)
|
4
|
+
super('validation error: %s' % message)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class ClubhouseAPIError < StandardError
|
9
|
+
def initialize(response)
|
10
|
+
super('api error (%d): %s' % [ response.code, response.to_s ])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class NoSuchMember < ClubhouseValidationError
|
15
|
+
def initialize(member)
|
16
|
+
super('no such member (%s)' % member)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class NoSuchFile < ClubhouseValidationError
|
21
|
+
def initialize(file)
|
22
|
+
super('no such file (%s)' % file)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class NoSuchLinkedFile < ClubhouseValidationError
|
27
|
+
def initialize(file)
|
28
|
+
super('no such linked file (%s)' % file)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class NoSuchTeam < ClubhouseValidationError
|
33
|
+
def initialize(team)
|
34
|
+
super('no such team (%s)' % team)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class NoSuchProject < ClubhouseValidationError
|
39
|
+
def initialize(project)
|
40
|
+
super('no such project (%s)' % project)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class NoSuchMilestone < ClubhouseValidationError
|
45
|
+
def initialize(milestone)
|
46
|
+
super('no such milestone (%s)' % milestone)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class NoSuchEpic < ClubhouseValidationError
|
51
|
+
def initialize(epic)
|
52
|
+
super('no such epic (%s)' % epic)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class File < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[
|
5
|
+
:content_type, :created_at, :description, :entity_type, :external_id, :filename, :id, :mention_ids, :name,
|
6
|
+
:size, :story_ids, :thumbnail_url, :updated_at, :uploader_id, :url
|
7
|
+
]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.api_url
|
11
|
+
'files'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Label < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[ :archived, :color, :created_at, :entity_type, :external_id, :id, :name, :stats, :updated_at ]
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.api_url
|
8
|
+
'labels'
|
9
|
+
end
|
10
|
+
|
11
|
+
def stories
|
12
|
+
@client.projects.collect(&:stories).reduce(:+).select { |s| s.labels.collect(&:id).include? @id }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Linkedfile < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[
|
5
|
+
:content_type, :created_at, :description, :entity_type, :id, :mention_ids, :name,
|
6
|
+
:size, :story_ids, :thumbnail_url, :type, :updated_at, :uploader_id
|
7
|
+
]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.api_url
|
11
|
+
'linkedfiles'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Member < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[ :created_at, :disabled, :id, :profile, :role, :updated_at ]
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(client:, object:)
|
8
|
+
super
|
9
|
+
@profile = Profile.new(client: client, object: @profile)
|
10
|
+
|
11
|
+
# Create accessors for profile properties
|
12
|
+
Profile.properties.each do |property|
|
13
|
+
self.class.send(:define_method, (property.to_sym)) { @profile.send(property) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.api_url
|
18
|
+
'members'
|
19
|
+
end
|
20
|
+
|
21
|
+
def stories_requested
|
22
|
+
@client.projects.collect(&:stories).reduce(:+).select { |s| s.requested_by_id == @id }
|
23
|
+
end
|
24
|
+
|
25
|
+
def stories_following
|
26
|
+
@client.projects.collect(&:stories).reduce(:+).select { |s| s.follower_ids.include? @id }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Milestone < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[
|
5
|
+
:categories, :completed, :completed_at, :completed_at_override, :created_at, :description, :entity_type,
|
6
|
+
:id, :name, :position, :started, :started_at, :started_at_override, :state, :updated_at
|
7
|
+
]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.api_url
|
11
|
+
'milestones'
|
12
|
+
end
|
13
|
+
|
14
|
+
def epics
|
15
|
+
@client.epics.select { |e| e.milestone_id == @id }
|
16
|
+
end
|
17
|
+
|
18
|
+
def stories
|
19
|
+
epics.collect(&:stories).reduce(:+)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'clubhouse2'
|
2
|
+
|
3
|
+
source_account = Clubhouse::Client.new(api_key: 'source_account_api_key')
|
4
|
+
target_account = Clubhouse::Client.new(api_key: 'target_account_api_key')
|
5
|
+
|
6
|
+
resolve_map = {}
|
7
|
+
|
8
|
+
# Map of workflows which don't directly relate to workflows in the other account
|
9
|
+
resolve_map[:workflow] = {
|
10
|
+
'Unscheduled' => 'Backlog',
|
11
|
+
'Ready for Deployment' => 'Awaiting UAT Deployment',
|
12
|
+
'Waiting for other team' => 'Blocked',
|
13
|
+
'Completed' => 'Done',
|
14
|
+
'Rejected' => 'Icebox'
|
15
|
+
}
|
16
|
+
|
17
|
+
# Map of members which aren't named the same in both accounts
|
18
|
+
resolve_map[:member] = {
|
19
|
+
'James' => 'James Denness',
|
20
|
+
}
|
21
|
+
|
22
|
+
# This method looks up the corresponding ID of a resource in the other account, by matching its name.
|
23
|
+
def resolve(source_client, target_client, resolve_map, type, id)
|
24
|
+
item = source_client.send(type, id: id)
|
25
|
+
if resolve_map[type]
|
26
|
+
counterpart = resolve_map[type][item.name] || item.name
|
27
|
+
else
|
28
|
+
counterpart = item.name
|
29
|
+
end
|
30
|
+
target_client.send(type, name: counterpart)&.id
|
31
|
+
end
|
32
|
+
|
33
|
+
# We need to create the empty projects before we can create epics
|
34
|
+
source_account.projects.each do |this_project|
|
35
|
+
target_account.create_project(
|
36
|
+
this_project.to_h.merge({
|
37
|
+
follower_ids: this_project.follower_ids.collect { |f| resolve(source_account, target_account, resolve_map, :member, f) }
|
38
|
+
})
|
39
|
+
) unless resolve(source_account, target_account, resolve_map, :project, this_project.id)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create labels
|
43
|
+
source_account.labels.each do |this_label|
|
44
|
+
target_account.create_label(this_label.to_h) unless resolve(source_account, target_account, resolve_map, :label, this_label.id)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create categories
|
48
|
+
source_account.categories.each do |this_category|
|
49
|
+
target_account.create_category(this_category.to_h) unless resolve(source_account, target_account, resolve_map, :category, this_category.id)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Create milestones
|
53
|
+
source_account.milestones.each do |this_milestone|
|
54
|
+
new_milestone = target_account.create_milestone(
|
55
|
+
this_milestone.to_h.merge({
|
56
|
+
labels: this_milestone.categories.collect { |c| resolve(source_account, target_account, resolve_map, :category, c ) }
|
57
|
+
})
|
58
|
+
) unless target_account.milestone(name: this_milestone.name)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Create epics
|
62
|
+
source_account.epics.each do |this_epic|
|
63
|
+
new_epic = target_account.create_epic(
|
64
|
+
this_epic.to_h.merge({
|
65
|
+
follower_ids: this_epic.follower_ids.collect { |f| resolve(source_account, target_account, resolve_map, :member, f) },
|
66
|
+
owner_ids: this_epic.owner_ids.collect { |o| resolve(source_account, target_account, resolve_map, :member, o) },
|
67
|
+
labels: this_epic.labels.collect { |l| resolve(source_account, target_account, resolve_map, :label, l ) },
|
68
|
+
started_at_override: this_epic.started_at,
|
69
|
+
completed_at_override: this_epic.completed_at,
|
70
|
+
milestone_id: resolve(source_account, target_account, resolve_map, :milestone, this_epic.milestone_id)
|
71
|
+
})
|
72
|
+
) unless target_account.epic(name: this_epic.name)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Fill the projects with stories
|
76
|
+
source_account.projects.each do |this_project|
|
77
|
+
target_account_project = target_account.project(id: resolve(source_account, target_account, resolve_map, :project, this_project.id))
|
78
|
+
this_project.stories.each do |this_story|
|
79
|
+
new_story = target_account.create_story(
|
80
|
+
this_story.to_h.merge({
|
81
|
+
project_id: target_account_project.id,
|
82
|
+
epic_id: (resolve(source_account, target_account, resolve_map, :epic, this_story.epic_id) if this_story.epic_id),
|
83
|
+
follower_ids: this_story.follower_ids.collect { |f| resolve(source_account, target_account, resolve_map, :member, f) },
|
84
|
+
owner_ids: this_story.owner_ids.collect { |o| resolve(source_account, target_account, resolve_map, :member, o) },
|
85
|
+
requested_by_id: resolve(source_account, target_account, resolve_map, :member, this_story.requested_by_id),
|
86
|
+
})
|
87
|
+
) unless target_account_project.story(name: this_story.name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Restore story links
|
92
|
+
source_account.story_links.each do |this_link|
|
93
|
+
begin
|
94
|
+
target_account.create_story_link(
|
95
|
+
this_link.to_h.merge({
|
96
|
+
object_id: resolve(source_account, target_account, resolve_map, :story, this_link.object_id),
|
97
|
+
subject_id: resolve(source_account, target_account, resolve_map, :story, this_link.subject_id)
|
98
|
+
})
|
99
|
+
) unless target_account.story_link(object_id: resolve(source_account, target_account, resolve_map, :story, this_link.object_id), subject_id: resolve(source_account, target_account, resolve_map, :story, this_link.subject_id))
|
100
|
+
rescue Clubhouse::ClubhouseAPIError => e
|
101
|
+
if e.message[/duplicated/]
|
102
|
+
next
|
103
|
+
else
|
104
|
+
raise e
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Project < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[
|
5
|
+
:abbreviation, :archived, :color, :created_at, :days_to_thermometer, :description, :entity_type, :external_id,
|
6
|
+
:follower_ids, :id, :iteration_length, :name, :show_thermometer, :start_time, :stats, :updated_at
|
7
|
+
]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.api_url
|
11
|
+
'projects'
|
12
|
+
end
|
13
|
+
|
14
|
+
def stories(**args)
|
15
|
+
@stories ||= JSON.parse(@client.api_request(:get, @client.url("#{api_url}/stories"))).collect { |story| Story.new(client: @client, object: story) }
|
16
|
+
@stories.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_story(**args)
|
20
|
+
@stories = nil
|
21
|
+
args[:project_id] = @id
|
22
|
+
Story.validate(**args)
|
23
|
+
@client.create_object(:story, args)
|
24
|
+
end
|
25
|
+
|
26
|
+
def story(**args); stories(args).first; end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class State < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[
|
5
|
+
:categories, :completed, :completed_at, :completed_at_override, :created_at, :description, :entity_type,
|
6
|
+
:id, :name, :position, :started, :started_at, :started_at_override, :state, :updated_at
|
7
|
+
]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Story < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[
|
5
|
+
:archived, :blocker, :blocker, :comment_ids, :completed, :completed_at, :completed_at_override, :created_at,
|
6
|
+
:deadline, :entity_type, :epic_id, :estimate, :external_id, :file_ids, :follower_ids, :id,
|
7
|
+
:linked_file_ids, :moved_at, :name, :owner_ids, :position, :project_id, :requested_by_id, :started,
|
8
|
+
:started_at, :started_at_override, :story_type, :task_ids, :updated_at, :workflow_state_id,
|
9
|
+
]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.api_url
|
13
|
+
'stories'
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate(**args)
|
17
|
+
raise NoSuchEpic.new(args[:epic_id]) unless @client.epic(id: args[:epic_id]) if args[:epic_id]
|
18
|
+
raise NoSuchProject.new(args[:project_id]) unless @client.project(id: args[:project_id]) if args[:project_id]
|
19
|
+
raise NoSuchMember.new(args[:requested_by_id]) unless @client.member(id: args[:requested_by_id]) if args[:requested_by_id]
|
20
|
+
|
21
|
+
(args[:follower_ids] || []).each do |this_member|
|
22
|
+
raise NoSuchMember.new(this_member) unless @client.member(id: this_member)
|
23
|
+
end
|
24
|
+
|
25
|
+
(args[:owner_ids] || []).each do |this_member|
|
26
|
+
raise NoSuchMember.new(this_member) unless @client.member(id: this_member)
|
27
|
+
end
|
28
|
+
|
29
|
+
(args[:file_ids] || []).each do |this_file|
|
30
|
+
raise NoSuchFile.new(this_file) unless @client.file(id: this_file)
|
31
|
+
end
|
32
|
+
|
33
|
+
(args[:linked_file_ids] || []).each do |this_linked_file|
|
34
|
+
raise NoSuchLinkedFile.new(this_linked_file) unless @client.linked_file(id: this_linked_file)
|
35
|
+
end
|
36
|
+
|
37
|
+
(args[:story_links] || []).each do |this_linked_story|
|
38
|
+
raise NoSuchLinkedStory.new(this_linked_story) unless @client.story(id: this_linked_story['subject_id'])
|
39
|
+
end
|
40
|
+
|
41
|
+
(args[:labels] || []).collect! do |this_label|
|
42
|
+
this_label.is_a? Label ? this_label : @client.label(id: this_label['name'])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_task(**args)
|
47
|
+
Task.validate(**args)
|
48
|
+
@tasks = nil
|
49
|
+
JSON.parse(@client.api_request(:post, @client.url("#{api_url}/#{Task.api_url}"), :json => args))
|
50
|
+
end
|
51
|
+
|
52
|
+
def comments(**args)
|
53
|
+
@comments ||= @comment_ids.collect do |this_comment_id|
|
54
|
+
comment_data = JSON.parse(@client.api_request(:get, @client.url("#{api_url}/comments/#{this_comment_id}")))
|
55
|
+
Storycomment.new(client: @client, object: comment_data)
|
56
|
+
end
|
57
|
+
|
58
|
+
@comments.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def story_links(**args)
|
62
|
+
@story_link_objects ||= @story_links.collect do |this_story_link|
|
63
|
+
link_data = JSON.parse(@client.api_request(:get, @client.url("#{Storylink.api_url}/#{this_story_link['id']}")))
|
64
|
+
link_data.reject { |k, v| v == @id}
|
65
|
+
Storylink.new(client: @client, object: link_data)
|
66
|
+
end
|
67
|
+
|
68
|
+
@story_link_objects.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def tasks(**args)
|
72
|
+
@tasks ||= @task_ids.collect do |this_task_id|
|
73
|
+
task_data = JSON.parse(@client.api_request(:get, @client.url("#{api_url}/tasks/#{this_task_id}")))
|
74
|
+
Task.new(client: @client, object: task_data)
|
75
|
+
end
|
76
|
+
|
77
|
+
@tasks.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def linked_files(**args)
|
81
|
+
@client.linked_files(story_ids: @id, **args)
|
82
|
+
end
|
83
|
+
|
84
|
+
def files(**args)
|
85
|
+
@client.files(story_ids: @id, **args)
|
86
|
+
end
|
87
|
+
|
88
|
+
def labels(**args)
|
89
|
+
@labels.collect { |l| @client.label(id: l['id'], **args) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_h
|
93
|
+
super.merge({
|
94
|
+
comments: [ *comments ].collect(&:to_h),
|
95
|
+
tasks: [ *tasks ].collect(&:to_h),
|
96
|
+
files: [ *files ].collect(&:to_h),
|
97
|
+
story_links: [ *story_links ].collect(&:to_h),
|
98
|
+
labels: [ *labels ].collect(&:to_h),
|
99
|
+
})
|
100
|
+
end
|
101
|
+
|
102
|
+
def create_comment(**args)
|
103
|
+
Task.validate(**args)
|
104
|
+
@comments = nil
|
105
|
+
JSON.parse(@client.api_request(:post, @client.url("#{api_url}/#{Storycomment.api_url}"), :json => args))
|
106
|
+
end
|
107
|
+
|
108
|
+
def comment(**args); comments(args).first; end
|
109
|
+
def story_link(**args); story_links(args).first; end
|
110
|
+
def task(**args); tasks(args).first; end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Storycomment < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[ :author_id, :comments, :created_at, :entity_type, :external_id, :id, :mention_ids, :position, :story_id, :text, :updated_at ]
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.api_url
|
8
|
+
'comments'
|
9
|
+
end
|
10
|
+
|
11
|
+
def api_url
|
12
|
+
"#{self.api_url}/#{@story_id}/#{id}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Task < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[
|
5
|
+
:complete, :completed_at, :created_at, :description, :entity_type, :external_id, :id, :mention_ids, :owner_ids,
|
6
|
+
:position, :story_id, :updated_at
|
7
|
+
]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.api_url
|
11
|
+
'tasks'
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
super.reject { |k, v| [ :story_id ].include? k }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Team < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[
|
5
|
+
:created_at, :description, :entity_type, :id, :name, :position, :project_ids, :updated_at, :workflow,
|
6
|
+
:team_id, :updated_at
|
7
|
+
]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.api_url
|
11
|
+
'teams'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Clubhouse
|
2
|
+
class Workflow < ClubhouseResource
|
3
|
+
def self.properties
|
4
|
+
[ :created_at, :default_state_id, :description, :entity_type, :id, :name, :states, :team_id, :updated_at ]
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(client:, object:)
|
8
|
+
super
|
9
|
+
@states = []
|
10
|
+
object['states'].each do |this_state|
|
11
|
+
this_state[:workflow_id] = @id
|
12
|
+
@states << State.new(client: client, object: this_state)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.api_url
|
17
|
+
'workflows'
|
18
|
+
end
|
19
|
+
|
20
|
+
def states(**args)
|
21
|
+
@states.reject { |s| args.collect { |k,v| s.send(k) != v }.reduce(:|) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def state(**args); states(args).first; end
|
25
|
+
end
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clubhouse2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Denness
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: http
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.10.4
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.10.4
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.10'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.10'
|
55
|
+
description: Does exactly what it says on the label
|
56
|
+
email:
|
57
|
+
- jd@masabi.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- VERSION
|
64
|
+
- clubhouse2.gemspec
|
65
|
+
- lib/clubhouse2.rb
|
66
|
+
- lib/clubhouse2/category.rb
|
67
|
+
- lib/clubhouse2/client.rb
|
68
|
+
- lib/clubhouse2/clubhouse_resource.rb
|
69
|
+
- lib/clubhouse2/epic.rb
|
70
|
+
- lib/clubhouse2/epic_comment.rb
|
71
|
+
- lib/clubhouse2/exceptions.rb
|
72
|
+
- lib/clubhouse2/file.rb
|
73
|
+
- lib/clubhouse2/label.rb
|
74
|
+
- lib/clubhouse2/linked_file.rb
|
75
|
+
- lib/clubhouse2/member.rb
|
76
|
+
- lib/clubhouse2/milestone.rb
|
77
|
+
- lib/clubhouse2/ops-migration.rb
|
78
|
+
- lib/clubhouse2/profile.rb
|
79
|
+
- lib/clubhouse2/project.rb
|
80
|
+
- lib/clubhouse2/repository.rb
|
81
|
+
- lib/clubhouse2/state.rb
|
82
|
+
- lib/clubhouse2/story.rb
|
83
|
+
- lib/clubhouse2/story_comment.rb
|
84
|
+
- lib/clubhouse2/story_link.rb
|
85
|
+
- lib/clubhouse2/task.rb
|
86
|
+
- lib/clubhouse2/team.rb
|
87
|
+
- lib/clubhouse2/workflow.rb
|
88
|
+
homepage: https://www.masabi.com
|
89
|
+
licenses:
|
90
|
+
- BSD
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 2.6.10
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: Clubhouse library for API version 2
|
112
|
+
test_files: []
|