tempo-cli 0.2.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 59fac2395c13e4a0a58701053e5ed9696146bb44
4
- data.tar.gz: 42df9a9ed12b2a50771cd0b9e64500f346bacef7
3
+ metadata.gz: fea9fde6712531a15750a50792f48596161e24f7
4
+ data.tar.gz: 9cf7aa5a815d65f23fde2da454a274334d4d04e8
5
5
  SHA512:
6
- metadata.gz: 26a42ed193477f0dde232ef0209d70899d1fdc1b338e27779c5714d73f1f6b8499db8d1de0f0f81955783067f97ed2b533d336f61aafd3f11a016a18a3fa26d5
7
- data.tar.gz: 77f7093821bd7a097f7cc7df90a7ea4426ff81acf10c776e3799af78a2ec6315744a82eb4e5ffedc7c556d76a32ea65c55bc64096afcfaaff8d5581e5db50d84
6
+ metadata.gz: e482886b1748a24e458f7c3b44ae2638f5746d25bbf8c5b4a5834fd878730f6f5f1bf2451bce3bc0e672f8908300c42a3ff92baf73d7df6fc2c8ac8721cbf1eb
7
+ data.tar.gz: eac361590d878b3414d557a0c672d8c5d139420e27efcf69c6022daabb268caa5cabab30daa1f6d88b2d4e440a86b96933caa2366d192824cfadd57cf370a563
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tempo-cli (0.2.5)
4
+ tempo-cli (1.0.0)
5
5
  chronic (~> 0.10)
6
6
  gli (~> 2.10)
7
7
 
@@ -27,7 +27,7 @@ GEM
27
27
  ffi (1.9.0)
28
28
  gherkin (2.12.0)
29
29
  multi_json (~> 1.3)
30
- gli (2.11.0)
30
+ gli (2.12.0)
31
31
  json (1.8.1)
32
32
  method_source (0.8.1)
33
33
  multi_json (1.7.7)
data/README.md CHANGED
@@ -4,7 +4,13 @@ A command line interface for time tracking by project.
4
4
 
5
5
  ## Installation
6
6
 
7
- gem install tempo-cli
7
+ gem install tempo-cli
8
+
9
+ ## Version 1.0
10
+
11
+ If you are updatging from version ~> 0.2.6 to 1.0, there is a new structure to the time records - time records are now kept in sub-folders by year.
12
+
13
+ Run `tempo clean` to make a backup of your old directory, move files into the proper folders, and check all earlier records for errors.
8
14
 
9
15
  ### binary
10
16
 
@@ -18,11 +24,9 @@ Tempo cli runs with the binary `tempo`, but you can alias a shorter binary for y
18
24
 
19
25
  Tempo tracks time against projects. Projects can be nested, and tagged, and each time entry can have an additional description. Time reports are produced by day.
20
26
 
21
- Future enhancements will include reports by project and time totals by day or by project.
22
-
23
27
  ### Records
24
28
 
25
- All records are produced in YAML files. These reports can be edited by hand, but keep in mind that any invalid data could cause problems when it is read back into the app. Make sure time formatting is valid and that they don't overlap, and that all ids are unique per page.
29
+ All records are produced in YAML files. These records can be edited by hand, but keep in mind that any invalid data could cause problems when it is read back into the app. Make sure time formatting is valid and that they don't overlap, and that all ids are unique per page.
26
30
 
27
31
  Each day's time records are designed to work independently. When adding or updating time records, only the records for the days in question are loaded into the app. The most recent day's records will also always be read in, to assure no running records are created earlier than existing records.
28
32
 
@@ -235,8 +239,9 @@ The description, if passed in, will be used to replace the existing description.
235
239
 
236
240
  ##### command options
237
241
 
238
- --from=time - begin time records on this date
239
- --to=time - end time records on this date
242
+ -f, --from=time - begin time records on this date
243
+ -o, --order=[project, date] - order reports by date or project
244
+ -t, --to=time - end time records on this date
240
245
 
241
246
  Reports time entries for a day or a period of days. By default, lists the last recorded day's entries. To list a different day, supply a day as the args. To list a series of records, include a --from and --to value
242
247
 
@@ -254,6 +259,9 @@ The description, if passed in, will be used to replace the existing description.
254
259
  # report a period of days
255
260
  $ tempo report -f 'last monday' -t 'last friday'
256
261
 
262
+ # organize reports as bullet lists under each project
263
+ $ tempo report --o p
264
+
257
265
  ## Assumptions and Limitations
258
266
 
259
267
  Before you begin using tempo, you must have at least one project. Projects can be tagged, and organized as sub-projects. See project in the commands section below, for more information. To view your projects file, look at ~/tempo/tempo_projects.yaml
data/bin/tempo CHANGED
@@ -464,6 +464,34 @@ command [:update, :u] do |c|
464
464
  end
465
465
  end
466
466
 
467
+ # COMMAND: CLEAN
468
+
469
+ desc 'Clean the tempo file structure'
470
+ long_desc "Creates a backup of the existing tempo directory,
471
+ and then clean all files in the current directory.
472
+
473
+ Cleaning the projects assures that:
474
+
475
+ 1. All projects that have been renamed have been updated in the files.
476
+
477
+ 2. Any files with time record conflicts are caught and can be corrected."
478
+
479
+ command [:clean] do |c|
480
+ require 'readline'
481
+ c.action do |global_options, options, args|
482
+ controller = Tempo::Controllers::Records
483
+
484
+ confirm = Tempo::Views::interactive_confirm_clean
485
+
486
+ if confirm.positive_response?
487
+ controller.backup_records options, args
488
+ controller.clean_records options, args
489
+ else
490
+ Tempo::Views::message "Cancelling clean"
491
+ end
492
+ end
493
+ end
494
+
467
495
  pre do |global,command,options,args|
