clubhouse2 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|