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,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
|