468
496
 
469
497
  # add reporting of all arguments
@@ -53,9 +53,9 @@ Given /^an alternate directory and an existing project file$/ do
53
53
  end
54
54
 
55
55
  Given /^an existing time record file$/ do
56
- @records_directory = File.join(ENV['HOME'], 'tempo/tempo_time_records')
56
+ @records_directory = File.join(ENV['HOME'], 'tempo/tempo_time_records', '2014')
57
57
  FileUtils.rm_r(@records_directory) if File.exists?(@records_directory)
58
- Dir.mkdir(@records_directory, 0700)
58
+ FileUtils.mkdir_p @records_directory
59
59
  projects_file = File.join(@records_directory, '20140101.yaml')
60
60
 
61
61
  File.open(projects_file,'w') do |f|
@@ -107,7 +107,7 @@ end
107
107
 
108
108
 
109
109
  Then /^the time record (.*?) should contain "(.*?)" at line (\d+)$/ do |arg1, arg2, arg3|
110
- file = File.join(ENV['HOME'], 'tempo/tempo_time_records', "#{arg1}.yaml")
110
+ file = File.join(ENV['HOME'], 'tempo/tempo_time_records', "#{arg1[0..3]}", "#{arg1}.yaml")
111
111
  contents = []
112
112
  File.open(file, "r") do |f|
113
113
  f.readlines.each do |line|
@@ -118,7 +118,7 @@ Then /^the time record (.*?) should contain "(.*?)" at line (\d+)$/ do |arg1, ar
118
118
  end
119
119
 
120
120
  Then /^the time record (.*?) should not contain "(.*?)"$/ do |arg1, arg2|
121
- file = File.join(ENV['HOME'], 'tempo/tempo_time_records', "#{arg1}.yaml")
121
+ file = File.join(ENV['HOME'], 'tempo/tempo_time_records', "#{arg1[0..3]}", "#{arg1}.yaml")
122
122
  contents = []
123
123
  File.open(file, "r") do |f|
124
124
  f.readlines.each do |line|
@@ -129,7 +129,7 @@ Then /^the time record (.*?) should not contain "(.*?)"$/ do |arg1, arg2|
129
129
  end
130
130
 
131
131
  Then /^the time record (.*?) should not exist$/ do |arg1|
132
- file = File.join(ENV['HOME'], 'tempo/tempo_time_records', "#{arg1}.yaml")
132
+ file = File.join(ENV['HOME'], 'tempo/tempo_time_records', "#{arg1[0..3]}", "#{arg1}.yaml")
133
133
  File.exists?(file).should == false
134
134
  end
135
135
 
@@ -200,7 +200,7 @@ Then /^the alternate directory (.*?) file should not contain "(.*?)"$/ do |arg1,
200
200
  end
201
201
 
202
202
  Then /^the alternate directory time record (\d+) should contain "(.*?)" at line (\d+)$/ do |arg1, arg2, arg3|
203
- file = File.join(ENV['HOME'], 'alt_dir', 'tempo', 'tempo_time_records', "#{arg1}.yaml")
203
+ file = File.join(ENV['HOME'], 'alt_dir', 'tempo', 'tempo_time_records', "#{arg1[0..3]}", "#{arg1}.yaml")
204
204
  contents = []
205
205
  File.open(file, "r") do |f|
206
206
  f.readlines.each do |line|
@@ -211,7 +211,7 @@ Then /^the alternate directory time record (\d+) should contain "(.*?)" at line
211
211
  end
212
212
 
213
213
  Then /^the alternate directory time record (.*?) should not exist$/ do |arg1|
214
- file = File.join(ENV['HOME'], 'alt_dir', 'tempo/tempo_time_records', "#{arg1}.yaml")
214
+ file = File.join(ENV['HOME'], 'alt_dir', 'tempo/tempo_time_records', "#{arg1[0..3]}", "#{arg1}.yaml")
215
215
  File.exists?(file).should == false
216
216
  end
217
217
 
@@ -1,3 +1,9 @@
1
+ require 'stringio'
2
+ require 'rubygems/package'
3
+ require 'rubygems/package'
4
+ require 'zlib'
5
+
6
+
1
7
  module FileRecord
2
8
  class Directory
3
9
 
@@ -14,7 +20,7 @@ module FileRecord
14
20
 
15
21
  def create_new(options={})
16
22
 
17
- directory = options.fetch( :directory, Dir.home )
23
+ directory = options.fetch(:directory, Dir.home)
18
24
  cwd = File.expand_path File.dirname(__FILE__)
19
25
  source = File.join(cwd, "directory_structure/tempo")
20
26
  if ! Dir.exists? directory
@@ -22,6 +28,103 @@ module FileRecord
22
28
  end
23
29
  FileUtils.cp_r source, directory
24
30
  end
