tempo-cli 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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +56 -0
- data/README.md +326 -0
- data/Rakefile +65 -0
- data/bin/tempo +477 -0
- data/features/arrange.feature +43 -0
- data/features/checkout.feature +63 -0
- data/features/end.feature +65 -0
- data/features/project.feature +246 -0
- data/features/report.feature +62 -0
- data/features/start.feature +87 -0
- data/features/step_definitions/tempo_steps.rb +138 -0
- data/features/support/env.rb +26 -0
- data/features/tempo.feature +13 -0
- data/features/update.feature +69 -0
- data/lib/file_record/directory.rb +11 -0
- data/lib/file_record/directory_structure/tempo/README.txt +4 -0
- data/lib/file_record/directory_structure/tempo/tempo_projects.yaml +6 -0
- data/lib/file_record/record.rb +120 -0
- data/lib/tempo/controllers/arrange_controller.rb +52 -0
- data/lib/tempo/controllers/base.rb +117 -0
- data/lib/tempo/controllers/checkout_controller.rb +42 -0
- data/lib/tempo/controllers/end_controller.rb +42 -0
- data/lib/tempo/controllers/projects_controller.rb +107 -0
- data/lib/tempo/controllers/records_controller.rb +21 -0
- data/lib/tempo/controllers/report_controller.rb +55 -0
- data/lib/tempo/controllers/start_controller.rb +42 -0
- data/lib/tempo/controllers/update_controller.rb +78 -0
- data/lib/tempo/models/base.rb +176 -0
- data/lib/tempo/models/composite.rb +71 -0
- data/lib/tempo/models/log.rb +194 -0
- data/lib/tempo/models/project.rb +73 -0
- data/lib/tempo/models/time_record.rb +235 -0
- data/lib/tempo/version.rb +3 -0
- data/lib/tempo/views/arrange_view.rb +27 -0
- data/lib/tempo/views/base.rb +82 -0
- data/lib/tempo/views/formatters/base.rb +30 -0
- data/lib/tempo/views/formatters/screen.rb +86 -0
- data/lib/tempo/views/projects_view.rb +82 -0
- data/lib/tempo/views/report_view.rb +26 -0
- data/lib/tempo/views/reporter.rb +70 -0
- data/lib/tempo/views/time_record_view.rb +30 -0
- data/lib/tempo/views/view_records/base.rb +117 -0
- data/lib/tempo/views/view_records/composite.rb +40 -0
- data/lib/tempo/views/view_records/log.rb +28 -0
- data/lib/tempo/views/view_records/project.rb +32 -0
- data/lib/tempo/views/view_records/time_record.rb +48 -0
- data/lib/tempo.rb +26 -0
- data/lib/time_utilities.rb +30 -0
- data/tempo-cli.gemspec +26 -0
- data/test/lib/file_record/directory_test.rb +30 -0
- data/test/lib/file_record/record_test.rb +106 -0
- data/test/lib/tempo/controllers/base_controller_test.rb +60 -0
- data/test/lib/tempo/controllers/project_controller_test.rb +24 -0
- data/test/lib/tempo/models/base_test.rb +173 -0
- data/test/lib/tempo/models/composite_test.rb +76 -0
- data/test/lib/tempo/models/log_test.rb +171 -0
- data/test/lib/tempo/models/project_test.rb +105 -0
- data/test/lib/tempo/models/time_record_test.rb +212 -0
- data/test/lib/tempo/views/base_test.rb +31 -0
- data/test/lib/tempo/views/formatters/base_test.rb +13 -0
- data/test/lib/tempo/views/formatters/screen_test.rb +94 -0
- data/test/lib/tempo/views/reporter_test.rb +40 -0
- data/test/lib/tempo/views/view_records/base_test.rb +77 -0
- data/test/lib/tempo/views/view_records/composite_test.rb +57 -0
- data/test/lib/tempo/views/view_records/log_test.rb +28 -0
- data/test/lib/tempo/views/view_records/project_test.rb +0 -0
- data/test/lib/tempo/views/view_records/time_record_test.rb +0 -0
- data/test/support/factories.rb +177 -0
- data/test/support/helpers.rb +69 -0
- data/test/test_helper.rb +31 -0
- metadata +230 -0
@@ -0,0 +1,235 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Model
|
3
|
+
class TimeRecord < Tempo::Model::Log
|
4
|
+
attr_accessor :project, :description
|
5
|
+
attr_reader :start_time, :end_time, :tags
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def current
|
10
|
+
return @current if @current && @current.end_time == :running
|
11
|
+
@current = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def current=( instance )
|
15
|
+
if instance.class == self
|
16
|
+
@current = instance
|
17
|
+
else
|
18
|
+
raise ArgumentError
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(options={})
|
24
|
+
|
25
|
+
# declare these first for model organization when sent to YAML
|
26
|
+
@project_title = nil
|
27
|
+
@description = options.fetch :description, ""
|
28
|
+
@start_time = nil
|
29
|
+
|
30
|
+
# verify both start time and end time before sending to super
|
31
|
+
options[:start_time] ||= Time.now
|
32
|
+
verify_start_time options[:start_time]
|
33
|
+
@end_time = options.fetch :end_time, :running
|
34
|
+
verify_end_time options[:start_time], @end_time
|
35
|
+
super options
|
36
|
+
|
37
|
+
project = options.fetch :project, Tempo::Model::Project.current
|
38
|
+
@project = project.kind_of?(Integer) ? project : project.id
|
39
|
+
|
40
|
+
@tags = []
|
41
|
+
tag options.fetch(:tags, [])
|
42
|
+
|
43
|
+
# close out the running time record
|
44
|
+
if running?
|
45
|
+
if not self.class.current
|
46
|
+
self.class.current = self
|
47
|
+
else
|
48
|
+
|
49
|
+
current = self.class.current
|
50
|
+
|
51
|
+
# more recent entries exist, need to close out immediately
|
52
|
+
if current.start_time > @start_time
|
53
|
+
if current.start_time.day > @start_time.day
|
54
|
+
out = self.class.end_of_day @start_time
|
55
|
+
@end_time = out
|
56
|
+
# TODO add a new record onto the next day
|
57
|
+
else
|
58
|
+
@end_time = current.start_time
|
59
|
+
end
|
60
|
+
|
61
|
+
# close out the last current record
|
62
|
+
else
|
63
|
+
self.class.close_current @start_time
|
64
|
+
self.class.current = self
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# close out any earlier running timerecords
|
69
|
+
else
|
70
|
+
if self.class.current
|
71
|
+
if self.class.current.start_time < @start_time
|
72
|
+
self.class.close_current @start_time
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def start_time= time
|
79
|
+
@start_time = time if verify_start_time time
|
80
|
+
end
|
81
|
+
|
82
|
+
def end_time= time
|
83
|
+
@end_time = time if verify_end_time self.start_time, time
|
84
|
+
end
|
85
|
+
|
86
|
+
def valid_start_time? time
|
87
|
+
begin
|
88
|
+
verify_start_time time
|
89
|
+
rescue ArgumentError => e
|
90
|
+
return false
|
91
|
+
end
|
92
|
+
true
|
93
|
+
end
|
94
|
+
|
95
|
+
def valid_end_time? time
|
96
|
+
begin
|
97
|
+
verify_end_time self.start_time, time
|
98
|
+
rescue ArgumentError => e
|
99
|
+
return false
|
100
|
+
end
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
def project_title
|
105
|
+
Project.find_by_id( @project ).title if @project
|
106
|
+
end
|
107
|
+
|
108
|
+
def duration
|
109
|
+
if @end_time.kind_of? Time
|
110
|
+
end_time = @end_time
|
111
|
+
else
|
112
|
+
end_time = Time.now()
|
113
|
+
end
|
114
|
+
end_time.to_i - @start_time.to_i
|
115
|
+
end
|
116
|
+
|
117
|
+
def running?
|
118
|
+
@end_time == :running
|
119
|
+
end
|
120
|
+
|
121
|
+
def freeze_dry
|
122
|
+
record = super
|
123
|
+
record[:project_title] = project_title
|
124
|
+
record
|
125
|
+
end
|
126
|
+
|
127
|
+
def tag( tags )
|
128
|
+
return unless tags and tags.kind_of? Array
|
129
|
+
tags.each do |tag|
|
130
|
+
tag.split.each {|t| @tags << t if ! @tags.include? t }
|
131
|
+
end
|
132
|
+
@tags.sort!
|
133
|
+
end
|
134
|
+
|
135
|
+
def untag( tags )
|
136
|
+
return unless tags and tags.kind_of? Array
|
137
|
+
tags.each do |tag|
|
138
|
+
tag.split.each {|t| @tags.delete t }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def to_s
|
143
|
+
"#{@start_time} - #{@end_time}, #{project_title}: #{@description}"
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
#close current at the end time, or on the last minute
|
149
|
+
# of the day if end time is another day
|
150
|
+
#
|
151
|
+
def self.close_current end_time
|
152
|
+
if end_time.day > current.start_time.day
|
153
|
+
out = end_of_day current.start_time
|
154
|
+
current.end_time = out
|
155
|
+
else
|
156
|
+
current.end_time = end_time
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# check a time against all loaded instances, verify that it doesn't
|
161
|
+
# fall in the middle of any closed time records
|
162
|
+
#
|
163
|
+
def verify_start_time time
|
164
|
+
|
165
|
+
# Check that there are currently
|
166
|
+
# records on the day to iterate through
|
167
|
+
dsym = self.class.date_symbol time
|
168
|
+
return true if not self.class.days_index[dsym]
|
169
|
+
|
170
|
+
self.class.days_index[dsym].each do |record|
|
171
|
+
|
172
|
+
next if record.end_time == :running
|
173
|
+
next if record == self
|
174
|
+
if time < record.end_time
|
175
|
+
raise ArgumentError, "Time conflict with existing record" if time_in_record? time, record
|
176
|
+
end
|
177
|
+
end
|
178
|
+
true
|
179
|
+
end
|
180
|
+
|
181
|
+
def verify_end_time start_time, end_time
|
182
|
+
|
183
|
+
# TODO: a better check for :running conditions
|
184
|
+
return true if end_time == :running
|
185
|
+
|
186
|
+
raise ArgumentError, "End time must be greater than start time" if end_time < start_time
|
187
|
+
|
188
|
+
dsym = self.class.date_symbol end_time
|
189
|
+
start_dsym = self.class.date_symbol start_time
|
190
|
+
raise ArgumentError, "End time must be on the same day as start time: #{start_time} : #{end_time}" if dsym != start_dsym
|
191
|
+
|
192
|
+
# this is necessary if this is the first record
|
193
|
+
# for the day and self is not yet added to index
|
194
|
+
return if not self.class.days_index[dsym]
|
195
|
+
|
196
|
+
self.class.days_index[dsym].each do |record|
|
197
|
+
next if record == self
|
198
|
+
raise ArgumentError, "Time conflict with existing record:" if time_span_intersects_record? start_time, end_time, record
|
199
|
+
end
|
200
|
+
true
|
201
|
+
end
|
202
|
+
|
203
|
+
# this is used for both start time and end times,
|
204
|
+
# so it will return true if the time is :running
|
205
|
+
# or if it is exactly the record start or end time
|
206
|
+
# these conditions need to be checked separately
|
207
|
+
def time_in_record? time, record
|
208
|
+
return false if record.end_time == :running
|
209
|
+
time >= record.start_time && time <= record.end_time
|
210
|
+
end
|
211
|
+
|
212
|
+
# All true conditions should be used to raise errors.
|
213
|
+
# Returns false is when sharing a single end and start point (a valid state).
|
214
|
+
# It does not invalidate a time span earlier than the record with a :running end time,
|
215
|
+
# this condition must be accounted for separately.
|
216
|
+
# It assumes a valid start and end time.
|
217
|
+
def time_span_intersects_record? start_time, end_time, record
|
218
|
+
if record.end_time == :running
|
219
|
+
return true if start_time <= record.start_time && end_time > record.start_time
|
220
|
+
return false
|
221
|
+
end
|
222
|
+
return false if start_time >= record.end_time
|
223
|
+
return true if start_time >= record.start_time && start_time < record.end_time
|
224
|
+
return false if record.end_time == :running
|
225
|
+
return true if end_time > record.start_time
|
226
|
+
return false
|
227
|
+
end
|
228
|
+
|
229
|
+
# returns the last minute of the day
|
230
|
+
def self.end_of_day time
|
231
|
+
Time.new(time.year, time.month, time.day, 23, 59)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Views
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def arrange_parent_child parent, child
|
6
|
+
ViewRecords::Message.new "parent project:"
|
7
|
+
ViewRecords::Project.new parent
|
8
|
+
ViewRecords::Message.new "child project:"
|
9
|
+
ViewRecords::Project.new child
|
10
|
+
end
|
11
|
+
|
12
|
+
def arrange_root project
|
13
|
+
ViewRecords::Message.new "root project:"
|
14
|
+
ViewRecords::Project.new project
|
15
|
+
end
|
16
|
+
|
17
|
+
def arrange_already_root project
|
18
|
+
ViewRecords::Message.new "already a root project:"
|
19
|
+
ViewRecords::Project.new project
|
20
|
+
end
|
21
|
+
|
22
|
+
def arrange_parse_error
|
23
|
+
ViewRecords::Message.new "arrange requires a colon (:) in the arguments", category: :error
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Views
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# called in the pre block, pushes relavent options to the reporter
|
6
|
+
def initialize_view_options command, global_options, options
|
7
|
+
view_opts = {}
|
8
|
+
view_opts[:verbose] = global_options[:verbose]
|
9
|
+
view_opts[:id] = global_options[:id]
|
10
|
+
case command
|
11
|
+
when :project, :p
|
12
|
+
if global_options[:verbose]
|
13
|
+
view_opts[:id] = true
|
14
|
+
view_opts[:tags] = true
|
15
|
+
view_opts[:active] = true
|
16
|
+
view_opts[:depth] = true
|
17
|
+
else
|
18
|
+
if options[:list]
|
19
|
+
view_opts[:depth] = true
|
20
|
+
view_opts[:active] = true
|
21
|
+
end
|
22
|
+
view_opts[:tags] = options[:tag] || options[:untag] ? true : false
|
23
|
+
view_opts[:id] = global_options[:id] || options[:id] ? true : false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
Tempo::Views::Reporter.add_options view_opts
|
27
|
+
end
|
28
|
+
|
29
|
+
# DEPRACATE- View is returned in post
|
30
|
+
#
|
31
|
+
# puts each line if output=true
|
32
|
+
# else returns an array of view lines
|
33
|
+
def return_view( view, options={} )
|
34
|
+
output = options.fetch( :output, true )
|
35
|
+
|
36
|
+
if output
|
37
|
+
if view.is_a? String
|
38
|
+
puts view
|
39
|
+
else
|
40
|
+
view.each { |line| puts line }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
view
|
44
|
+
end
|
45
|
+
|
46
|
+
def options_report( command, global_options, options, args )
|
47
|
+
globals_list = "global options: "
|
48
|
+
global_options.each {|k,v| globals_list += "#{k} = #{v}, " if k.kind_of? String and k.length > 1 and !v.nil? }
|
49
|
+
ViewRecords::Message.new globals_list[0..-2], category: :debug
|
50
|
+
|
51
|
+
options_list = "command options: "
|
52
|
+
options.each {|k,v| options_list += "#{k} = #{v}, " if k.kind_of? String and k.length > 1 and !v.nil? }
|
53
|
+
ViewRecords::Message.new options_list[0..-2], category: :debug
|
54
|
+
|
55
|
+
|
56
|
+
ViewRecords::Message.new "command: #{command}", category: :debug
|
57
|
+
ViewRecords::Message.new "args: #{args}", category: :debug
|
58
|
+
end
|
59
|
+
|
60
|
+
def no_items( items, category=:info )
|
61
|
+
ViewRecords::Message.new "no #{items} exist", category: category
|
62
|
+
end
|
63
|
+
|
64
|
+
def no_match_error( items, request, plural=true )
|
65
|
+
match = plural ? "match" : "matches"
|
66
|
+
ViewRecords::Message.new "no #{items} #{match} the request: #{request}", category: :error
|
67
|
+
end
|
68
|
+
|
69
|
+
def already_exists_error( item, request )
|
70
|
+
ViewRecords::Message.new "#{item} '#{request}' already exists", category: :error
|
71
|
+
end
|
72
|
+
|
73
|
+
def checkout_assistance( options={} )
|
74
|
+
ViewRecords::Message.new "checkout command run with no arguments"
|
75
|
+
ViewRecords::Message.new "perhaps you meant one of these?"
|
76
|
+
ViewRecords::Message.new " tempo checkout --add <new project name>"
|
77
|
+
ViewRecords::Message.new " tempo checkout <existing project>"
|
78
|
+
ViewRecords::Message.new "run `tempo checkout --help` for more information"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Tempo View Formatters are triggered by the View Reporter.
|
2
|
+
# The View Reporter sends it's stored view messages to each
|
3
|
+
# of it's formatters. It calls the method format_records and
|
4
|
+
# passes in the view records. If the formatter has a class method
|
5
|
+
# that handles the type of block passed in, it will process
|
6
|
+
# that view record. These class methods take the name "<record type>_block"
|
7
|
+
# where record type can be any child class of ViewRecord::Base
|
8
|
+
# see the screen formatter for an example of processing blocks.
|
9
|
+
|
10
|
+
module Tempo
|
11
|
+
module Views
|
12
|
+
module Formatters
|
13
|
+
|
14
|
+
class Base
|
15
|
+
|
16
|
+
# Here we check if our class methods include a proc block to handle the particular
|
17
|
+
# record type. See View Records for all possible record types. See screen formatter
|
18
|
+
# for examples of proc blocks.
|
19
|
+
#
|
20
|
+
def format_records records, options={}
|
21
|
+
@options = options
|
22
|
+
records.each do |record|
|
23
|
+
class_block = "#{record.type}_block"
|
24
|
+
send( class_block, record ) if respond_to? class_block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Tempo View Formatters are triggered by the View Reporter.
|
2
|
+
# The View Reporter sends it's stored view messages to each
|
3
|
+
# of it's formatters. It calls the method process records and
|
4
|
+
# passes in the view records. If the formatter has a class method
|
5
|
+
# that handles the type of block passed in, it will process
|
6
|
+
# that view record. These class methods take the name "<record type>_block"
|
7
|
+
# where record type can be any child class of ViewRecord::Base
|
8
|
+
# see <TODO> for an example of proc blocks.
|
9
|
+
|
10
|
+
module Tempo
|
11
|
+
module Views
|
12
|
+
module Formatters
|
13
|
+
|
14
|
+
class Screen < Tempo::Views::Formatters::Base
|
15
|
+
|
16
|
+
def message_block record
|
17
|
+
record.format do |m|
|
18
|
+
case m.category
|
19
|
+
when :error
|
20
|
+
raise m.message
|
21
|
+
when :info, :debug
|
22
|
+
puts m.message
|
23
|
+
end
|
24
|
+
m.message
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def duration_block record
|
29
|
+
record.format do |d|
|
30
|
+
puts "#{ d.hours.to_s }:#{ d.minutes.to_s.rjust(2, '0') }"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# spacer for project titles, active project marked with *
|
35
|
+
def active_indicator( project )
|
36
|
+
indicator = project.current ? "* " : " "
|
37
|
+
end
|
38
|
+
|
39
|
+
def tag_partial tags, title_length
|
40
|
+
max_length = ViewRecords::Project.max_title_length
|
41
|
+
max_length += ViewRecords::Project.max_depth * 2 if @options[:depth]
|
42
|
+
max_length += 6 if @options[:id]
|
43
|
+
max_length += 2 if @options[:active]
|
44
|
+
spacer = [0, max_length - title_length].max
|
45
|
+
view = " " + ( " " * spacer )
|
46
|
+
return view + "tags: none" if tags.length < 1
|
47
|
+
|
48
|
+
view += "tags: ["
|
49
|
+
tags.each { |t| view += "#{t}, "}
|
50
|
+
view[0..-3] + "]"
|
51
|
+
end
|
52
|
+
|
53
|
+
def id_partial id
|
54
|
+
@options[:id] ? "[#{id}] ".rjust(6, ' ') : ""
|
55
|
+
end
|
56
|
+
|
57
|
+
def project_block record
|
58
|
+
|
59
|
+
record.format do |r|
|
60
|
+
@options[:active] = @options.fetch( :active, false )
|
61
|
+
record = r.title
|
62
|
+
|
63
|
+
id = id_partial r.id
|
64
|
+
active = @options[:active] ? active_indicator( r ) : ""
|
65
|
+
depth = @options[:depth] ? " " * r.depth : ""
|
66
|
+
title = r.title
|
67
|
+
view = "#{id}#{active}#{depth}#{title}"
|
68
|
+
tags = @options[:tags] ? tag_partial( r.tags, view.length ) : ""
|
69
|
+
view += tags
|
70
|
+
puts view
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def timerecord_block record
|
75
|
+
record.format do |r|
|
76
|
+
id = id_partial r.id
|
77
|
+
running = r.running ? "*" : " "
|
78
|
+
description = r.description.empty? ? "#{r.project}" : "#{r.project}: #{r.description}"
|
79
|
+
view = "#{id}#{r.start_time.strftime('%H:%M')} - #{r.end_time.strftime('%H:%M')}#{running} [#{r.duration.format}] #{description}"
|
80
|
+
puts view
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Views
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def project_view project, depth=0
|
6
|
+
ViewRecords::Project.new project, depth: depth
|
7
|
+
end
|
8
|
+
|
9
|
+
def projects_list_view projects=Tempo::Model::Project.index, parent=:root, depth=0
|
10
|
+
return no_items( "projects" ) if projects.empty?
|
11
|
+
|
12
|
+
Tempo::Model::Project.sort_by_title projects do |projects|
|
13
|
+
projects.each do |p|
|
14
|
+
|
15
|
+
if p.parent == parent
|
16
|
+
project_view p, depth
|
17
|
+
|
18
|
+
if not p.children.empty?
|
19
|
+
next_depth = depth + 1
|
20
|
+
next_parent = p.id
|
21
|
+
child_array = projects_list_view projects, next_parent, next_depth
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# list of sorted projects, no hierarchy
|
29
|
+
def projects_flat_list_view projects=Tempo::Model::Project.index
|
30
|
+
|
31
|
+
Tempo::Model::Project.sort_by_title projects do |projects|
|
32
|
+
projects.each do |p|
|
33
|
+
project_view p
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def project_added project
|
39
|
+
ViewRecords::Message.new "added project:"
|
40
|
+
project_view project
|
41
|
+
end
|
42
|
+
|
43
|
+
def project_deleted project
|
44
|
+
ViewRecords::Message.new "deleted project:"
|
45
|
+
project_view project
|
46
|
+
end
|
47
|
+
|
48
|
+
def project_checkout project
|
49
|
+
ViewRecords::Message.new "switched to project:"
|
50
|
+
project_view project
|
51
|
+
end
|
52
|
+
|
53
|
+
def project_already_current project
|
54
|
+
ViewRecords::Message.new "already on project:"
|
55
|
+
project_view project
|
56
|
+
end
|
57
|
+
|
58
|
+
def project_tags project
|
59
|
+
ViewRecords::Message.new "altered project tags:"
|
60
|
+
project_view project
|
61
|
+
end
|
62
|
+
|
63
|
+
def ambiguous_project( matches, command )
|
64
|
+
|
65
|
+
ViewRecords::Message.new "The following projects matched your search:"
|
66
|
+
|
67
|
+
Tempo::Views::Reporter.add_options active: true
|
68
|
+
projects_flat_list_view matches
|
69
|
+
|
70
|
+
ViewRecords::Message.new "please refine your search or use --exact to match args exactly"
|
71
|
+
|
72
|
+
ViewRecords::Message.new "cannot #{command} multiple projects", category: :error
|
73
|
+
end
|
74
|
+
|
75
|
+
def project_assistance
|
76
|
+
ViewRecords::Message.new "you need to set up a new project before running your command"
|
77
|
+
ViewRecords::Message.new "run`tempo project --help` for more information"
|
78
|
+
no_items "projects", :error
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Views
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def report_records_view options={}
|
6
|
+
|
7
|
+
projects = options.fetch( :projects, Tempo::Model::Project.index )
|
8
|
+
return no_items( "projects" ) if projects.empty?
|
9
|
+
|
10
|
+
time_records = options.fetch( :time_records, Tempo::Model::TimeRecord.days_index )
|
11
|
+
return no_items( "time records" ) if time_records.empty?
|
12
|
+
|
13
|
+
time_records.each do |d_id, days_record|
|
14
|
+
|
15
|
+
day = Tempo::Model::TimeRecord.day_id_to_time d_id
|
16
|
+
ViewRecords::Message.new ""
|
17
|
+
ViewRecords::Message.new day.strftime("Records for %m/%d/%Y:")
|
18
|
+
|
19
|
+
days_record.each do |time_record|
|
20
|
+
time_record_view time_record
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# The Views Reporter is responsible for presenting all the views
|
2
|
+
# using the specified formatters. The Reporter is initialized in the GLI pre block,
|
3
|
+
# where all additional formats are added.
|
4
|
+
# The reporter executes the reports during the post block, displaying all views that
|
5
|
+
# have been added by the views.
|
6
|
+
#
|
7
|
+
# class instance variables:
|
8
|
+
#
|
9
|
+
# @@formats
|
10
|
+
# an array of formatters, which will be passed the view records on exit
|
11
|
+
# Reporter will always run the error formater first, to check for errors in
|
12
|
+
# the view reports, followed by all added formatters, and then finally the screen
|
13
|
+
# formatter. This allows additional formatters to add view records, which
|
14
|
+
# will be presented on screen.
|
15
|
+
#
|
16
|
+
# @@view_records
|
17
|
+
# add view_records
|
18
|
+
|
19
|
+
module Tempo
|
20
|
+
module Views
|
21
|
+
|
22
|
+
class Reporter
|
23
|
+
@@formats
|
24
|
+
@@view_records
|
25
|
+
@@options
|
26
|
+
|
27
|
+
class << self
|
28
|
+
attr_accessor :view_records
|
29
|
+
|
30
|
+
def add_format *formats
|
31
|
+
@@formats ||= []
|
32
|
+
formats.each {|format| @@formats << format}
|
33
|
+
end
|
34
|
+
|
35
|
+
def formats
|
36
|
+
@@formats ||= []
|
37
|
+
end
|
38
|
+
|
39
|
+
def options
|
40
|
+
@@options ||= {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_options options
|
44
|
+
@@options ||= {}
|
45
|
+
@@options.merge! options
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_view_record record
|
49
|
+
@@view_records ||= []
|
50
|
+
|
51
|
+
if /Views::ViewRecords/.match record.class.name
|
52
|
+
@@view_records << record
|
53
|
+
else
|
54
|
+
raise InvalidViewRecordError
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def view_records
|
59
|
+
@@view_records ||= []
|
60
|
+
end
|
61
|
+
|
62
|
+
def report
|
63
|
+
# TODO send records to added formatters
|
64
|
+
screen_formatter = Formatters::Screen.new
|
65
|
+
screen_formatter.format_records view_records, options
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Views
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def time_record_view time_record
|
6
|
+
ViewRecords::TimeRecord.new time_record
|
7
|
+
end
|
8
|
+
|
9
|
+
def start_time_record_view time_record
|
10
|
+
ViewRecords::Message.new "time record started:"
|
11
|
+
time_record_view time_record
|
12
|
+
end
|
13
|
+
|
14
|
+
def end_time_record_view time_record
|
15
|
+
ViewRecords::Message.new "time record ended:"
|
16
|
+
time_record_view time_record
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_time_record_view time_record
|
20
|
+
ViewRecords::Message.new "time record updated:"
|
21
|
+
time_record_view time_record
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete_time_record_view time_record
|
25
|
+
ViewRecords::Message.new "time record deleted:"
|
26
|
+
time_record_view time_record
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|