tempo-cli 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|