tempo-cli 0.1.6 → 0.2.1

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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile.lock +3 -3
  3. data/README.md +9 -3
  4. data/bin/tempo +20 -2
  5. data/features/arrange.feature +1 -1
  6. data/features/directory.feature +108 -0
  7. data/features/project.feature +2 -2
  8. data/features/report.feature +3 -2
  9. data/features/start.feature +0 -1
  10. data/features/step_definitions/tempo_steps.rb +99 -20
  11. data/features/update.feature +7 -1
  12. data/lib/file_record/directory.rb +19 -3
  13. data/lib/file_record/file_utility.rb +122 -0
  14. data/lib/file_record/record.rb +36 -83
  15. data/lib/tempo/controllers/arrange_controller.rb +5 -5
  16. data/lib/tempo/controllers/base.rb +8 -8
  17. data/lib/tempo/controllers/checkout_controller.rb +4 -4
  18. data/lib/tempo/controllers/end_controller.rb +4 -5
  19. data/lib/tempo/controllers/projects_controller.rb +13 -10
  20. data/lib/tempo/controllers/records_controller.rb +8 -5
  21. data/lib/tempo/controllers/report_controller.rb +4 -4
  22. data/lib/tempo/controllers/start_controller.rb +4 -3
  23. data/lib/tempo/controllers/update_controller.rb +22 -8
  24. data/lib/tempo/exceptions.rb +2 -2
  25. data/lib/tempo/models/base.rb +26 -18
  26. data/lib/tempo/models/composite.rb +5 -3
  27. data/lib/tempo/models/log.rb +61 -38
  28. data/lib/tempo/models/project.rb +9 -6
  29. data/lib/tempo/models/time_record.rb +14 -14
  30. data/lib/tempo/version.rb +1 -1
  31. data/lib/tempo/views/arrange_view.rb +4 -4
  32. data/lib/tempo/views/base.rb +10 -23
  33. data/lib/tempo/views/formatters/base.rb +2 -2
  34. data/lib/tempo/views/formatters/screen.rb +7 -7
  35. data/lib/tempo/views/projects_view.rb +8 -8
  36. data/lib/tempo/views/report_view.rb +5 -5
  37. data/lib/tempo/views/reporter.rb +4 -4
  38. data/lib/tempo/views/time_record_view.rb +5 -5
  39. data/lib/tempo/views/view_records/base.rb +8 -8
  40. data/lib/tempo/views/view_records/composite.rb +4 -4
  41. data/lib/tempo/views/view_records/log.rb +3 -3
  42. data/lib/tempo/views/view_records/project.rb +3 -3
  43. data/lib/tempo/views/view_records/time_record.rb +5 -5
  44. data/lib/tempo.rb +3 -0
  45. data/lib/time_utilities.rb +4 -4
  46. data/tempo-cli.gemspec +8 -7
  47. data/test/lib/file_record/directory_test.rb +14 -1
  48. data/test/lib/file_record/record_test.rb +40 -75
  49. data/test/lib/tempo/models/base_test.rb +2 -2
  50. data/test/lib/tempo/models/composite_test.rb +9 -9
  51. data/test/lib/tempo/models/log_test.rb +31 -16
  52. data/test/lib/tempo/models/time_record_test.rb +29 -19
  53. data/test/support/factories.rb +5 -0
  54. data/test/support/helpers.rb +7 -7
  55. metadata +40 -53
@@ -1,65 +1,20 @@
1
+ # Handles the CRUD of base, composite and log models
2
+ # relies on file utility to manage the directories and filenames.
3
+
4
+ require 'yaml'
5
+
1
6
  module FileRecord
2
7
  class Record
3
8
  class << self
4
9
 
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
10
+ # record a child of Tempo::Model::Base
11
+ def save_model( model, options={} )
52
12
 
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
13
+ options = options.dup
14
+ options[:create] = true
15
+ options[:destroy] = true
57
16
 
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 )
17
+ file_path = FileUtility.new(model, options).file_path
63
18
 
64
19
  File.open( file_path,'a' ) do |f|
65
20
  model.index.each do |m|
@@ -69,52 +24,50 @@ module FileRecord
69
24
  end
70
25
 
71
26
  # record a child of Tempo::Model::Log