31
+
32
+ # Backup the tempo directory to tempo_backup/tempo_backup_20140101_HrMnS.tar.gz
33
+ # pass in an optional directory (see create_new)
34
+ # Pass in a timestamp, or default to 20140101_120000
35
+ def backup(options={})
36
+ directory = options.fetch(:directory, Dir.home)
37
+ timestamp = options.fetch(:timestamp, Time.new.strftime("%Y%m%d_%H%M%S"))
38
+ source = File.join(directory, "tempo")
39
+ dest = File.join(directory, "tempo_backups")
40
+ Dir.mkdir dest unless Dir.exists? dest
41
+ destination = File.join(directory, "tempo_backups", "tempo_backup_#{timestamp}.tar.gz")
42
+
43
+ io = tar(source)
44
+ gz = gzip(io)
45
+
46
+ File.open(destination,"w") do |file|
47
+ file.binmode
48
+ file.write gz.read
49
+ end
50
+
51
+ # return the new directory name
52
+ destination
53
+ end
54
+
55
+ private
56
+
57
+ # COMPRESSION / DECOMPRESSION
58
+ # From: https://gist.github.com/sinisterchipmunk/1335041
59
+
60
+ # Creates a tar file in memory recursively
61
+ # from the given dir.
62
+ #
63
+ # Returns a StringIO whose underlying String
64
+ # is the contents of the tar file.
65
+ def tar(dir)
66
+ tarfile = StringIO.new("")
67
+ Gem::Package::TarWriter.new(tarfile) do |tar|
68
+ Dir[File.join(dir, "**/*")].each do |file|
69
+ mode = File.stat(file).mode
70
+ relative_file = file.sub /^#{Regexp::escape dir}\/?/, ''
71
+
72
+ if File.directory?(file)
73
+ tar.mkdir relative_file, mode
74
+ else
75
+ tar.add_file relative_file, mode do |tf|
76
+ File.open(file, "rb") { |f| tf.write f.read }
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ tarfile.rewind
83
+ tarfile
84
+ end
85
+
86
+ # gzips the underlying string in the given StringIO,
87
+ # returning a new StringIO representing the
88
+ # compressed file.
89
+ def gzip(tarfile)
90
+ gz = StringIO.new("")
91
+ z = Zlib::GzipWriter.new(gz)
92
+ z.write tarfile.string
93
+ z.close # this is necessary!
94
+
95
+ # z was closed to write the gzip footer, so
96
+ # now we need a new StringIO
97
+ StringIO.new gz.string
98
+ end
99
+
100
+ # un-gzips the given IO, returning the
101
+ # decompressed version as a StringIO
102
+ def ungzip(tarfile)
103
+ z = Zlib::GzipReader.new(tarfile)
104
+ unzipped = StringIO.new(z.read)
105
+ z.close
106
+ unzipped
107
+ end
108
+
109
+ # untars the given IO into the specified
110
+ # directory
111
+ def untar(io, destination)
112
+ Gem::Package::TarReader.new io do |tar|
113
+ tar.each do |tarfile|
114
+ destination_file = File.join destination, tarfile.full_name
115
+
116
+ if tarfile.directory?
117
+ FileUtils.mkdir_p destination_file
118
+ else
119
+ destination_directory = File.dirname(destination_file)
120
+ FileUtils.mkdir_p destination_directory unless File.directory?(destination_directory)
121
+ File.open destination_file, "wb" do |f|
122
+ f.print tarfile.read
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
25
128
  end
26
129
  end
27
130
  end
@@ -73,14 +73,28 @@ module FileRecord
73
73
  "#{sn[0]}_#{sn[-1]}s"
74
74
  end
75
75
 
76
- # Tempo::Model::Log -> Users/usrname/(alternate_directory/)tempo/tempo_logs/
76
+
77
+ def log_year_directory
78
+ if @time.kind_of? Time
79
+ @time.strftime("%Y")
80
+ else
81
+ @time[0..3]
82
+ end
83
+ end
84
+
85
+ # Tempo::Model::Log -> Users/usrname/(alternate_directory/)tempo/tempo_logs'
77
86
  # Will also create the directory if not found
78
- # This method does not require time to be present in options
79
- def log_directory_path
87
+ def log_main_directory_path
80
88
  dir = File.join(@directory, module_name, log_directory)
89
+ end
90
+
91
+ # Tempo::Model::Log -> Users/usrname/(alternate_directory/)tempo/tempo_logs/20XX
92
+ # Will also create the directory if not found
93
+ def log_directory_path
94
+ dir = File.join(log_main_directory_path, log_year_directory)
81
95
 
82
96
  if @create and !File.exists?(dir)
83
- Dir.mkdir(dir, 0700)
97
+ FileUtils.mkdir_p dir
84
98
  end
85
99
 
86
100
  dir
@@ -104,9 +118,37 @@ module FileRecord
104
118
  clean_path File.join(dir, filename)
105
119
  end
106
120
 
121
+ def old_style_log_records_exists?
122
+ return false if !File.exists?(log_main_directory_path)
123
+ Pathname.new(log_main_directory_path).children.each do |c|
124
+ return true if c.to_s.match(/\.yaml/)
125
+ end
126
+ false
127
+ end
128
+
129
+ def move_old_records
130
+ return false if !File.exists?(log_main_directory_path)
131
+ puts "moving files in #{log_main_directory_path}"
132
+ Pathname.new(log_main_directory_path).children.each do |c|
133
+ if c.to_s.match(/\.yaml/)
134
+ year = File.basename(c).match(/(^\d{4})/)[1]
135
+ raise Tempo::DuplicateRecordError.new(File.join(log_main_directory_path,year,File.basename(c))) if File.exists?(File.join(log_main_directory_path,year,File.basename(c)))
136
+ FileUtils.mkdir_p File.join(log_main_directory_path,year) if !File.exists? File.join(log_main_directory_path,year)
137
+ FileUtils.cp c, File.join(log_main_directory_path,year,File.basename(c))
138
+ FileUtils.rm c
139
+ end
140
+ end
141
+ end
142
+
107
143
  # Returns the list of log records from a log directory
108
144
  def log_records
