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,120 @@
|
|
1
|
+
module FileRecord
|
2
|
+
class Record
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# record text as a string, and all objects as yaml
|
6
|
+
# don't write over an existing document unless :force = true
|
7
|
+
# @options file file path to record to
|
8
|
+
# @options record string or object to record
|
9
|
+
# options
|
10
|
+
# - force: true, overwrite file
|
11
|
+
# - format: 'yaml', 'string'
|
12
|
+
def create( file, record, options={} )
|
13
|
+
|
14
|
+
if record.is_a?(String)
|
15
|
+
format = options.fetch(:format, 'string')
|
16
|
+
else
|
17
|
+
format = options.fetch(:format, 'yaml')
|
18
|
+
end
|
19
|
+
|
20
|
+
if File.exists?(file)
|
21
|
+
raise ArgumentError.new "file already exists" unless options[:force]
|
22
|
+
end
|
23
|
+
|
24
|
+
File.open( file,'w' ) do |f|
|
25
|
+
|
26
|
+
case format
|
27
|
+
when 'yaml'
|
28
|
+
f.puts YAML::dump( record )
|
29
|
+
when 'string'
|
30
|
+
f.puts record
|
31
|
+
else
|
32
|
+
f.puts record
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def log_dirname( model )
|
38
|
+
dir_name = model.name[14..-1].gsub(/([A-Z])/, '_\1').downcase
|
39
|
+
dir = "tempo#{dir_name}s"
|
40
|
+
end
|
41
|
+
|
42
|
+
def log_dir( model )
|
43
|
+
dir_name = log_dirname model
|
44
|
+
dir = File.join(Dir.home,'tempo', dir_name)
|
45
|
+
Dir.mkdir(dir, 0700) unless File.exists?(dir)
|
46
|
+
dir
|
47
|
+
end
|
48
|
+
|
49
|
+
def log_filename( model, time )
|
50
|
+
file = "#{model.day_id( time )}.yaml"
|
51
|
+
end
|
52
|
+
|
53
|
+
def model_filename( model )
|
54
|
+
file_name = model.name[14..-1].gsub(/([A-Z])/, '_\1').downcase
|
55
|
+
file = "tempo#{file_name}s.yaml"
|
56
|
+
end
|
57
|
+
|
58
|
+
# record a child of Tempo::Model::Base
|
59
|
+
def save_model( model )
|
60
|
+
file = model_filename model
|
61
|
+
file_path = File.join(Dir.home,'tempo', file)
|
62
|
+
File.delete( file_path ) if File.exists?( file_path )
|
63
|
+
|
64
|
+
File.open( file_path,'a' ) do |f|
|
65
|
+
model.index.each do |m|
|
66
|
+
f.puts YAML::dump( m.freeze_dry )
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# record a child of Tempo::Model::Log
|
72
|
+
def save_log( model )
|
73
|
+
dir = log_dir model
|
74
|
+
|
75
|
+
model.days_index.each do |day, days_logs|
|
76
|
+
file = "#{day.to_s}.yaml"
|
77
|
+
file_path = File.join(dir, file)
|
78
|
+
File.delete( file_path ) if File.exists?( file_path )
|
79
|
+
|
80
|
+
# don't write to an empty file
|
81
|
+
next if days_logs.empty?
|
82
|
+
|
83
|
+
File.open( file_path,'a' ) do |f|
|
84
|
+
days_logs.each do |log|
|
85
|
+
f.puts YAML::dump( log.freeze_dry )
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def read_instances( model, file )
|
92
|
+
instances = YAML::load_stream( File.open( file ) )
|
93
|
+
instances.each do |i|
|
94
|
+
model.new( i )
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def read_model( model )
|
99
|
+
file = File.join(Dir.home,'tempo', model.file)
|
100
|
+
read_instances model, file
|
101
|
+
end
|
102
|
+
|
103
|
+
def read_log( model, time )
|
104
|
+
dir = File.join(Dir.home,'tempo', model.dir)
|
105
|
+
file = File.join(dir, model.file( time ))
|
106
|
+
if File.exists? file
|
107
|
+
read_instances model, file
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def update
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
def delete
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Controllers
|
3
|
+
class Arrange < Tempo::Controllers::Base
|
4
|
+
@projects = Model::Project
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def parse options, args
|
9
|
+
|
10
|
+
return Views::arrange_parse_error unless args.include? ":"
|
11
|
+
|
12
|
+
parent_args = []
|
13
|
+
child_args = []
|
14
|
+
in_parent = true
|
15
|
+
args.each do |a|
|
16
|
+
if a != ":"
|
17
|
+
in_parent ? parent_args << a : child_args << a
|
18
|
+
else
|
19
|
+
in_parent = false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if parent_args.empty?
|
24
|
+
make_root_project options, child_args
|
25
|
+
else
|
26
|
+
make_child_project options, parent_args, child_args
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def make_root_project options, args
|
31
|
+
root = match_project :arrange, options, args
|
32
|
+
if root.parent == :root
|
33
|
+
Views::arrange_already_root root
|
34
|
+
else
|
35
|
+
parent = match_project :arrange, {id: true}, root.parent
|
36
|
+
parent.remove_child root
|
37
|
+
@projects.save_to_file
|
38
|
+
Views::arrange_root root
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def make_child_project options, parent_args, child_args
|
43
|
+
parent = match_project :arrange, options, parent_args
|
44
|
+
child = match_project :arrange, options, child_args
|
45
|
+
parent << child
|
46
|
+
@projects.save_to_file
|
47
|
+
Views::arrange_parent_child parent, child
|
48
|
+
end
|
49
|
+
end #class << self
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Controllers
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def filter_projects_by_title options, args
|
7
|
+
if options[:exact]
|
8
|
+
match = reassemble_the args
|
9
|
+
match = [match]
|
10
|
+
model_match @projects, match, "title", :exact
|
11
|
+
else
|
12
|
+
model_match @projects, args, "title", :fuzzy
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Takes an array of source strings
|
17
|
+
# and filters them down to the ones
|
18
|
+
# that match positively against every
|
19
|
+
# member of the matches array
|
20
|
+
#
|
21
|
+
def fuzzy_match haystack, matches, attribute="id"
|
22
|
+
|
23
|
+
matches = [matches] unless matches.is_a? Array
|
24
|
+
|
25
|
+
if haystack.is_a? Array
|
26
|
+
fuzzy_array_match( haystack, matches )
|
27
|
+
|
28
|
+
elsif haystack.superclass == Model::Base
|
29
|
+
model_match( haystack, matches, attribute )
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Gli default behavior: When args are sent in a
|
34
|
+
# command without quotes, they are broken into an array,
|
35
|
+
# and the first block is passed to a flag if present.
|
36
|
+
#
|
37
|
+
# Here we reassemble the string, and add value stored in
|
38
|
+
# a flag in the front. The value is also added back intto the
|
39
|
+
# front of the original array
|
40
|
+
|
41
|
+
def reassemble_the args, flag=nil
|
42
|
+
assembled = ""
|
43
|
+
args.unshift flag if flag
|
44
|
+
args.each { |a| assembled += " #{a}" }
|
45
|
+
assembled.strip!
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# TODO: escape regex characters ., (), etc.
|
51
|
+
def match_to_regex match, type=:fuzzy
|
52
|
+
match.downcase!
|
53
|
+
if type == :exact
|
54
|
+
/^#{match}$/
|
55
|
+
else
|
56
|
+
/#{match}/
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def fuzzy_array_match haystack, matches
|
61
|
+
results = []
|
62
|
+
matches.each do |m|
|
63
|
+
reg = match_to_regex m
|
64
|
+
haystack.each do |h|
|
65
|
+
results << h if reg.match h
|
66
|
+
end
|
67
|
+
haystack = results
|
68
|
+
results = []
|
69
|
+
end
|
70
|
+
haystack
|
71
|
+
end
|
72
|
+
|
73
|
+
def model_match haystack, matches, attribute, type=:fuzzy
|
74
|
+
attribute = "@#{attribute}".to_sym
|
75
|
+
contenders = haystack.index
|
76
|
+
results = []
|
77
|
+
matches.each do |m|
|
78
|
+
reg = match_to_regex m, type
|
79
|
+
contenders.each do |c|
|
80
|
+
results << c if reg.match c.instance_variable_get(attribute).to_s.downcase
|
81
|
+
end
|
82
|
+
contenders = results
|
83
|
+
results = []
|
84
|
+
end
|
85
|
+
contenders
|
86
|
+
end
|
87
|
+
|
88
|
+
def match_project command, options, args
|
89
|
+
if options[:id]
|
90
|
+
match = @projects.find_by_id args[0]
|
91
|
+
Views::no_match_error( "projects", "id=#{args[0]}" ) if not match
|
92
|
+
else
|
93
|
+
matches = filter_projects_by_title options, args
|
94
|
+
request = reassemble_the args
|
95
|
+
match = single_match matches, request, command
|
96
|
+
end
|
97
|
+
match
|
98
|
+
end
|
99
|
+
|
100
|
+
# verify one and only one match returned in match array
|
101
|
+
# returns the single match
|
102
|
+
def single_match matches, request, command
|
103
|
+
|
104
|
+
if matches.length == 0
|
105
|
+
Views::no_match_error "projects", request
|
106
|
+
return false
|
107
|
+
elsif matches.length > 1
|
108
|
+
Views::ambiguous_project matches, command
|
109
|
+
return false
|
110
|
+
else
|
111
|
+
match = matches[0]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Controllers
|
3
|
+
class Checkout < Tempo::Controllers::Base
|
4
|
+
@projects = Model::Project
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def add_project options, args
|
9
|
+
request = reassemble_the args, options[:add]
|
10
|
+
|
11
|
+
if @projects.include? request
|
12
|
+
Views::already_exists_error "project", request
|
13
|
+
|
14
|
+
else
|
15
|
+
project = @projects.new({ title: request, current: true })
|
16
|
+
@projects.save_to_file
|
17
|
+
Views::project_checkout project
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def existing_project options, args
|
22
|
+
|
23
|
+
match = match_project :checkout, options, args
|
24
|
+
|
25
|
+
if match
|
26
|
+
if @projects.current == match
|
27
|
+
Views::project_already_current match
|
28
|
+
else
|
29
|
+
@projects.current = match
|
30
|
+
@projects.save_to_file
|
31
|
+
Views::project_checkout match
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def assistance
|
37
|
+
Views::checkout_assistance
|
38
|
+
end
|
39
|
+
end #class << self
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'chronic'
|
2
|
+
|
3
|
+
module Tempo
|
4
|
+
module Controllers
|
5
|
+
class End < Tempo::Controllers::Base
|
6
|
+
@projects = Model::Project
|
7
|
+
@time_records = Model::TimeRecord
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
def end_timer options, args
|
12
|
+
|
13
|
+
return Views.project_assistance if Model::Project.index.empty?
|
14
|
+
|
15
|
+
if not options[:at]
|
16
|
+
time_out = Time.new()
|
17
|
+
else
|
18
|
+
time_out = Time.parse options[:at]
|
19
|
+
end
|
20
|
+
|
21
|
+
return Views.no_match_error( "valid timeframe", options[:at], false ) if not time_out
|
22
|
+
|
23
|
+
options = { end_time: time_out }
|
24
|
+
options[:description] = reassemble_the args
|
25
|
+
|
26
|
+
@time_records.load_last_day
|
27
|
+
record = @time_records.current
|
28
|
+
|
29
|
+
return Views.no_items( "running time records", :error ) if ! record
|
30
|
+
|
31
|
+
record.end_time = time_out
|
32
|
+
record.description = options[:description] if options[:description]
|
33
|
+
@time_records.save_to_file
|
34
|
+
|
35
|
+
Views.end_time_record_view record
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end #class << self
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Controllers
|
3
|
+
class Projects < Tempo::Controllers::Base
|
4
|
+
@projects = Model::Project
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def load
|
9
|
+
if File.exists?( File.join( ENV['HOME'], 'tempo', @projects.file ))
|
10
|
+
@projects.read_from_file
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def index options, args
|
15
|
+
|
16
|
+
request = reassemble_the args
|
17
|
+
|
18
|
+
if args.empty?
|
19
|
+
Views::projects_list_view
|
20
|
+
|
21
|
+
else
|
22
|
+
matches = filter_projects_by_title options, args
|
23
|
+
|
24
|
+
if matches.empty?
|
25
|
+
Views::no_match_error "projects", request
|
26
|
+
|
27
|
+
else
|
28
|
+
Views::projects_list_view matches
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def show_active
|
34
|
+
if @projects.index.empty?
|
35
|
+
Views::no_items "projects"
|
36
|
+
else
|
37
|
+
Views::Reporter.add_options active: true
|
38
|
+
Views::project_view @projects.current
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def add options, args, tags=nil
|
43
|
+
request = reassemble_the args
|
44
|
+
|
45
|
+
if @projects.include? request
|
46
|
+
Views::already_exists_error "project", request
|
47
|
+
|
48
|
+
else
|
49
|
+
project = @projects.new({ title: request, tags: tags })
|
50
|
+
|
51
|
+
if @projects.index.length == 1
|
52
|
+
@projects.current = project
|
53
|
+
end
|
54
|
+
|
55
|
+
@projects.save_to_file
|
56
|
+
|
57
|
+
Views::project_added project
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete options, args
|
62
|
+
|
63
|
+
reassemble_the args, options[:delete]
|
64
|
+
match = match_project :delete, options, args
|
65
|
+
|
66
|
+
if match
|
67
|
+
if match == @projects.current
|
68
|
+
return Views::ViewRecords::Message.new "cannot delete the active project", category: :error
|
69
|
+
end
|
70
|
+
|
71
|
+
if @projects.index.include?(match)
|
72
|
+
match.delete
|
73
|
+
@projects.save_to_file
|
74
|
+
Views::projects_list_view if options[:list]
|
75
|
+
Views::project_deleted match
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# add a project with tags, or tag or untag an existing project
|
81
|
+
def tag options, args
|
82
|
+
|
83
|
+
# TODO @projects_find_by_tag if args.empty?
|
84
|
+
|
85
|
+
tags = options[:tag].split if options[:tag]
|
86
|
+
untags = options[:untag].split if options[:untag]
|
87
|
+
|
88
|
+
# add a new project
|
89
|
+
if options[:add]
|
90
|
+
add options, args, tags
|
91
|
+
|
92
|
+
else
|
93
|
+
command = options[:tag] ? "tag" : "untag"
|
94
|
+
match = match_project command, options, args
|
95
|
+
|
96
|
+
if match
|
97
|
+
match.tag tags
|
98
|
+
match.untag untags
|
99
|
+
@projects.save_to_file
|
100
|
+
Views::project_tags match
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end #class << self
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Controllers
|
3
|
+
class Records < Tempo::Controllers::Base
|
4
|
+
@projects = Model::Project
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def initialize_from_records options, args
|
9
|
+
if File.exists?( File.join( ENV['HOME'], 'tempo' ))
|
10
|
+
|
11
|
+
Tempo::Controllers::Projects.load
|
12
|
+
|
13
|
+
else
|
14
|
+
FileRecord::Directory.create_new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end #class << self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Tempo
|
2
|
+
module Controllers
|
3
|
+
class Report < Tempo::Controllers::Base
|
4
|
+
@projects = Model::Project
|
5
|
+
@time_records = Model::TimeRecord
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def report options, args
|
10
|
+
|
11
|
+
return Tempo::Views.project_assistance if Tempo::Model::Project.index.empty?
|
12
|
+
|
13
|
+
# A from flag has been supplied by the user
|
14
|
+
# and possible a to flag as well,
|
15
|
+
# so we return a period of day records
|
16
|
+
#
|
17
|
+
if options[:from] != "last record"
|
18
|
+
from = Time.parse options[:from]
|
19
|
+
return Views.no_match_error( "valid timeframe", options[:from], false ) if from.nil?
|
20
|
+
|
21
|
+
to = Time.parse options[:to]
|
22
|
+
return Views.no_match_error( "valid timeframe", options[:to], false ) if to.nil?
|
23
|
+
|
24
|
+
@time_records.load_days_records from, to
|
25
|
+
|
26
|
+
error_timeframe = " from #{from.strftime('%m/%d/%Y')} to #{to.strftime('%m/%d/%Y')}"
|
27
|
+
|
28
|
+
# no arguments or flags have been supplied, so we return the
|
29
|
+
# current day record
|
30
|
+
#
|
31
|
+
elsif args.empty?
|
32
|
+
@time_records.load_last_day
|
33
|
+
|
34
|
+
# arguments have been supplied,
|
35
|
+
# so we return the records for a single day
|
36
|
+
#
|
37
|
+
else
|
38
|
+
time = reassemble_the args
|
39
|
+
|
40
|
+
day = Time.parse time
|
41
|
+
return Views.no_match_error( "valid timeframe", time, false ) if day.nil?
|
42
|
+
|
43
|
+
@time_records.load_day_record day
|
44
|
+
|
45
|
+
error_timeframe = " on #{day.strftime('%m/%d/%Y')}"
|
46
|
+
end
|
47
|
+
|
48
|
+
return Views.no_items( "time records#{error_timeframe}", :error ) if @time_records.index.empty?
|
49
|
+
|
50
|
+
Views.report_records_view
|
51
|
+
end
|
52
|
+
end #class << self
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'chronic'
|
2
|
+
|
3
|
+
module Tempo
|
4
|
+
module Controllers
|
5
|
+
class Start < Tempo::Controllers::Base
|
6
|
+
@projects = Model::Project
|
7
|
+
@time_records = Model::TimeRecord
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
def start_timer options, args
|
12
|
+
|
13
|
+
return Views.project_assistance if Model::Project.index.empty?
|
14
|
+
|
15
|
+
if not options[:at]
|
16
|
+
time_in = Time.new()
|
17
|
+
else
|
18
|
+
time_in = Time.parse options[:at]
|
19
|
+
end
|
20
|
+
|
21
|
+
return Views.no_match_error( "valid timeframe", options[:at], false ) if time_in.nil?
|
22
|
+
|
23
|
+
opts = { start_time: time_in }
|
24
|
+
opts[:description] = reassemble_the args
|
25
|
+
|
26
|
+
if options[:end]
|
27
|
+
time_out = Time.parse options[:end]
|
28
|
+
return Views.no_match_error( "valid timeframe", options[:end], false ) if time_out.nil?
|
29
|
+
opts[:end_time] = time_out
|
30
|
+
end
|
31
|
+
@time_records.load_last_day
|
32
|
+
record = @time_records.new(opts)
|
33
|
+
@time_records.save_to_file
|
34
|
+
|
35
|
+
Views.start_time_record_view record
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end #class << self
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'chronic'
|
2
|
+
|
3
|
+
module Tempo
|
4
|
+
module Controllers
|
5
|
+
class Update < Tempo::Controllers::Base
|
6
|
+
@projects = Model::Project
|
7
|
+
@time_records = Model::TimeRecord
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
def parse options, args
|
12
|
+
|
13
|
+
reassemble_the args
|
14
|
+
|
15
|
+
return Views.project_assistance if Model::Project.index.empty?
|
16
|
+
|
17
|
+
if options[:on]
|
18
|
+
day = Time.parse options[:on]
|
19
|
+
return Views.no_match_error( "valid timeframe", options[:from], false ) if day.nil?
|
20
|
+
@time_records.load_day_record day
|
21
|
+
else
|
22
|
+
day = @time_records.load_last_day
|
23
|
+
end
|
24
|
+
|
25
|
+
if options[:id]
|
26
|
+
record = @time_records.find_by_id( options[:id], day )
|
27
|
+
return Views.no_match_error( "time record on #{day.strftime('%m/%d/%Y')}", "id = #{options[:id]}", false ) if !record
|
28
|
+
else
|
29
|
+
record = @time_records.index.last
|
30
|
+
return Views.no_items( "time records on #{day.strftime('%m/%d/%Y')}", :error ) if ! record
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
if options[:delete]
|
35
|
+
record.delete
|
36
|
+
@time_records.save_to_file
|
37
|
+
Views.delete_time_record_view record
|
38
|
+
|
39
|
+
else
|
40
|
+
if options[:start]
|
41
|
+
start_time = Time.parse options[:start]
|
42
|
+
return Views.no_match_error( "valid timeframe", options[:at], false ) if start_time.nil?
|
43
|
+
|
44
|
+
# TODO: add "today " to start time and try again if not valid
|
45
|
+
if record.valid_start_time? start_time
|
46
|
+
record.start_time = start_time
|
47
|
+
else
|
48
|
+
return Views::ViewRecords::Message.new "cannot change start time to #{start_time.strftime('%H:%M')}", category: :error
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if options[:end]
|
53
|
+
end_time = Time.parse options[:end]
|
54
|
+
return Views.no_match_error( "valid timeframe", options[:at], false ) if end_time.nil?
|
55
|
+
|
56
|
+
# TODO: add "today " to end time and try again if not valid
|
57
|
+
if record.valid_end_time? end_time
|
58
|
+
record.end_time = end_time
|
59
|
+
else
|
60
|
+
return Views::ViewRecords::Message.new "cannot change end time to #{end_time.strftime('%H:%M')}", category: :error
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if options[:project]
|
65
|
+
record.project = @projects.current.id
|
66
|
+
end
|
67
|
+
|
68
|
+
options[:description] = reassemble_the args
|
69
|
+
record.description = options[:description] if options[:description] && !options[:description].empty?
|
70
|
+
|
71
|
+
@time_records.save_to_file
|
72
|
+
Views.update_time_record_view record
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end #class << self
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|