72
- def save_log( model )
73
- dir = log_dir model
27
+ def save_log( model, options={} )
28
+
29
+ options = options.dup
30
+ options[:create] = true
31
+ options[:destroy] = true
74
32
 
75
33
  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
34
 
80
- # don't write to an empty file
35
+ options[:time] = day
36
+ ut = FileUtility.new(model, options)
37
+
38
+ # don't create an empty file
81
39
  next if days_logs.empty?
82
40
 
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
41
+ ut.save_instances_to_file days_logs
88
42
  end
89
43
  end
90
44
 
91
- def read_instances( model, file )
45
+ # Used by read_model and read_log to load all instances from a file
46
+ #
47
+ def read_instances( model, file, options={} )
92
48
  instances = YAML::load_stream( File.open( file ) )
93
49
  instances.each do |i|
94
50
  model.new( i )
95
51
  end
96
52
  end
97
53
 
98
- def read_model( model )
99
- file = File.join(Dir.home,'tempo', model.file)
100
- read_instances model, file
101
- end
54
+ # Read in all models instances from the model file
55
+ def read_model( model, options={} )
102
56
 
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
57
+ file_path = FileUtility.new(model, options).file_path
58
+ read_instances model, file_path
109
59
  end
110
- end
111
60
 
112
- def update
61
+ # Read in all log model instances from a time stamped file
62
+ def read_log( model, time, options={} )
113
63
 
114
- end
115
-
116
- def delete
64
+ options[:time] = time
65
+ file_path = FileUtility.new(model, options).file_path
117
66
 
67
+ if File.exists? file_path
68
+ read_instances model, file_path
69
+ end
70
+ end
118
71
  end
119
72
  end
120
73
  end
@@ -5,7 +5,7 @@ module Tempo
5
5
 
6
6
  class << self
7
7
 
8
- def parse options, args
8
+ def parse(options, args)
9
9
 
10
10
  return Views.project_assistance if Model::Project.index.empty?
11
11
 
@@ -29,23 +29,23 @@ module Tempo
29
29
  end
30
30
  end
31
31
 
32
- def make_root_project options, args
32
+ def make_root_project(options, args)
33
33
  root = match_project :arrange, options, args
34
34
  if root.parent == :root
35
35
  Views::arrange_already_root root
36
36
  else
37
37
  parent = match_project :arrange, {id: true}, root.parent
38
38
  parent.remove_child root
39
- @projects.save_to_file
39
+ @projects.save_to_file options
40
40
  Views::arrange_root root
41
41
  end
42
42
  end
43
43
 
44
- def make_child_project options, parent_args, child_args
44
+ def make_child_project(options, parent_args, child_args)
45
45
  parent = match_project :arrange, options, parent_args
46
46
  child = match_project :arrange, options, child_args
47
47
  parent << child
48
- @projects.save_to_file
48
+ @projects.save_to_file options
49
49
  Views::arrange_parent_child parent, child
50
50
  end
51
51
  end #class << self
@@ -3,7 +3,7 @@ module Tempo
3
3
  class Base
4
4
  class << self
5
5
 
6
- def filter_projects_by_title options, args
6
+ def filter_projects_by_title(options, args)
7
7
  if options[:exact]
8
8
  match = reassemble_the args
9
9
  match = [match]
@@ -18,7 +18,7 @@ module Tempo
18
18
  # that match positively against every
19
19
  # member of the matches array
20
20
  #
21
- def fuzzy_match haystack, matches, attribute="id"
21
+ def fuzzy_match(haystack, matches, attribute="id")
22
22
 
23
23
  matches = [matches] unless matches.is_a? Array
24
24
 
@@ -38,7 +38,7 @@ module Tempo
38
38
  # a flag in the front. The value is also added back intto the
39
39
  # front of the original array
40
40
 
41
- def reassemble_the args, flag=nil
41
+ def reassemble_the(args, flag=nil)
42
42
  assembled = ""
43
43
  args.unshift flag if flag
44
44
  args.each { |a| assembled += " #{a}" }
@@ -48,7 +48,7 @@ module Tempo
48
48
  private
49
49
 
50
50
  # TODO: escape regex characters ., (), etc.