109
- Dir[log_directory_path + "/*.yaml"].sort!
145
+ records = []
146
+ return records if !File.exists?(log_main_directory_path)
147
+ years = Pathname.new(log_main_directory_path).children.select { |c| c.directory? }
148
+ years.each do |dir|
149
+ records = records | Dir[dir.to_s + "/*.yaml"]
150
+ end
151
+ records.sort!
110
152
  end
111
153
 
112
154
  # remove existing file when passed destroy:true in options
@@ -42,15 +42,6 @@ module FileRecord
42
42
  end
43
43
  end
44
44
 
45
- # Used by read_model and read_log to load all instances from a file
46
- #
47
- def read_instances( model, file, options={} )
48
- instances = YAML::load_stream( File.open( file ) )
49
- instances.each do |i|
50
- model.new( i )
51
- end
52
- end
53
-
54
45
  # Read in all models instances from the model file
55
46
  def read_model( model, options={} )
56
47
 
@@ -68,6 +59,16 @@ module FileRecord
68
59
  read_instances model, file_path
69
60
  end
70
61
  end
62
+
63
+ protected
64
+
65
+ # Used by read_model and read_log to load all instances from a file
66
+ def read_instances( model, file, options={} )
67
+ instances = YAML::load_stream( File.open( file ) )
68
+ instances.each do |i|
69
+ model.new( i )
70
+ end
71
+ end
71
72
  end
72
73
  end
73
74
  end
@@ -13,7 +13,7 @@ module Tempo
13
13
  return Views.project_assistance if Model::Project.index.empty?
14
14
 
15
15
  if not options[:at]
16
- time_out = Time.new()
16
+ time_out = Time.new().round
17
17
  else
18
18
  time_out = Time.parse options[:at]
19
19
  end
@@ -7,17 +7,69 @@ module Tempo
7
7
 
8
8
  def initialize_from_records(options, args)
9
9
 
10
- dir = File.join( options.fetch( :directory, ENV['HOME']))
10
+ dir = options.fetch( :directory, ENV['HOME'])
11
11
 
12
12
  if File.exists?(File.join(dir, 'tempo'))
13
-
14
13
  Tempo::Controllers::Projects.load directory: dir
15
-
16
14
  else
17
15
  FileRecord::Directory.create_new directory: dir
18
16
  end
19
17
  end
20
18
 
19
+ def backup_records(options, args)
20
+ dir = options.fetch( :directory, ENV['HOME'])
21
+ Views::interactive_progress "\nBacking up #{dir}/tempo"
22
+
23
+ if File.exists?(File.join(dir, 'tempo'))
24
+ dest = FileRecord::Directory.backup directory: dir
25
+ Views::interactive_progress "Sucessfully created #{dest}"
26
+ else
27
+ Views::no_items("directory #{dir}/tempo", :error)
28
+ end
29
+ end
30
+
31
+ def old_records_present?(options)
32
+ file_utility = FileRecord::FileUtility.new(Tempo::Model::TimeRecord, options)
33
+ file_utility.old_style_log_records_exists?
34
+ end
35
+
36
+ def move_old_records(options)
37
+ file_utility = FileRecord::FileUtility.new(Tempo::Model::TimeRecord, options)
38
+ file_utility.move_old_records
39
+ end
40
+
41
+ def clean_records(options, args)
42
+ dir = File.join( options.fetch( :directory, ENV['HOME']), "tempo", "tempo_time_records")
43
+
44
+ if old_records_present? options
45
+
46
+ confirm = Tempo::Views::interactive_confirm_move_old_records
47
+
48
+ if confirm.positive_response?
49
+ move_old_records options
50
+ end
51
+ end
52
+
53
+ Views::interactive_progress "Loading records from #{dir}"
54
+
55
+ days = Model::TimeRecord.record_d_ids(options)
56
+
57
+ days.each do |d_id|
58
+ begin
59
+ date = "#{d_id[4..5].to_i}/#{d_id[6..7]}/#{d_id[0..3]}"
60
+ Views::interactive_progress_partial date
61
+ Model::TimeRecord.load_day_record(d_id, options)
62
+ Model::TimeRecord.save_to_file(options)
63
+ Model::TimeRecord.clear_all
64
+ rescue TimeConflictError => e
65
+ Views::message " exiting on error..."
66
+ Views::message "\nAn error occurred which prevented cleaning all the records on #{date}"
67
+ Views::message "Please repair the records in file #{dir}/#{Model::TimeRecord.file(d_id)}"
68
+ return Views::error e
69
+ end
70
+ end
71
+ Views::message "\nSuccess -- all files are clean!"
72
+ end
21
73
  end #class << self
22
74
  end
23
75
  end
@@ -11,7 +11,7 @@ module Tempo
11
11
  return Tempo::Views.project_assistance if Tempo::Model::Project.index.empty?
12
12
 
13
13
  # A from flag has been supplied by the user
14
- # and possible a to flag as well,
14
+ # and possibly a to flag,
15
15
  # so we return a period of day records
16
16
  #
17
17
  if options[:from] != "last record"
@@ -13,14 +13,14 @@ module Tempo
13
13
  return Views.project_assistance if Model::Project.index.empty?
14
14
 
15
15
  if not options[:at]
16
- start_time = Time.new()
16
+ start_time = Time.new().round()
17
17
  else
18
18
  start_time = Time.parse options[:at]
19
19
  end
20
20
 
21
21
  return Views.no_match_error( "valid timeframe", options[:at], false ) if start_time.nil?
22
22
 
23
- if start_time > Time.new()
23
+ if start_time > Time.new().round()
24
24
  Views.warning("WARNING: logging time in the future may cause trouble maintaining running records")
25
25
  end
26
26
 
@@ -70,4 +70,15 @@ module Tempo
70
70
  @message
