task_report 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 +1 -0
- data/.ruby-version +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +79 -0
- data/Rakefile +2 -0
- data/VERSION +1 -0
- data/bin/task +58 -0
- data/lib/task_report/duration.rb +17 -0
- data/lib/task_report/gist.rb +81 -0
- data/lib/task_report/report.rb +202 -0
- data/lib/task_report/task.rb +72 -0
- data/lib/task_report/user.rb +7 -0
- data/lib/task_report.rb +126 -0
- data/task_report.gemspec +22 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 83f485efcd3d9a42cb28df8584a6b8395281b57a
|
4
|
+
data.tar.gz: dccb48b7fb94e18d523b0ac5e9b121790a529fa3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3ce79635dda36eea5e05d39c2f2ad3b806a7eb037662532a92bb87259e63aeb5b334487b33d5997764f0066c27395478a22746b6b8a38d05b5895106354a1dd2
|
7
|
+
data.tar.gz: 3420cda1a200e996badde56d0514876b784608be9882eb9ceab08c14126cbfa8319e5143d52884b4a9f0426e083868cff689a2c68b01fd7a68db170d1239084c
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.byebug_history
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.0
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Mat Pataki
|
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,79 @@
|
|
1
|
+
This repo is currently under construction. Don't use it while this message is here.
|
2
|
+
|
3
|
+
## Description
|
4
|
+
|
5
|
+
This tool enables you to track where your time goes via a private [gist](https://gist.github.com). The data itself is stored in a gist as json, and a formatted summary can be generated and shared (TODO, right now there's only a stdout summary). This means that task tracking is not tied to any specific machine, but rather a github account.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
- [Make a github / gist account.](https://github.com/join?return_to=https%3A%2F%2Fgist.github.com%2F%3Fsignup%3Dtrue&source=header-gist)
|
10
|
+
- [Generate a Personal Access Token](https://help.github.com/articles/creating-an-access-token-for-command-line-use/) that has read / write access to your gists.
|
11
|
+
- From a shell:
|
12
|
+
```shell
|
13
|
+
gem install task_report
|
14
|
+
```
|
15
|
+
|
16
|
+
- Add your *github user name* and *personal access token* a YAML config file located at `~/.task_report_config`.
|
17
|
+
|
18
|
+
Example configuration:
|
19
|
+
```
|
20
|
+
user: you_github_username
|
21
|
+
personal_access_token: 12345678abcdefghi9101112131415jklmnop
|
22
|
+
```
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
```
|
27
|
+
Use `task` as follows:
|
28
|
+
|
29
|
+
`task start TASK-DESCRIPTION`
|
30
|
+
- finds or creates a new gist for today
|
31
|
+
- adds a new item with the provided TASK-DESCRIPTION
|
32
|
+
|
33
|
+
`task stop`
|
34
|
+
- stops time tracking the current task, if it exists
|
35
|
+
|
36
|
+
`task continue [TASK-ID, TASK-DESCRIPTION]`
|
37
|
+
- continues tracking the provided task, or latest task if none if provided
|
38
|
+
|
39
|
+
`task current`
|
40
|
+
- lists the currently ongoing task
|
41
|
+
|
42
|
+
`task list`
|
43
|
+
- Lists all of today's tasks
|
44
|
+
|
45
|
+
`task summary`
|
46
|
+
- prints a task summary to the command line
|
47
|
+
|
48
|
+
`task delete {TASK-ID, TASK-DESCRIPTION, today, gist}`
|
49
|
+
- deletes the provided task if it exists
|
50
|
+
- if 'today' is passed, then all tasks in today's report will be deleted
|
51
|
+
- if 'gist' is passed, then the whole report gist for today will be deleted
|
52
|
+
|
53
|
+
`task help`
|
54
|
+
- shows this message
|
55
|
+
```
|
56
|
+
|
57
|
+
## System Requirements
|
58
|
+
|
59
|
+
- ruby >= 2.3.0
|
60
|
+
|
61
|
+
## TODO:
|
62
|
+
|
63
|
+
- [x] `continue`
|
64
|
+
- [ ] `info`
|
65
|
+
- [x] `delete`
|
66
|
+
- [ ] confirmation messages for `delete`
|
67
|
+
- [x] `list`
|
68
|
+
- [x] `current`
|
69
|
+
- [x] basic `summary`
|
70
|
+
- [x] add configuration file support
|
71
|
+
- [ ] gist `summary`
|
72
|
+
- [ ] setup install
|
73
|
+
- [ ] add jira support?
|
74
|
+
- at the very least, a ticket field
|
75
|
+
- [ ] allow `summary` to take a gist id, so you can retroactively generate summaries
|
76
|
+
|
77
|
+
## License
|
78
|
+
|
79
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/task
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'task_report'
|
4
|
+
|
5
|
+
def show_usage_message(exit_status = 0)
|
6
|
+
puts \
|
7
|
+
"Use `task` as follows:
|
8
|
+
|
9
|
+
`task start TASK-DESCRIPTION`
|
10
|
+
- finds or creates a new gist for today
|
11
|
+
- adds a new item with the provided TASK-DESCRIPTION
|
12
|
+
|
13
|
+
`task stop`
|
14
|
+
- stops time tracking the current task, if it exists
|
15
|
+
|
16
|
+
`task continue [TASK-ID, TASK-DESCRIPTION]`
|
17
|
+
- continues tracking the provided task, or latest task if none if provided
|
18
|
+
|
19
|
+
`task current`
|
20
|
+
- lists the currently ongoing task
|
21
|
+
|
22
|
+
`task list`
|
23
|
+
- Lists all of today's tasks
|
24
|
+
|
25
|
+
`task summary`
|
26
|
+
- prints a task summary to the command line
|
27
|
+
|
28
|
+
`task delete {TASK-ID, TASK-DESCRIPTION, today, gist}`
|
29
|
+
- deletes the provided task if it exists
|
30
|
+
- if 'today' is passed, then all tasks in today's report will be deleted
|
31
|
+
- if 'gist' is passed, then the whole report gist for today will be deleted
|
32
|
+
|
33
|
+
`task help`
|
34
|
+
- shows this message"
|
35
|
+
|
36
|
+
exit exit_status
|
37
|
+
end
|
38
|
+
|
39
|
+
TaskReport.read_config
|
40
|
+
|
41
|
+
case ARGV[0]
|
42
|
+
when 'list'
|
43
|
+
TaskReport.list
|
44
|
+
when 'start'
|
45
|
+
TaskReport.start(ARGV[1])
|
46
|
+
when 'stop'
|
47
|
+
TaskReport.stop
|
48
|
+
when 'continue'
|
49
|
+
TaskReport.continue(ARGV[1])
|
50
|
+
when 'delete'
|
51
|
+
TaskReport.delete(ARGV[1])
|
52
|
+
when 'current'
|
53
|
+
TaskReport.current
|
54
|
+
when 'summary'
|
55
|
+
TaskReport.summary
|
56
|
+
else
|
57
|
+
show_usage_message
|
58
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TaskReport
|
2
|
+
class Duration
|
3
|
+
attr_accessor :seconds
|
4
|
+
|
5
|
+
def initialize(seconds)
|
6
|
+
@seconds = seconds.floor
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
min, sec = @seconds.divmod(60)
|
11
|
+
min %= 60
|
12
|
+
hour, _ = @seconds.divmod(3600)
|
13
|
+
|
14
|
+
"#{hour} hours, #{min} minutes, #{sec} seconds"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module TaskReport
|
6
|
+
module Gist
|
7
|
+
class << self
|
8
|
+
def find_gist_from_today_by_description(description)
|
9
|
+
Gist.get_recent_gists_for_user.find do |gist|
|
10
|
+
gist['description'] == description
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_recent_gists_for_user
|
15
|
+
now = Time.now
|
16
|
+
|
17
|
+
# kind of like Time.now.midnight in UTC
|
18
|
+
time_string = Time.new(now.year, now.month, now.day).getgm.strftime('%Y-%m-%dT%H:%M:%SZ')
|
19
|
+
|
20
|
+
params = {
|
21
|
+
access_token: User.api_token,
|
22
|
+
since: time_string
|
23
|
+
}
|
24
|
+
|
25
|
+
uri = URI "https://api.github.com/users/#{User.name}/gists"
|
26
|
+
uri.query = URI.encode_www_form params
|
27
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
28
|
+
http.use_ssl = true
|
29
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
30
|
+
response = http.request(request)
|
31
|
+
JSON.parse(response.body)
|
32
|
+
end
|
33
|
+
|
34
|
+
def create(params)
|
35
|
+
uri = URI "https://api.github.com/gists"
|
36
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
37
|
+
http.use_ssl = true
|
38
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
39
|
+
request['Accept'] = 'application/json'
|
40
|
+
request['Content-Type'] = 'application/json'
|
41
|
+
request['Authorization'] = "token #{User.api_token}"
|
42
|
+
request.body = JSON.dump(params)
|
43
|
+
response = http.request(request)
|
44
|
+
JSON.parse(response.body)
|
45
|
+
end
|
46
|
+
|
47
|
+
def edit(gist_id, params)
|
48
|
+
uri = URI "https://api.github.com/gists/#{gist_id}"
|
49
|
+
request = Net::HTTP::Patch.new(uri.request_uri)
|
50
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
51
|
+
http.use_ssl = true
|
52
|
+
request['Accept'] = 'application/json'
|
53
|
+
request['Content-Type'] = 'application/json'
|
54
|
+
request['Authorization'] = "token #{User.api_token}"
|
55
|
+
request.body = JSON.dump(params)
|
56
|
+
response = http.request(request)
|
57
|
+
JSON.parse(response.body)
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete(gist_id)
|
61
|
+
uri = URI "https://api.github.com/gists/#{gist_id}"
|
62
|
+
request = Net::HTTP::Delete.new(uri.request_uri)
|
63
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
64
|
+
http.use_ssl = true
|
65
|
+
request['Authorization'] = "token #{User.api_token}"
|
66
|
+
http.request(request)
|
67
|
+
end
|
68
|
+
|
69
|
+
def file_content(raw_url)
|
70
|
+
uri = URI raw_url
|
71
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
72
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
73
|
+
http.use_ssl = true
|
74
|
+
request['Accept'] = 'application/json'
|
75
|
+
request['Authorization'] = "token #{User.api_token}"
|
76
|
+
response = http.request(request)
|
77
|
+
JSON.parse(response.body)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
module TaskReport
|
2
|
+
class Report
|
3
|
+
TaskAlreadyTracked = Class.new StandardError
|
4
|
+
TaskAlreadyOngoing = Class.new StandardError
|
5
|
+
TaskDNE = Class.new StandardError
|
6
|
+
MultipleOngoingTasks = Class.new StandardError
|
7
|
+
|
8
|
+
attr_reader :gist_id
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def gist_description
|
12
|
+
"#{User.name}_report_#{Time.now.strftime('%Y-%m-%d')}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def json_file_name
|
16
|
+
"#{gist_description}.json"
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(new_task_description:)
|
20
|
+
Report.new(
|
21
|
+
description: gist_description,
|
22
|
+
json_file_name: json_file_name
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_from_gist(gist)
|
27
|
+
raw_url = gist['files'][json_file_name]['raw_url']
|
28
|
+
|
29
|
+
Report.new(
|
30
|
+
description: gist_description,
|
31
|
+
json_file_name: json_file_name,
|
32
|
+
gist_id: gist['id'],
|
33
|
+
existing_json_content: Gist.file_content(raw_url)
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def start_task(new_task_description)
|
39
|
+
if @tasks.any? { |t| t.description == new_task_description }
|
40
|
+
raise TaskAlreadyTracked
|
41
|
+
end
|
42
|
+
|
43
|
+
task = Task.new(description: new_task_description)
|
44
|
+
puts "Starting #{task.to_s}."
|
45
|
+
@tasks << task
|
46
|
+
end
|
47
|
+
|
48
|
+
def stop_all_tasks
|
49
|
+
@tasks.each(&:stop)
|
50
|
+
end
|
51
|
+
|
52
|
+
def continue(task_id)
|
53
|
+
ensure_no_tasks_ongoing!
|
54
|
+
|
55
|
+
task =
|
56
|
+
if task_id.nil?
|
57
|
+
find_last_task_to_be_worked_on
|
58
|
+
else
|
59
|
+
find_task_by_id(task_id) || find_task_by_description(task_id)
|
60
|
+
end
|
61
|
+
|
62
|
+
task.continue
|
63
|
+
end
|
64
|
+
|
65
|
+
def delete(task_id)
|
66
|
+
task = find_task_by_id(task_id) || find_task_by_description(task_id)
|
67
|
+
raise TaskDNE if task.nil?
|
68
|
+
|
69
|
+
puts "Deleting #{task.to_s}."
|
70
|
+
@tasks.delete_if { |t| t.id == task.id }
|
71
|
+
end
|
72
|
+
|
73
|
+
def delete_all
|
74
|
+
puts "Deleting all tasks for today."
|
75
|
+
@tasks = []
|
76
|
+
end
|
77
|
+
|
78
|
+
def print_tasks
|
79
|
+
if @tasks.empty?
|
80
|
+
puts 'There are no tasks reported for today.'
|
81
|
+
return
|
82
|
+
end
|
83
|
+
|
84
|
+
puts "Tasks:"
|
85
|
+
|
86
|
+
@tasks.each do |task|
|
87
|
+
puts "- #{task.to_s}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def print_current_task
|
92
|
+
ensure_only_one_ongoing_task!
|
93
|
+
|
94
|
+
task = @tasks.find(&:ongoing?)
|
95
|
+
|
96
|
+
if task.nil?
|
97
|
+
puts "There is no task ongoing."
|
98
|
+
else
|
99
|
+
puts "Curent task: #{task.to_s}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def print_summary
|
104
|
+
if @tasks.empty?
|
105
|
+
puts 'There are no tasks reported for today.'
|
106
|
+
return
|
107
|
+
end
|
108
|
+
|
109
|
+
puts "#{User.name} Task Report #{@date.strftime('%Y-%m-%d')}"
|
110
|
+
|
111
|
+
@tasks.each do |task|
|
112
|
+
puts "'#{task.description}'"
|
113
|
+
puts " - #{task.duration.to_s}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def save_to_gist!
|
118
|
+
if @gist_id
|
119
|
+
edit_existing_gist!
|
120
|
+
else
|
121
|
+
save_new_gist!
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
def initialize(description:, json_file_name:, gist_id: nil, existing_json_content: {})
|
127
|
+
@description = description
|
128
|
+
@json_file_name = json_file_name
|
129
|
+
@gist_id = gist_id
|
130
|
+
|
131
|
+
@date = Time.parse(
|
132
|
+
existing_json_content.fetch('date', Time.now.strftime('%Y-%m-%d %z'))
|
133
|
+
)
|
134
|
+
|
135
|
+
@tasks =
|
136
|
+
existing_json_content.fetch('tasks', []).map do |hash|
|
137
|
+
Task.from_existing_tasks(hash)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_h
|
142
|
+
{
|
143
|
+
date: @date.strftime('%Y-%m-%d %z'),
|
144
|
+
tasks: @tasks.map(&:to_h)
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
def task_json
|
149
|
+
JSON.pretty_generate(to_h)
|
150
|
+
end
|
151
|
+
|
152
|
+
def save_new_gist!
|
153
|
+
puts "Starting a new report gist for the day."
|
154
|
+
|
155
|
+
Gist.create(
|
156
|
+
public: false,
|
157
|
+
description: @description,
|
158
|
+
files: {
|
159
|
+
@json_file_name => {
|
160
|
+
content: task_json
|
161
|
+
}
|
162
|
+
}
|
163
|
+
)
|
164
|
+
end
|
165
|
+
|
166
|
+
def edit_existing_gist!
|
167
|
+
puts "Saving to today's report gist."
|
168
|
+
|
169
|
+
Gist.edit(@gist_id,
|
170
|
+
description: @description, # do we actually need this? Seems odd...
|
171
|
+
files: {
|
172
|
+
@json_file_name => {
|
173
|
+
content: task_json
|
174
|
+
}
|
175
|
+
}
|
176
|
+
)
|
177
|
+
end
|
178
|
+
|
179
|
+
def find_task_by_id(task_id)
|
180
|
+
@tasks.find { |t| t.id == task_id }
|
181
|
+
end
|
182
|
+
|
183
|
+
def find_task_by_description(task_description)
|
184
|
+
@tasks.find { |t| t.description == task_description }
|
185
|
+
end
|
186
|
+
|
187
|
+
def find_last_task_to_be_worked_on
|
188
|
+
@tasks.inject(@tasks.first) do |result, task|
|
189
|
+
task.last_start_time > result.last_start_time ? task : result
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def ensure_no_tasks_ongoing!
|
194
|
+
ongoing_task = @tasks.find(&:ongoing?)
|
195
|
+
raise TaskAlreadyOngoing, ongoing_task if ongoing_task
|
196
|
+
end
|
197
|
+
|
198
|
+
def ensure_only_one_ongoing_task!
|
199
|
+
raise MultipleOngoingTasks if @tasks.count(&:ongoing?) > 1
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'time' # Time.parse)
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module TaskReport
|
5
|
+
class Task
|
6
|
+
TaskOngoing = Class.new StandardError
|
7
|
+
|
8
|
+
attr_reader :id, :description
|
9
|
+
|
10
|
+
def self.from_existing_tasks(hash)
|
11
|
+
time =
|
12
|
+
hash['time'].map do |t|
|
13
|
+
{
|
14
|
+
start: Time.parse(t['start']),
|
15
|
+
end: t['end'].nil? ? nil : Time.parse(t['end'])
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
self.new(
|
20
|
+
id: hash['id'],
|
21
|
+
description: hash['description'],
|
22
|
+
time: time
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(description:, time: nil, id: nil)
|
27
|
+
@description = description
|
28
|
+
@time = time || [{ start: Time.now, end: nil }]
|
29
|
+
@id = id || SecureRandom.hex(4)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_h
|
33
|
+
{
|
34
|
+
id: @id,
|
35
|
+
description: @description,
|
36
|
+
time: @time
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
"Task #{@id}, '#{@description}'"
|
42
|
+
end
|
43
|
+
|
44
|
+
def duration
|
45
|
+
Duration.new(
|
46
|
+
@time.inject(0) do |sum, time|
|
47
|
+
sum + ( (time[:end] || Time.now) - time[:start] )
|
48
|
+
end
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def stop
|
53
|
+
return unless @time.last[:end].nil?
|
54
|
+
puts "Stopping #{self.to_s}"
|
55
|
+
@time.last[:end] = Time.now
|
56
|
+
end
|
57
|
+
|
58
|
+
def continue
|
59
|
+
raise TaskOngoing if @time.last[:end].nil?
|
60
|
+
puts "Continueing #{self.to_s}"
|
61
|
+
@time << { start: Time.now, end: nil }
|
62
|
+
end
|
63
|
+
|
64
|
+
def last_start_time
|
65
|
+
@time.last[:start]
|
66
|
+
end
|
67
|
+
|
68
|
+
def ongoing?
|
69
|
+
@time.last[:end].nil?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/task_report.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'task_report/user'
|
3
|
+
require 'task_report/gist'
|
4
|
+
require 'task_report/task'
|
5
|
+
require 'task_report/report'
|
6
|
+
require 'task_report/duration'
|
7
|
+
|
8
|
+
module TaskReport
|
9
|
+
class << self
|
10
|
+
def read_config
|
11
|
+
config_path = File.expand_path('~/.task_report_config')
|
12
|
+
config = YAML.load(File.read(config_path))
|
13
|
+
TaskReport::User.name = config.fetch('user')
|
14
|
+
TaskReport::User.api_token = config.fetch('personal_access_token')
|
15
|
+
rescue Errno::ENOENT
|
16
|
+
puts 'Config file not found. It should be located at ~/.task_report_config.'
|
17
|
+
puts 'See https://github.com/mpataki/task_report for help.'
|
18
|
+
puts 'Exiting'
|
19
|
+
exit 1
|
20
|
+
rescue Psych::SyntaxError
|
21
|
+
puts 'The config file must be valid yaml syntax.'
|
22
|
+
puts 'Exiting'
|
23
|
+
exit 1
|
24
|
+
rescue KeyError
|
25
|
+
puts 'Config key not found.'
|
26
|
+
puts 'Required configuration keys are `user` and `personal_access_token`'
|
27
|
+
puts 'See an example at https://github.com/mpataki/task_report'
|
28
|
+
puts 'Exiting'
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
|
32
|
+
def start(new_task_description)
|
33
|
+
@report ||=
|
34
|
+
if report_gist.nil?
|
35
|
+
Report.create(new_task_description: new_task_description)
|
36
|
+
else
|
37
|
+
Report.create_from_gist(report_gist).tap(&:stop_all_tasks)
|
38
|
+
end
|
39
|
+
|
40
|
+
@report.start_task(new_task_description)
|
41
|
+
@report.save_to_gist!
|
42
|
+
rescue Report::TaskAlreadyTracked
|
43
|
+
puts "Task '#{new_task_description}' is already being tracked. Continuing the task."
|
44
|
+
continue(new_task_description)
|
45
|
+
end
|
46
|
+
|
47
|
+
def stop
|
48
|
+
return if no_gist?
|
49
|
+
|
50
|
+
@report ||= Report.create_from_gist(report_gist)
|
51
|
+
@report.stop_all_tasks
|
52
|
+
@report.save_to_gist!
|
53
|
+
puts "All tasks stopped"
|
54
|
+
end
|
55
|
+
|
56
|
+
def continue(task_id)
|
57
|
+
return if no_gist?
|
58
|
+
|
59
|
+
@report ||= Report.create_from_gist(report_gist)
|
60
|
+
@report.continue(task_id)
|
61
|
+
@report.save_to_gist!
|
62
|
+
rescue Report::TaskAlreadyOngoing, Task::TaskOngoing => e
|
63
|
+
puts "Task already underway - #{e.message}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def list
|
67
|
+
return if no_gist?
|
68
|
+
(@report || Report.create_from_gist(report_gist)).print_tasks
|
69
|
+
end
|
70
|
+
|
71
|
+
def delete(identifier)
|
72
|
+
return if no_gist?
|
73
|
+
|
74
|
+
@report ||= Report.create_from_gist(report_gist)
|
75
|
+
|
76
|
+
case identifier
|
77
|
+
when 'today'
|
78
|
+
@report.delete_all
|
79
|
+
when 'gist'
|
80
|
+
puts "Deleting today's report gist"
|
81
|
+
Gist.delete(@report.gist_id)
|
82
|
+
return
|
83
|
+
else
|
84
|
+
@report.delete(identifier)
|
85
|
+
end
|
86
|
+
|
87
|
+
@report.save_to_gist!
|
88
|
+
rescue Report::TaskDNE
|
89
|
+
puts "Task '#{identifier}' does not exist - nothing to do."
|
90
|
+
end
|
91
|
+
|
92
|
+
def current
|
93
|
+
return if no_gist?
|
94
|
+
|
95
|
+
@report ||= Report.create_from_gist(report_gist)
|
96
|
+
@report.print_current_task
|
97
|
+
rescue Report::MultipleOngoingTasks
|
98
|
+
puts 'Something went wrong. There are multiple ongoing tasks.'
|
99
|
+
end
|
100
|
+
|
101
|
+
def summary
|
102
|
+
return if no_gist?
|
103
|
+
|
104
|
+
@report ||= Report.create_from_gist(report_gist)
|
105
|
+
@report.print_summary
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
def report_gist
|
110
|
+
@report_gist ||=
|
111
|
+
Gist.find_gist_from_today_by_description(
|
112
|
+
Report.gist_description
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
def no_gist?
|
117
|
+
if report_gist.nil?
|
118
|
+
puts 'No report exists for today - nothing to do.'
|
119
|
+
puts 'See `task help` for usage info.'
|
120
|
+
return true
|
121
|
+
end
|
122
|
+
|
123
|
+
false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/task_report.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
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 = 'task_report'
|
7
|
+
spec.version = File.read('VERSION')
|
8
|
+
spec.authors = ['Mat Pataki']
|
9
|
+
spec.email = ['matpataki@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Task tracker'
|
12
|
+
spec.homepage = 'https://github.com/mpataki/task_report'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = 'bin'
|
17
|
+
spec.executables = ['task']
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
21
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: task_report
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mat Pataki
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-08-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.12'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- matpataki@gmail.com
|
44
|
+
executables:
|
45
|
+
- task
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".gitignore"
|
50
|
+
- ".ruby-version"
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- VERSION
|
55
|
+
- bin/task
|
56
|
+
- lib/task_report.rb
|
57
|
+
- lib/task_report/duration.rb
|
58
|
+
- lib/task_report/gist.rb
|
59
|
+
- lib/task_report/report.rb
|
60
|
+
- lib/task_report/task.rb
|
61
|
+
- lib/task_report/user.rb
|
62
|
+
- task_report.gemspec
|
63
|
+
homepage: https://github.com/mpataki/task_report
|
64
|
+
licenses:
|
65
|
+
- MIT
|
66
|
+
metadata: {}
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 2.5.1
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: Task tracker
|
87
|
+
test_files: []
|