51
- def match_to_regex match, type=:fuzzy
51
+ def match_to_regex(match, type=:fuzzy)
52
52
  match.downcase!
53
53
  if type == :exact
54
54
  /^#{match}$/
@@ -57,7 +57,7 @@ module Tempo
57
57
  end
58
58
  end
59
59
 
60
- def fuzzy_array_match haystack, matches
60
+ def fuzzy_array_match(haystack, matches)
61
61
  results = []
62
62
  matches.each do |m|
63
63
  reg = match_to_regex m
@@ -70,7 +70,7 @@ module Tempo
70
70
  haystack
71
71
  end
72
72
 
73
- def model_match haystack, matches, attribute, type=:fuzzy
73
+ def model_match(haystack, matches, attribute, type=:fuzzy)
74
74
  attribute = "@#{attribute}".to_sym
75
75
  contenders = haystack.index
76
76
  results = []
@@ -85,7 +85,7 @@ module Tempo
85
85
  contenders
86
86
  end
87
87
 
88
- def match_project command, options, args
88
+ def match_project(command, options, args)
89
89
  if options[:id]
90
90
  match = @projects.find_by_id args[0]
91
91
  Views::no_match_error( "projects", "id=#{args[0]}" ) if not match
@@ -99,7 +99,7 @@ module Tempo
99
99
 
100
100
  # verify one and only one match returned in match array
101
101
  # returns the single match
102
- def single_match matches, request, command
102
+ def single_match(matches, request, command)
103
103
 
104
104
  if matches.length == 0
105
105
  Views::no_match_error "projects", request
@@ -5,7 +5,7 @@ module Tempo
5
5
 
6
6
  class << self
7
7
 
8
- def add_project options, args
8
+ def add_project(options, args)
9
9
  request = reassemble_the args, options[:add]
10
10
 
11
11
  if @projects.include? request
@@ -13,12 +13,12 @@ module Tempo
13
13
 
14
14
  else
15
15
  project = @projects.new({ title: request, current: true })
16
- @projects.save_to_file
16
+ @projects.save_to_file options
17
17
  Views::project_checkout project
18
18
  end
19
19
  end
20
20
 
21
- def existing_project options, args
21
+ def existing_project(options, args)
22
22
 
23
23
  match = match_project :checkout, options, args
24
24
 
@@ -27,7 +27,7 @@ module Tempo
27
27
  Views::project_already_current match
28
28
  else
29
29
  @projects.current = match
30
- @projects.save_to_file
30
+ @projects.save_to_file options
31
31
  Views::project_checkout match
32
32
  end
33
33
  end
@@ -8,7 +8,7 @@ module Tempo
8
8
 
9
9
  class << self
10
10
 
11
- def end_timer options, args
11
+ def end_timer(options, args)
12
12
 
13
13
  return Views.project_assistance if Model::Project.index.empty?
14
14
 
@@ -20,17 +20,16 @@ module Tempo
20
20
 
21
21
  return Views.no_match_error( "valid timeframe", options[:at], false ) if not time_out
22
22
 
23
- options = { end_time: time_out }
23
+ options[:end_time] = time_out
24
24
  options[:description] = reassemble_the args
25
25
 
26
- @time_records.load_last_day
26
+ @time_records.load_last_day options
27
27
  record = @time_records.current
28
-
29
28
  return Views.no_items( "running time records", :error ) if ! record
30
29
 
31
30
  record.end_time = time_out
32
31
  record.description = options[:description] if options[:description]
33
- @time_records.save_to_file
32
+ @time_records.save_to_file options
34
33
 
35
34
  Views.end_time_record_view record
36
35
 
@@ -5,13 +5,16 @@ module Tempo
5
5
 
6
6
  class << self
7
7
 
8
- def load
9
- if File.exists?( File.join( ENV['HOME'], 'tempo', @projects.file ))
10
- @projects.read_from_file
8
+ def load(options={})
9
+
10
+ directory = options.fetch( :directory, ENV['HOME'])
11
+
12
+ if File.exists?( File.join( directory, 'tempo', @projects.file ))
13
+ @projects.read_from_file options
11
14
  end
12
15
  end
13
16
 
14
- def index options, args
17
+ def index(options, args)
15
18
 