71
71
  end
72
72
  end
73
+
74
+ class DuplicateRecordError < Exception
75
+
76
+ def initialize(file)
77
+ @message = "The file #{file} already exists"
78
+ end
79
+
80
+ def to_s
81
+ @message
82
+ end
83
+ end
73
84
  end
@@ -46,9 +46,13 @@ module Tempo
46
46
  FileRecord::FileUtility.new(self, {time: time}).filename
47
47
  end
48
48
 
49
+ def d_id_from_file(file)
50
+ /(\d+)\.yaml/.match(file)[1]
51
+ end
52
+
49
53
  # Returns the immediate directory for the log
50
54
  # Tempo::Model::MessageLog => tempo_message_logs
51
- def dir
55
+ def dir(time)
52
56
  FileRecord::FileUtility.new(self).log_directory
53
57
  end
54
58
 
@@ -58,6 +62,10 @@ module Tempo
58
62
  FileRecord::FileUtility.new(self, options).log_records
59
63
  end
60
64
 
65
+ def record_d_ids(options={})
66
+ records(options).each_with_object(Array.new) {|file,d_ids| d_ids << d_id_from_file(file)}
67
+ end
68
+
61
69
  # returns the loaded record with the latest start time
62
70
  # Only loads records if options[:load] is true,
63
71
  # otherwise assumes records are already loaded
@@ -102,10 +110,9 @@ module Tempo
102
110
 
103
111
  # Return a Time object for the last record's date
104
112
  def last_day(options={})
105
- reg = /(\d+)\.yaml/
106
113
  recs = records options
107
114
  if recs.last
108
- d_id = reg.match(recs.last)[1]
115
+ d_id = d_id_from_file(recs.last)
109
116
  time = day_id_to_time d_id if d_id
110
117
  return time
111
118
  end
@@ -131,6 +138,16 @@ module Tempo
131
138
  FileRecord::FileUtility.new(self, options).file_path
132
139
  end
133
140
 
141
+ # Normally not necessary to perform, only
142
+ # used when cleaning (and testing) records
143
+ def clear_all
144
+ @ids = {}
145
+ @index = []
146
+ @days_index = {}
147
+ @id_counter = {}
148
+ @current = nil
149
+ end
150
+
134
151
  # takes and integer, and time or day_id
135
152
  # and returns the instance that matches both
136
153
  # the id and d_id
@@ -41,6 +41,12 @@ module Tempo
41
41
  # super handles start_time, not end time
42
42
  options[:start_time] ||= Time.now
43
43
  @end_time = options.fetch :end_time, :running
44
+
45
+ if ! options[:exact_time]
46
+ options[:start_time] = options[:start_time].round unless options[:start_time] == :running
47
+ @end_time = @end_time.round unless @end_time == :running
48
+ end
49
+
44
50
  verify_times options[:start_time], @end_time
45
51
 
46
52
  super options
@@ -146,7 +152,7 @@ module Tempo
146
152
  if @end_time.kind_of? Time
147
153
  end_time = @end_time
148
154
  else
149
- end_time = Time.now()
155
+ end_time = Time.now().round
150
156
  end
151
157
  end_time.to_i - @start_time.to_i
152
158
  end
@@ -1,3 +1,3 @@
1
1
  module Tempo
2
- VERSION = '0.2.6'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -48,13 +48,13 @@ module Tempo
48
48
  def no_items(items, category=:info)
49
49
  ViewRecords::Message.new "no #{items} exist", category: category
50
50
  if items == "projects"
51
- ViewRecords::Message.new "You must at least one project before you can begin tracking time"
51
+ ViewRecords::Message.new "You must create at least one project before you can begin tracking time"
52
52
  ViewRecords::Message.new "run `tempo project --help` for more information"
53
53
  end
54
54
  end
55
55
 
56
- def message(message)
57
- ViewRecords::Message.new message, category: :info
56
+ def message(message, category=:info)
57
+ ViewRecords::Message.new message, category: category
58
58
  end
59
59
 
60
60
  def warning(message)
@@ -0,0 +1,44 @@
1
+ # The Console block handles interactive queries and progress reports.
2
+ # It is required when reporting must be done in 'real time' rather than compiled
3
+ # during runtime and then presented at the end (see Views::Screen). It is the only
4
+ # formatter that receives blocks as soon as they are handed to the Reporter
5
+
6
+ module Tempo
7
+ module Views
8
+ module Formatters
9
+
10
+ class Interactive < Tempo::Views::Formatters::Base
11
+
12
+
13
+ def message_block(record)
14
+ record.format do |m|
15
+ case m.category
16
+ when :immediate
17
+ puts "#{m.message}"
18
+ when :progress
19
+ puts "#{m.message}..."
20
+ when :progress_partial
21
+ $stdout.sync = true
22
+ print "#{m.message}..."
23
+ end
24
+ m.message
25
+ end
26
+ end
27
+
28
+ def query_block(query)
29
+ query.format do |q|
30
+ puts q.query
31
+ response = Readline.readline('> ', true)
32
+ end
33
+ end
34
+
35
+ def format_records_container(container)
36
+ # Pass through over-ride
37
+ # We don't allow interactive containers at this time because they
38
+ # would need to be able to detect when the container is complete.
39
+ # (report containers raised errors on nil durations).
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,11 +1,10 @@
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.
1
+ # Tempo View Formatters are triggered by the View Reporter, and all inherit from
2
+ # Views::Base
3
+ #
4
+ # The screen formatter is the primary formatter for reporting results back to the
5
+ # screen. All formatting is handled after the main processes, when the Reporter is
6
+ # invoked during the post block. (If immediate feedback is needed,
7
+ # see Formatters::Console)
9
8
 
