redpomo-reloaded 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Dockerfile +5 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +139 -0
- data/Guardfile +23 -0
- data/LICENSE +22 -0
- data/README.md +176 -0
- data/Rakefile +6 -0
- data/bin/redpomo +7 -0
- data/docker-compose.yml +7 -0
- data/lib/redpomo.rb +10 -0
- data/lib/redpomo/cli.rb +172 -0
- data/lib/redpomo/config.rb +26 -0
- data/lib/redpomo/entries_printer.rb +33 -0
- data/lib/redpomo/entry.rb +65 -0
- data/lib/redpomo/file_cache.rb +44 -0
- data/lib/redpomo/fuzzy_converter.rb +68 -0
- data/lib/redpomo/issue.rb +38 -0
- data/lib/redpomo/null_cache.rb +9 -0
- data/lib/redpomo/numeric_ext.rb +30 -0
- data/lib/redpomo/task.rb +103 -0
- data/lib/redpomo/task_list.rb +61 -0
- data/lib/redpomo/templates/config.yml +47 -0
- data/lib/redpomo/templates/issue_stub.textile +7 -0
- data/lib/redpomo/tracker.rb +175 -0
- data/lib/redpomo/ui.rb +73 -0
- data/lib/redpomo/version.rb +3 -0
- data/redpomo.gemspec +33 -0
- data/spec/file_cache_spec.rb +22 -0
- data/spec/fixtures/add_results.txt +4 -0
- data/spec/fixtures/cassettes/cli_add.yml +102 -0
- data/spec/fixtures/cassettes/cli_close.yml +50 -0
- data/spec/fixtures/cassettes/cli_pull.yml +1222 -0
- data/spec/fixtures/cassettes/cli_push.yml +297 -0
- data/spec/fixtures/cassettes/close_issue.yml +50 -0
- data/spec/fixtures/cassettes/create_issue.yml +102 -0
- data/spec/fixtures/cassettes/issues.yml +449 -0
- data/spec/fixtures/cassettes/push_entry.yml +55 -0
- data/spec/fixtures/close_results.txt +2 -0
- data/spec/fixtures/config.yml +16 -0
- data/spec/fixtures/printer_output.txt +16 -0
- data/spec/fixtures/proper_timelog.csv +4 -0
- data/spec/fixtures/pull_results.txt +20 -0
- data/spec/fixtures/tasks.txt +3 -0
- data/spec/fixtures/timelog.csv +6 -0
- data/spec/integration/add_spec.rb +29 -0
- data/spec/integration/init_spec.rb +33 -0
- data/spec/lib/redpomo/cli_spec.rb +91 -0
- data/spec/lib/redpomo/entry_spec.rb +23 -0
- data/spec/lib/redpomo/fuzzy_converter_spec.rb +65 -0
- data/spec/lib/redpomo/task_spec.rb +39 -0
- data/spec/lib/redpomo/tracker_spec.rb +72 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/cli_helpers.rb +76 -0
- data/spec/support/fixtures.rb +24 -0
- data/spec/support/ruby_ext.rb +20 -0
- data/spec/tmp/REDME.md +0 -0
- metadata +296 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
---
|
2
|
+
todo: "~/Documents/todo/todo.txt" # Path of your local Todo.txt
|
3
|
+
|
4
|
+
trackers:
|
5
|
+
|
6
|
+
my_tracker: # This is the name of the context you want
|
7
|
+
# to use for this tracker (--> @my_tracker)
|
8
|
+
|
9
|
+
url: "http://my_tracker.com" # The URL of your Redmine instance
|
10
|
+
|
11
|
+
token: "" # Find this at the following URL:
|
12
|
+
# http://mytracker.com/my/account
|
13
|
+
|
14
|
+
default_activity_id: 9 # Choose a timetrack activity ID from one
|
15
|
+
# of those specified at:
|
16
|
+
# http://mytracker.com/enumerations
|
17
|
+
#
|
18
|
+
# -- 9 is "Development" on clean Redmine
|
19
|
+
|
20
|
+
default_project_id: "project" # If timetracks you're pushing don't have
|
21
|
+
# a +project specified, use this.
|
22
|
+
|
23
|
+
default_priority_id: 4 # If issues you're adding don't have a
|
24
|
+
# priority specified, then use this.
|
25
|
+
#
|
26
|
+
# -- 4 is "Normal" on clean Redmine
|
27
|
+
|
28
|
+
closed_status_id: 5 # Identifier of the status used to mark
|
29
|
+
# issues as closed. Find this ID here:
|
30
|
+
# http://mytracker.com/issue_statuses
|
31
|
+
#
|
32
|
+
# -- 5 is "Closed" on clean Redmine
|
33
|
+
|
34
|
+
priority_ids: # Map a Todo.txt priority with a Redmine
|
35
|
+
# priority ID.
|
36
|
+
|
37
|
+
- 7 # -> (A)
|
38
|
+
- 6 # -> (B)
|
39
|
+
- 5 # -> (C)
|
40
|
+
# ... and so on
|
41
|
+
#
|
42
|
+
# Find those IDs at the following URL:
|
43
|
+
# http://mytracker.com/enumerations
|
44
|
+
#
|
45
|
+
# -- 7, 6 and 5 are "Immediate", "Urgent"
|
46
|
+
# and "High" on clean Redmine
|
47
|
+
|
@@ -0,0 +1,7 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
# Please enter the description of your new task on the first line.
|
4
|
+
# Following lines will be used as issue descripton (textile allowed).
|
5
|
+
#
|
6
|
+
# Make sure to add a project and context. Lines starting with '#'
|
7
|
+
# will be ignored, and an empty message aborts the creation.
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'active_support/core_ext/hash'
|
2
|
+
require 'redpomo/issue'
|
3
|
+
require 'redpomo/config'
|
4
|
+
|
5
|
+
require 'rest_client'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module Redpomo
|
9
|
+
class Tracker
|
10
|
+
|
11
|
+
def self.find(name)
|
12
|
+
if data = Config.trackers_data[name.to_sym]
|
13
|
+
Tracker.new(name.to_sym, data)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.all
|
18
|
+
Config.trackers_data.map do |name, data|
|
19
|
+
Tracker.new(name.to_sym, data)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :name, :base_url
|
24
|
+
|
25
|
+
def initialize(name, options)
|
26
|
+
options.symbolize_keys!
|
27
|
+
@name = name
|
28
|
+
@base_url = options[:url]
|
29
|
+
@api_key = options[:token]
|
30
|
+
@default_project_id = options[:default_project_id]
|
31
|
+
@default_priority_id = options[:default_priority_id]
|
32
|
+
@closed_status_id = options[:closed_status_id].to_i
|
33
|
+
@default_activity_id = options[:default_activity_id]
|
34
|
+
@priorities = options[:priority_ids]
|
35
|
+
end
|
36
|
+
|
37
|
+
def todo_priority(priority_name)
|
38
|
+
return nil if @priorities.nil?
|
39
|
+
alphabet = ('A' .. 'Z').to_a.join
|
40
|
+
if index = @priorities.index(priority_name)
|
41
|
+
"(#{alphabet[index]})"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def issue_priority_id(priority_val)
|
46
|
+
if index = ('A' .. 'Z').to_a.index(priority_val)
|
47
|
+
@priorities[index]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def issues
|
52
|
+
data = get("/issues", assigned_to_id: current_user_id, status_id: "open", limit: 100)
|
53
|
+
data["issues"].map do |issue|
|
54
|
+
issue["project_id"] = project_identifier_for(issue["project"]["id"])
|
55
|
+
Issue.new(self, issue)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_issue!(issue)
|
60
|
+
data = {
|
61
|
+
subject: issue.subject,
|
62
|
+
description: issue.description,
|
63
|
+
due_date: issue.due_date,
|
64
|
+
project_id: issue.project_id || @default_project_id,
|
65
|
+
priority_id: issue.priority_id || @default_priority_id,
|
66
|
+
assigned_to_id: current_user_id
|
67
|
+
}
|
68
|
+
|
69
|
+
post("/issues", issue: data)
|
70
|
+
end
|
71
|
+
|
72
|
+
def pushable_entry?(entry)
|
73
|
+
data = entry_data(entry)
|
74
|
+
if data[:project_id]
|
75
|
+
project_exists?(data[:project_id])
|
76
|
+
else
|
77
|
+
issue_exists?(data[:issue_id])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def push_entry!(entry)
|
82
|
+
post("/time_entries", time_entry: entry_data(entry))
|
83
|
+
end
|
84
|
+
|
85
|
+
def close_issue!(id, message = nil)
|
86
|
+
issue = { status_id: @closed_status_id }
|
87
|
+
issue[:notes] = message if message.present?
|
88
|
+
put("/issues/#{id}", issue: issue)
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def project_exists?(project_id)
|
94
|
+
project_identifier_for(project_id).present?
|
95
|
+
end
|
96
|
+
|
97
|
+
def issue_exists?(issue_id)
|
98
|
+
issue_identifier_for(issue_id).present?
|
99
|
+
end
|
100
|
+
|
101
|
+
def entry_data(entry)
|
102
|
+
task = entry.to_task
|
103
|
+
time_entry = {}
|
104
|
+
|
105
|
+
if issue = task.issue
|
106
|
+
time_entry[:issue_id] = issue
|
107
|
+
elsif project = task.project
|
108
|
+
time_entry[:project_id] = project
|
109
|
+
else
|
110
|
+
time_entry[:project_id] = @default_project_id
|
111
|
+
end
|
112
|
+
|
113
|
+
time_entry[:spent_on] = entry.datetime
|
114
|
+
time_entry[:hours] = entry.duration / 3600.0
|
115
|
+
time_entry[:comments] = task.text
|
116
|
+
|
117
|
+
if @default_activity_id.present?
|
118
|
+
time_entry[:activity_id] = @default_activity_id.to_i
|
119
|
+
end
|
120
|
+
|
121
|
+
time_entry
|
122
|
+
end
|
123
|
+
|
124
|
+
def issue_identifier_for(issue_id)
|
125
|
+
Config.cache.get("#{@name}:issue:#{issue_id}:identifier") do
|
126
|
+
data = get("/issues/#{issue_id}")
|
127
|
+
data["issue"]["id"]
|
128
|
+
end
|
129
|
+
rescue RestClient::ResourceNotFound => e
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
|
133
|
+
def project_identifier_for(project_id)
|
134
|
+
Config.cache.get("#{@name}:project:#{project_id}:identifier") do
|
135
|
+
data = get("/projects/#{project_id}")
|
136
|
+
data["project"]["identifier"]
|
137
|
+
end
|
138
|
+
rescue RestClient::ResourceNotFound => e
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
|
142
|
+
def current_user_id
|
143
|
+
get("/users/current")["user"]["id"]
|
144
|
+
end
|
145
|
+
|
146
|
+
def get(url, params = {})
|
147
|
+
request(:get, url, params)
|
148
|
+
end
|
149
|
+
|
150
|
+
def post(url, data)
|
151
|
+
request(:post, url, body: data)
|
152
|
+
end
|
153
|
+
|
154
|
+
def put(url, data)
|
155
|
+
request(:put, url, body: data)
|
156
|
+
end
|
157
|
+
|
158
|
+
def request(type, url, params = {})
|
159
|
+
args = []
|
160
|
+
args << @base_url + url + ".json"
|
161
|
+
args << JSON.generate(params.delete(:body)) unless type == :get
|
162
|
+
args << {
|
163
|
+
accept: :json,
|
164
|
+
content_type: :json,
|
165
|
+
params: params.merge(key: @api_key)
|
166
|
+
}
|
167
|
+
parse RestClient.send(type, *args)
|
168
|
+
end
|
169
|
+
|
170
|
+
def parse(json)
|
171
|
+
json && json.length >= 2 ? ::JSON.parse(json) : nil
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|
data/lib/redpomo/ui.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module Redpomo
|
2
|
+
|
3
|
+
class UI
|
4
|
+
def warn(message, newline = nil)
|
5
|
+
end
|
6
|
+
|
7
|
+
def debug(message, newline = nil)
|
8
|
+
end
|
9
|
+
|
10
|
+
def error(message, newline = nil)
|
11
|
+
end
|
12
|
+
|
13
|
+
def info(message, newline = nil)
|
14
|
+
end
|
15
|
+
|
16
|
+
def confirm(message, newline = nil)
|
17
|
+
end
|
18
|
+
|
19
|
+
def debug?
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
class Shell < UI
|
24
|
+
attr_writer :shell
|
25
|
+
|
26
|
+
def initialize(shell)
|
27
|
+
@shell = shell
|
28
|
+
@quiet = false
|
29
|
+
@debug = ENV['DEBUG']
|
30
|
+
end
|
31
|
+
|
32
|
+
def info(msg, newline = nil)
|
33
|
+
tell_me(msg, nil, newline) if !@quiet
|
34
|
+
end
|
35
|
+
|
36
|
+
def confirm(msg, newline = nil)
|
37
|
+
tell_me(msg, :green, newline) if !@quiet
|
38
|
+
end
|
39
|
+
|
40
|
+
def warn(msg, newline = nil)
|
41
|
+
tell_me(msg, :yellow, newline)
|
42
|
+
end
|
43
|
+
|
44
|
+
def error(msg, newline = nil)
|
45
|
+
tell_me(msg, :red, newline)
|
46
|
+
end
|
47
|
+
|
48
|
+
def be_quiet!
|
49
|
+
@quiet = true
|
50
|
+
end
|
51
|
+
|
52
|
+
def debug?
|
53
|
+
# needs to be false instead of nil to be newline param to other methods
|
54
|
+
!!@debug && !@quiet
|
55
|
+
end
|
56
|
+
|
57
|
+
def debug!
|
58
|
+
@debug = true
|
59
|
+
end
|
60
|
+
|
61
|
+
def debug(msg, newline = nil)
|
62
|
+
tell_me(msg, nil, newline) if debug?
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
# valimism
|
67
|
+
def tell_me(msg, color = nil, newline = nil)
|
68
|
+
newline.nil? ? @shell.say(msg, color) : @shell.say(msg, color, newline)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
data/redpomo.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/redpomo/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Stefano Verna", "Yuri Freire"]
|
6
|
+
gem.email = ["yurifrl@outlook.com"]
|
7
|
+
gem.description = %q{A nice little gem that integrates Redmine, Todo.txt and Pomodoro.app}
|
8
|
+
gem.summary = %q{A nice little gem that integrates Redmine, Todo.txt and Pomodoro.app}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = Dir.glob("./**/*")
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "redpomo-reloaded"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Redpomo::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency "activesupport"
|
19
|
+
gem.add_dependency "thor"
|
20
|
+
gem.add_dependency "todo-txt", '~> 0.11'
|
21
|
+
gem.add_dependency "rest-client"
|
22
|
+
gem.add_dependency "launchy"
|
23
|
+
gem.add_dependency "applescript"
|
24
|
+
gem.add_dependency "terminal-table"
|
25
|
+
|
26
|
+
gem.add_development_dependency "rspec"
|
27
|
+
gem.add_development_dependency "guard-rspec"
|
28
|
+
gem.add_development_dependency "vcr"
|
29
|
+
gem.add_development_dependency "webmock"
|
30
|
+
gem.add_development_dependency "mocha"
|
31
|
+
gem.add_development_dependency "simplecov"
|
32
|
+
gem.add_development_dependency "rake"
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'redpomo/file_cache'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
describe Redpomo::FileCache do
|
6
|
+
|
7
|
+
describe "#get" do
|
8
|
+
it "executes the block on cache miss" do
|
9
|
+
old_cache_path = Redpomo::FileCache.instance.cache_path
|
10
|
+
Redpomo::FileCache.instance.cache_path = tmp_path("cache_file")
|
11
|
+
|
12
|
+
counter = Redpomo::FileCache.get("foobar") { 5 }
|
13
|
+
counter.should == 5
|
14
|
+
|
15
|
+
counter = Redpomo::FileCache.get("foobar") { 10 }
|
16
|
+
counter.should == 5
|
17
|
+
|
18
|
+
Redpomo::FileCache.instance.cache_path = old_cache_path
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: get
|
5
|
+
uri: http://code.welaika.com/users/current.json?key=WELAIKA_TOKEN
|
6
|
+
body:
|
7
|
+
encoding: US-ASCII
|
8
|
+
string: ''
|
9
|
+
headers:
|
10
|
+
Accept:
|
11
|
+
- application/json
|
12
|
+
Accept-Encoding:
|
13
|
+
- gzip, deflate
|
14
|
+
Content-Type:
|
15
|
+
- application/json
|
16
|
+
User-Agent:
|
17
|
+
- Ruby
|
18
|
+
response:
|
19
|
+
status:
|
20
|
+
code: 200
|
21
|
+
message: OK
|
22
|
+
headers:
|
23
|
+
Date:
|
24
|
+
- Sun, 13 May 2012 15:49:32 GMT
|
25
|
+
Server:
|
26
|
+
- Apache/2.2.9 (Debian) PHP/5.2.6-1+lenny9 with Suhosin-Patch mod_ssl/2.2.9
|
27
|
+
OpenSSL/0.9.8g Phusion_Passenger/3.0.0
|
28
|
+
X-Powered-By:
|
29
|
+
- Phusion Passenger (mod_rails/mod_rack) 3.0.0
|
30
|
+
Etag:
|
31
|
+
- ! '"210f37abc00e2352ce9a7ebfc82e684d"'
|
32
|
+
X-Runtime:
|
33
|
+
- '48'
|
34
|
+
Cache-Control:
|
35
|
+
- private, max-age=0, must-revalidate
|
36
|
+
Content-Length:
|
37
|
+
- '196'
|
38
|
+
Status:
|
39
|
+
- '200'
|
40
|
+
Content-Type:
|
41
|
+
- application/json; charset=utf-8
|
42
|
+
body:
|
43
|
+
encoding: US-ASCII
|
44
|
+
string: ! '{"user":{"mail":"stefano.verna@welaika.com","last_login_on":"2012/05/13
|
45
|
+
11:17:28 +0200","lastname":"Verna","login":"steffoz","created_on":"2010/02/11
|
46
|
+
09:34:28 +0100","firstname":"Stefano","id":4}}'
|
47
|
+
http_version:
|
48
|
+
recorded_at: Sun, 13 May 2012 15:49:33 GMT
|
49
|
+
- request:
|
50
|
+
method: post
|
51
|
+
uri: http://code.welaika.com/issues.json?key=WELAIKA_TOKEN
|
52
|
+
body:
|
53
|
+
encoding: UTF-8
|
54
|
+
string: ! '{"issue":{"subject":"foobar","description":null,"due_date":null,"project_id":"olasagasti","priority_id":4,"assigned_to_id":4}}'
|
55
|
+
headers:
|
56
|
+
Accept:
|
57
|
+
- application/json
|
58
|
+
Accept-Encoding:
|
59
|
+
- gzip, deflate
|
60
|
+
Content-Type:
|
61
|
+
- application/json
|
62
|
+
Content-Length:
|
63
|
+
- '126'
|
64
|
+
User-Agent:
|
65
|
+
- Ruby
|
66
|
+
response:
|
67
|
+
status:
|
68
|
+
code: 201
|
69
|
+
message: Created
|
70
|
+
headers:
|
71
|
+
Date:
|
72
|
+
- Sun, 13 May 2012 15:49:33 GMT
|
73
|
+
Server:
|
74
|
+
- Apache/2.2.9 (Debian) PHP/5.2.6-1+lenny9 with Suhosin-Patch mod_ssl/2.2.9
|
75
|
+
OpenSSL/0.9.8g Phusion_Passenger/3.0.0
|
76
|
+
X-Powered-By:
|
77
|
+
- Phusion Passenger (mod_rails/mod_rack) 3.0.0
|
78
|
+
X-Runtime:
|
79
|
+
- '288'
|
80
|
+
Cache-Control:
|
81
|
+
- no-cache
|
82
|
+
Set-Cookie:
|
83
|
+
- _redmine_session=BAh7BzoPc2Vzc2lvbl9pZCIlMGE5Y2M5NGZmNjRiYjA4NjE2MTY1MDk3ZDE2MmUzN2IiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7BjoLbm90aWNlIhpDcmVhemlvbmUgZWZmZXR0dWF0YS4GOgpAdXNlZHsGOwdG--4f2ec2b1d5f84985b5475828990ec5b42bdd9c17;
|
84
|
+
path=/; HttpOnly
|
85
|
+
Location:
|
86
|
+
- http://code.welaika.com/issues/3397
|
87
|
+
Content-Length:
|
88
|
+
- '408'
|
89
|
+
Status:
|
90
|
+
- '201'
|
91
|
+
Content-Type:
|
92
|
+
- application/json; charset=utf-8
|
93
|
+
body:
|
94
|
+
encoding: US-ASCII
|
95
|
+
string: ! '{"issue":{"status":{"name":"New","id":1},"updated_on":"2012/05/13
|
96
|
+
17:49:33 +0200","assigned_to":{"name":"Stefano Verna","id":4},"done_ratio":0,"tracker":{"name":"Bug","id":1},"start_date":"2012/05/13","created_on":"2012/05/13
|
97
|
+
17:49:33 +0200","spent_hours":0.0,"subject":"foobar","author":{"name":"Stefano
|
98
|
+
Verna","id":4},"id":3397,"priority":{"name":"Normal","id":4},"project":{"name":"Olasagasti
|
99
|
+
","id":52}}}'
|
100
|
+
http_version:
|
101
|
+
recorded_at: Sun, 13 May 2012 15:49:33 GMT
|
102
|
+
recorded_with: VCR 2.0.1
|