16
19
  request = reassemble_the args
17
20
 
@@ -39,7 +42,7 @@ module Tempo
39
42
  end
40
43
  end
41
44
 
42
- def add options, args, tags=nil
45
+ def add(options, args, tags=nil)
43
46
  request = reassemble_the args
44
47
 
45
48
  if @projects.include? request
@@ -52,13 +55,13 @@ module Tempo
52
55
  @projects.current = project
53
56
  end
54
57
 
55
- @projects.save_to_file
58
+ @projects.save_to_file options
56
59
 
57
60
  Views::project_added project
58
61
  end
59
62
  end
60
63
 
61
- def delete options, args
64
+ def delete(options, args)
62
65
 
63
66
  reassemble_the args, options[:delete]
64
67
  match = match_project :delete, options, args
@@ -70,7 +73,7 @@ module Tempo
70
73
 
71
74
  if @projects.index.include?(match)
72
75
  match.delete
73
- @projects.save_to_file
76
+ @projects.save_to_file options
74
77
  Views::projects_list_view if options[:list]
75
78
  Views::project_deleted match
76
79
  end
@@ -78,7 +81,7 @@ module Tempo
78
81
  end
79
82
 
80
83
  # add a project with tags, or tag or untag an existing project
81
- def tag options, args
84
+ def tag(options, args)
82
85
 
83
86
  # TODO @projects_find_by_tag if args.empty?
84
87
 
@@ -96,7 +99,7 @@ module Tempo
96
99
  if match
97
100
  match.tag tags
98
101
  match.untag untags
99
- @projects.save_to_file
102
+ @projects.save_to_file options
100
103
  Views::project_tags match
101
104
  end
102
105
  end
@@ -5,17 +5,20 @@ module Tempo
5
5
 
6
6
  class << self
7
7
 
8
- def initialize_from_records options, args
9
- if File.exists?( File.join( ENV['HOME'], 'tempo' ))
8
+ def initialize_from_records(options, args)
10
9
 
11
- Tempo::Controllers::Projects.load
10
+ dir = File.join( options.fetch( :directory, ENV['HOME']))
11
+
12
+ if File.exists?(File.join(dir, 'tempo'))
13
+
14
+ Tempo::Controllers::Projects.load directory: dir
12
15
 
13
16
  else
14
- FileRecord::Directory.create_new
17
+ FileRecord::Directory.create_new directory: dir
15
18
  end
16
19
  end
17
20
 
18
21
  end #class << self
19
22
  end
20
23
  end
21
- end
24
+ end
@@ -6,7 +6,7 @@ module Tempo
6
6
 
7
7
  class << self
8
8
 
9
- def report options, args
9
+ def report(options, args)
10
10
 
11
11
  return Tempo::Views.project_assistance if Tempo::Model::Project.index.empty?
12
12
 
@@ -21,7 +21,7 @@ module Tempo
21
21
  to = Time.parse options[:to]
22
22
  return Views.no_match_error( "valid timeframe", options[:to], false ) if to.nil?
23
23
 
24
- @time_records.load_days_records from, to
24
+ @time_records.load_days_records from, to, options
25
25
 
26
26
  error_timeframe = " from #{from.strftime('%m/%d/%Y')} to #{to.strftime('%m/%d/%Y')}"
27
27
 
@@ -29,7 +29,7 @@ module Tempo
29
29
  # current day record
30
30
  #
31
31
  elsif args.empty?
32
- @time_records.load_last_day
32
+ @time_records.load_last_day options
33
33
 
34
34
  # arguments have been supplied,
35
35
  # so we return the records for a single day
@@ -40,7 +40,7 @@ module Tempo
40
40
  day = Time.parse time
41
41
  return Views.no_match_error( "valid timeframe", time, false ) if day.nil?
42
42
 
43
- @time_records.load_day_record day
43
+ @time_records.load_day_record day, options
44
44
 
45
45
  error_timeframe = " on #{day.strftime('%m/%d/%Y')}"
46
46
  end
@@ -8,7 +8,7 @@ module Tempo
8
8
 
9
9
  class << self
10
10
 
11
- def start_timer options, args
11
+ def start_timer(options, args)
12
12
 
13
13
  return Views.project_assistance if Model::Project.index.empty?