10
9
  module Tempo
11
10
  module Views
@@ -31,7 +30,7 @@ module Tempo
31
30
  end
32
31
  end
33
32
 
34
- # PARTIALS --------------------------------------------------------------------/
33
+ # PARTIALS vv-----------------------------------------------------------------vv
35
34
 
36
35
  # spacer for project titles, active project marked with *
37
36
  def active_indicator(project)
@@ -56,7 +55,7 @@ module Tempo
56
55
  @options[:id] ? "[#{id}] ".rjust(6, ' ') : ""
57
56
  end
58
57
 
59
- # PARTIALS --------------------------------------------------------------------/
58
+ # PARTIALS ^^-----------------------------------------------------------------^^
60
59
 
61
60
 
62
61
  def project_block(record)
@@ -0,0 +1,32 @@
1
+ module Tempo
2
+ module Views
3
+ class << self
4
+
5
+ # Must be allowed to return results
6
+ def interactive_query(query)
7
+ ViewRecords::Query.new query
8
+ end
9
+
10
+ def interactive_progress(message)
11
+ ViewRecords::Message.new message, category: :progress
12
+ end
13
+
14
+ def interactive_progress_partial(message)
15
+ ViewRecords::Message.new message, category: :progress_partial
16
+ end
17
+
18
+ def interactive_confirm_clean
19
+ query = "\nCleaning Tempo records resaves all records, looking for errors.\n" +
20
+ "In the event that a record cannot be corrected, you wil be prompted to repair the record manually.\n" +
21
+ "A backup of the records will also be created before any changes are made.\n\n" +
22
+ "Do you wish to continue? [YyNn]"
23
+ interactive_query(query)
24
+ end
25
+
26
+ def interactive_confirm_move_old_records
27
+ query = "\nYou have files which match an older file structure, do you want to move them so they will be included in your records? [YyNn]"
28
+ interactive_query(query)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -6,6 +6,11 @@
6
6
  #
7
7
  # class instance variables:
8
8
  #
9
+ # @@console
10
+ # A shell formtter which receives view records as they are created and can
11
+ # intercept messages that should be displayed in real time, as well as interactive
12
+ # prompts
13
+ #
9
14
  # @@formats
10
15
  # an array of formatters, which will be passed the view records on exit
11
16
  # Reporter will always run the error formater first, to check for errors in
@@ -20,6 +25,7 @@ module Tempo
20
25
  module Views
21
26
 
22
27
  class Reporter
28
+ @@console
23
29
  @@formats
24
30
  @@view_records
25
31
  @@options
@@ -40,6 +46,12 @@ module Tempo
40
46
  @@options ||= {}
41
47
  end
42
48
 
49
+ # All records are sent directly to the console, so it can decide if
50
+ # action is required immediately based on the type of record
51
+ def console
52
+ @@console ||= Formatters::Interactive.new(options)
53
+ end
54
+
43
55
  def add_options(options)
44
56
  @@options ||= {}
45
57
  @@options.merge! options
@@ -50,6 +62,9 @@ module Tempo
50
62
 
51
63
  if /Views::ViewRecords/.match record.class.name
52
64
  @@view_records << record
65
+
66
+ # console must be able to return a value
67
+ return console.report record
53
68
  else
54
69
  raise InvalidViewRecordError
55
70
  end
@@ -46,6 +46,30 @@ module Tempo
46
46
  end
47
47
  end
48
48
 
49
+ # Query records are handled by the console formatter,
50
+ # returning results from a call to Readline
51
+ class Query
52
+ attr_accessor :type, :query, :match, :response
53
+
54
+ def initialize(query, options={})
55
+ @query = query
56
+ @type = "query"
57
+ @match = options.fetch(:match, /(y|Y)(es)?/)
58
+ @response = Reporter.add_view_record self
59
+ end
60
+
61
+ def positive_response?
62
+ true if @response.match(@match)
63
+ end
64
+
65
+ def format(&block)
66
+ # TODO: should we create an interactive default? using:
67
+ # confirm = Readline.readline('> ', true).match(/(y|Y)(es)?/)
68
+ block ||= lambda { |q| puts "#{q.query}"; Readline.readline('> ', true) }
69
+ response = block.call self
70
+ end
71
+ end
72
+
49
73
  # Specifically for managing a time duration, nested in other
50
74
  # view records. This can be used with a start and end time,
51
75
  # or used to manage a sum of times.
@@ -27,7 +27,7 @@ module Tempo
27
27
  @description = model.description
28
28
  @description ||= ""
29
29
  @duration = Duration.new model.duration
30
- @end_time = model.end_time == :running ? Time.now() : model.end_time
30
+ @end_time = model.end_time == :running ? Time.now().round : model.end_time
31
31
  @project = model.project_title
32
32
  @running = model.running?
33
33
  self.class.max_description_length @description.length
@@ -2,23 +2,22 @@ require 'test_helper'
2
2
 
3
3
  describe FileRecord do
4
4
 
5
- before do
5
+ def setup
6
6
  @dir = File.join(Dir.home,"tempo")
7
7
  @alt_dir = File.join(Dir.home, "testdir", "tempo")
8
8
  FileUtils.rm_r @dir if File.exists?(@dir)
9
9
  end
10
10
 
11
- after do
11
+ def teardown
12
12
  FileUtils.rm_r @dir if File.exists?(@dir)
13
13
  FileUtils.rm_r @alt_dir if File.exists?(@alt_dir)
14
14
  end
15
15
 
16
-
17
16
  describe "Directory" do