14
14
 
@@ -28,9 +28,10 @@ module Tempo
28
28
  return Views.no_match_error( "valid timeframe", options[:end], false ) if time_out.nil?
29
29
  opts[:end_time] = time_out
30
30
  end
31
- @time_records.load_last_day
31
+
32
+ @time_records.load_last_day options
32
33
  record = @time_records.new(opts)
33
- @time_records.save_to_file
34
+ @time_records.save_to_file options
34
35
 
35
36
  Views.start_time_record_view record
36
37
 
@@ -8,20 +8,22 @@ module Tempo
8
8
 
9
9
  class << self
10
10
 
11
- def parse options, args
11
+ def parse(options, args)
12
12
 
13
13
  reassemble_the args
14
14
 
15
15
  return Views.project_assistance if Model::Project.index.empty?
16
16
 
17
+ # Load last day, or specific day if options includes an on-date
17
18
  if options[:on]
18
19
  day = Time.parse options[:on]
19
20
  return Views.no_match_error( "valid timeframe", options[:from], false ) if day.nil?
20
- @time_records.load_day_record day
21
+ @time_records.load_day_record day, options
21
22
  else
22
- day = @time_records.load_last_day
23
+ day = @time_records.load_last_day options
23
24
  end
24
25
 
26
+ # Load the last record, or record by id if options includes an id
25
27
  if options[:id]
26
28
  record = @time_records.find_by_id( options[:id], day )
27
29
  return Views.no_match_error( "time record on #{day.strftime('%m/%d/%Y')}", "id = #{options[:id]}", false ) if !record
@@ -30,13 +32,22 @@ module Tempo
30
32
  return Views.no_items( "time records on #{day.strftime('%m/%d/%Y')}", :error ) if ! record
31
33
  end
32
34
 
33
-
35
+ # DELETE and existing record, no need to check for further updates
34
36
  if options[:delete]
35
- record.delete
36
- @time_records.save_to_file
37
+
38
+ # If only record on the given day, delete the file
39
+ if Tempo::Model::TimeRecord.ids(record.d_id).length == 1
40
+ @time_records.delete_day_record record.d_id, options
41
+ else
42
+ record.delete
43
+ @time_records.save_to_file options
44
+ end
45
+
37
46
  Views.delete_time_record_view record
38
47
 
39
- else
48
+ else # check for flags and update one or all attributes
49
+
50
+ # Update the START time of the record
40
51
  if options[:start]
41
52
  start_time = Time.parse options[:start]
42
53
  return Views.no_match_error( "valid timeframe", options[:at], false ) if start_time.nil?
@@ -49,6 +60,7 @@ module Tempo
49
60
  end
50
61
  end
51
62
 
63
+ # Update the END time of the record
52
64
  if options[:end]
53
65
  end_time = Time.parse options[:end]
54
66
  return Views.no_match_error( "valid timeframe", options[:at], false ) if end_time.nil?
@@ -61,14 +73,16 @@ module Tempo
61
73
  end
62
74
  end
63
75
 
76
+ # Update the PROJECT
64
77
  if options[:project]
65
78
  record.project = @projects.current.id
66
79
  end
67
80
 
81
+ # Update the DESCRIPTION
68
82
  options[:description] = reassemble_the args
69
83
  record.description = options[:description] if options[:description] && !options[:description].empty?
70
84
 
71
- @time_records.save_to_file
85
+ @time_records.save_to_file options
72
86
  Views.update_time_record_view record
73
87
  end
74
88
  end
@@ -19,7 +19,7 @@ module Tempo
19
19
  #
20
20
  class TimeConflictError < ArgumentError
21
21
 
22
- def initialize( start_time=nil, end_time=nil, target_start_time=nil, target_end_time=nil )
22
+ def initialize(start_time=nil, end_time=nil, target_start_time=nil, target_end_time=nil)
23
23
 
24
24
  @end_time = (end_time.kind_of? Time) ? end_time.strftime('%H:%M') : end_time.to_s
25
25
  @end_time = " - #{@end_time}" if !@end_time.empty?
@@ -36,4 +36,4 @@ module Tempo
36
36
  @message
37
37
  end
38
38
  end
39
- end
39
+ end