18
17
 
19
18
  describe "initialize" do
20
19
 
21
- it "should initialize a new directory structure" do
20
+ it "initializes a new directory structure" do
22
21
  project_file = File.join(@dir, "tempo_projects.yaml")
23
22
  readme = File.join(@dir, "README.txt")
24
23
 
@@ -28,7 +27,7 @@ describe FileRecord do
28
27
  File.exists?( readme ).must_equal true
29
28
  end
30
29
 
31
- it "should take an optional directory parameter" do
30
+ it "takes an optional directory parameter" do
32
31
  project_file = File.join(@alt_dir, "tempo_projects.yaml")
33
32
  readme = File.join(@alt_dir, "README.txt")
34
33
  dir = File.join(Dir.home, "testdir")
@@ -38,6 +37,10 @@ describe FileRecord do
38
37
  File.exists?( project_file ).must_equal true
39
38
  File.exists?( readme ).must_equal true
40
39
  end
40
+
41
+ it "creates a zipped backup directory" do
42
+
43
+ end
41
44
  end
42
45
  end
43
46
  end
@@ -28,13 +28,13 @@ describe FileRecord do
28
28
  end
29
29
  end
30
30
 
31
- describe "redording a Tempo Log" do
31
+ describe "recording a Tempo Log" do
32
32
 
33
33
  it "should create daily records containing each instance" do
34
- test_file_1 = File.join(ENV['HOME'],'tempo','tempo_message_logs', '20140101.yaml')
35
- File.delete( test_file ) if File.exists?( test_file_1 )
36
- test_file_2 = File.join(ENV['HOME'],'tempo','tempo_message_logs', '20140102.yaml')
37
- File.delete( test_file ) if File.exists?( test_file_2 )
34
+ test_file_1 = File.join(ENV['HOME'],'tempo','tempo_message_logs', '2014', '20140101.yaml')
35
+ File.delete( test_file_1 ) if File.exists?( test_file_1 )
36
+ test_file_2 = File.join(ENV['HOME'],'tempo','tempo_message_logs', '2014', '20140102.yaml')
37
+ File.delete( test_file_2 ) if File.exists?( test_file_2 )
38
38
 
39
39
  log_factory
40
40
  FileRecord::Record.save_log(Tempo::Model::MessageLog)
@@ -67,5 +67,8 @@ describe FileRecord do
67
67
  ":message: day 2 water the bonsai"]
68
68
  end
69
69
  end
70
+
71
+ describe "reading a Tempo Log" do
72
+ end
70
73
  end
71
74
  end
@@ -34,8 +34,8 @@ describe Tempo do
34
34
  end
35
35
 
36
36
  it "knows which directory to save to" do
37
- log_factory
38
- Tempo::Model::MessageLog.dir.must_equal "tempo_message_logs"
37
+ date = Time.new(2021,1,1)
38
+ Tempo::Model::MessageLog.dir(date).must_equal "tempo_message_logs"
39
39
  end
40
40
 
41
41
  it "knows which file name to save to" do
@@ -44,13 +44,14 @@ describe Tempo do
44
44
  Tempo::Model::MessageLog.file(date).must_equal "20140101.yaml"
45
45
  end
46
46
 
47
+ #TODO-NEW-LOG-DIR
47
48
  it "grants children the ability to write to a file" do
48
49
  log_factory
49
50
  test_dir = File.join(ENV['HOME'],'tempo','tempo_message_logs')
50
51
  FileUtils.rm_r test_dir if File.exists?(test_dir)
51
52
  Tempo::Model::MessageLog.save_to_file
52
- test_file_1 = File.join(test_dir, "20140101.yaml")
53
- test_file_2 = File.join(test_dir, "20140102.yaml")
53
+ test_file_1 = File.join(test_dir, "2014/20140101.yaml")
54
+ test_file_2 = File.join(test_dir, "2014/20140102.yaml")
54
55
  contents = eval_file_as_array( test_file_1 )
55
56
  # testing with regex because time zone will be different on different computers,
56
57
  # ex: ":start_time: 2014-01-02 07:15:00.000000000 -05:00"
@@ -92,6 +93,15 @@ describe Tempo do
92
93
  Tempo::Model::MessageLog.index[0].message.must_equal "day 1 pet the sheep"
93
94
  end
94
95
 
96
+ it "can clear records out after load" do
97
+ log_record_factory
98
+ time = Time.new(2014, 1, 1)
99
+ Tempo::Model::MessageLog.load_day_record time
100
+ Tempo::Model::MessageLog.ids( time ).must_equal [1,2,3]
101
+ Tempo::Model::MessageLog.clear_all
102
+ Tempo::Model::MessageLog.ids( time ).must_equal []
103
+ end
104
+
95
105
  it "knows the date of the last record" do
96
106
  log_record_factory
97
107
  last_day = Tempo::Model::MessageLog.last_day
@@ -275,8 +275,8 @@ describe Tempo do
275
275
  it "saves to file a collection of projects" do
276
276
  time_record_factory
277
277
  Tempo::Model::TimeRecord.save_to_file
278
- test_file_1 = File.join(ENV['HOME'],'tempo/tempo_time_records/20140101.yaml')
279
- test_file_2 = File.join(ENV['HOME'],'tempo/tempo_time_records/20140102.yaml')
278
+ test_file_1 = File.join(ENV['HOME'],'tempo/tempo_time_records/2014/20140101.yaml')
279
+ test_file_2 = File.join(ENV['HOME'],'tempo/tempo_time_records/2014/20140102.yaml')
280
280
  contents = eval_file_as_array( test_file_1 )
281
281
 
282
282
  # testing with regex because time zone will be different on different computers,
@@ -0,0 +1,32 @@
1
+ require "test_helper"
2
+
3
+ describe Tempo do
4
+ describe "Views" do
5
+ describe "Formatters" do
6
+ describe "Console" do
7
+
8
+ before do
9
+ view_records_factory
10
+ end
11
+
12
+ describe "Message View Records" do
13
+
14
+ it "outputs the message" do
15
+ record = @progress_message
16
+ formatter = Tempo::Views::Formatters::Interactive.new
17
+ out = capture_stdout { formatter.report record }
18
+
19
+ assert_equal "Making progress...\n", out.string
20
+ end
21
+ end
22
+
23
+ describe "Interactive View Records" do
24
+
25
+ it "outputs the interaction with a prompt" do
26
+ # pending
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -22,6 +22,12 @@ describe Tempo do
22
22
  proc { Tempo::Views::Reporter.add_view_record record }.must_raise Tempo::InvalidViewRecordError
23
23
  end
24
24
 
25
+ it "sends the reports immediately to the console formatter" do
26
+ Tempo::Views::Reporter.clear_records
27
+ out = capture_stdout { Tempo::Views::ViewRecords::Message.new "an immediate message to report", category: :immediate }
28
+ assert_equal "an immediate message to report\n", out.string
29
+ end
30
+
25
31
  it "sends the reports to the screen formatter on report" do
26
32
  Tempo::Views::Reporter.clear_records
27
33
  record_1 = Tempo::Views::ViewRecords::Message.new "a message to report"
@@ -37,4 +43,4 @@ describe Tempo do
37
43
  end
38
44
  end
39
45
  end
40
- end
46
+ end
@@ -27,16 +27,6 @@ module Tempo
27
27
  end
28
28
  end
29
29
 
30
- class Log
31
- def self.clear_all()
32
- @ids = {}
33
- @index = []
34
- @days_index = {}
35
- @id_counter = {}
36
- @current = nil
37
- end
38
- end
39
-
40
30
  class MessageLog < Tempo::Model::Log
41
31
  attr_accessor :message
42
32
 
@@ -133,10 +123,10 @@ end
133
123
  # For creating a file to load records from
134
124
  def log_record_factory
135
125
  Tempo::Model::MessageLog.clear_all
136
- test_dir = File.join(ENV['HOME'],'tempo','tempo_message_logs')
126
+ test_dir = File.join(ENV['HOME'],'tempo','tempo_message_logs','2014')
137
127
 
138
128
  FileUtils.rm_r test_dir if File.exists?(test_dir)
139
- Dir.mkdir(test_dir, 0700) unless File.exists?(test_dir)
129
+ FileUtils.mkdir_p(test_dir) unless File.exists?(test_dir)
140
130
  file_lines = ["---", ":start_time: 2014-01-01 07:15:00.000000000 -05:00",
141
131
  ":id: 1", ":message: day 1 pet the sheep",
142
132
  "---", ":start_time: 2014-01-01 07:45:00.000000000 -05:00",
@@ -170,13 +160,15 @@ end
170
160
 
171
161
  def view_records_factory
172
162
  time_record_factory
173
- @message_1 = Tempo::Views::ViewRecords::Message.new "All The Things I Did", class: :title
174
- @message_2 = Tempo::Views::ViewRecords::Message.new "on a busy busy day", class: :title
163
+ @message_1 = Tempo::Views::ViewRecords::Message.new "All The Things I Did"#, class: :title
164
+ @message_2 = Tempo::Views::ViewRecords::Message.new "on a busy busy day"#, class: :title
175
165
  @error = Tempo::Views::ViewRecords::Message.new "raising an error", category: :error
176
166
  @duration = Tempo::Views::ViewRecords::Duration.new 9600 # 2 hours and 40 minutes
177
167
  @project_1 = Tempo::Views::ViewRecords::Project.new @project_1
178
168
  @project_2 = Tempo::Views::ViewRecords::Project.new @project_2
179
169
  @time_record_1 = Tempo::Views::ViewRecords::TimeRecord.new @record_1
180
170
  @time_record_6 = Tempo::Views::ViewRecords::TimeRecord.new @record_6
181
- #@records = [@message, @project, @time_record]
171
+ @progress_message = Tempo::Views::ViewRecords::Message.new "Making progress", category: :progress
172
+ # change to interaction:
173
+ @interaction = Tempo::Views::ViewRecords::Message.new "Making progress", category: :progress
182
174
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tempo-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Gabel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-11 00:00:00.000000000 Z
11
+ date: 2014-10-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -158,7 +158,9 @@ files:
158
158
  - lib/tempo/views/arrange_view.rb
159
159
  - lib/tempo/views/base.rb
160
160
  - lib/tempo/views/formatters/base.rb
161
+ - lib/tempo/views/formatters/interactive.rb
161
162
  - lib/tempo/views/formatters/screen.rb
163
+ - lib/tempo/views/interactive.rb
162
164
  - lib/tempo/views/projects_view.rb
163
165
  - lib/tempo/views/report_view.rb
164
166
  - lib/tempo/views/reporter.rb
@@ -183,6 +185,7 @@ files:
183
185
  - test/lib/tempo/models/time_record_test.rb
184
186
  - test/lib/tempo/views/base_test.rb
185
187
  - test/lib/tempo/views/formatters/base_test.rb
188
+ - test/lib/tempo/views/formatters/console_test.rb
186
189
  - test/lib/tempo/views/formatters/screen_test.rb
187
190
  - test/lib/tempo/views/reporter_test.rb
188
191
  - test/lib/tempo/views/view_records/base_test.rb