syc-task 0.0.7 → 0.1.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ require 'yaml'
2
+ require_relative 'environment.rb'
3
+
4
+ # Syctask module implements functions for managing tasks
5
+ module Syctask
6
+
7
+ # Creates settings for syctask that are saved to files and retrieved when
8
+ # syctask is started
9
+ class Settings
10
+
11
+ # Creates general purpose tasks that can be tracked with syctask start. The
12
+ # general purpose files are saved to a file default_tasks in the syctask
13
+ # system directory
14
+ def tasks(tasks)
15
+ service = Syctask::TaskService.new
16
+ if File.exists? Syctask::DEFAULT_TASKS
17
+ general = YAML.load_file(Syctask::DEFAULT_TASKS)
18
+ else
19
+ general = {}
20
+ end
21
+ tasks.split(',').each do |task|
22
+ index = general.keys.find_index(task.upcase)
23
+ general[task.upcase] = service.create(Syctask::SYC_DIR,
24
+ {},
25
+ task.upcase) unless index
26
+ end
27
+ File.open(Syctask::DEFAULT_TASKS, 'w') {|f| YAML.dump(general, f)}
28
+ end
29
+
30
+ # Retrieves the general purpose files from the default_tasks file in the
31
+ # syctask system directory
32
+ def read_tasks
33
+ if File.exists? Syctask::DEFAULT_TASKS and not \
34
+ File.read(Syctask::DEFAULT_TASKS).empty?
35
+ YAML.load_file(Syctask::DEFAULT_TASKS)
36
+ else
37
+ {}
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,196 @@
1
+ require 'rainbow'
2
+ require_relative '../syctime/time_util.rb'
3
+ require_relative 'settings.rb'
4
+
5
+ include Syctime
6
+
7
+ module Syctask
8
+
9
+ # Creates statistics about the work and meeting times as well about the task
10
+ # processing
11
+ class Statistics
12
+
13
+ # Initializes the Statistics object with the general purpose tassk
14
+ def initialize
15
+ settings = Settings::new
16
+ tasks = settings.read_tasks
17
+ @general_purpose_tasks = tasks.nil? ? [] : tasks.keys
18
+ end
19
+
20
+ # Creates a statistics report
21
+ def report(file, from="", to=from)
22
+
23
+ from, to, time_log, count_log = logs(file, from, to)
24
+ working_days = time_log["work"].count.to_s if time_log["work"]
25
+ working_days ||= "0"
26
+ value_size = {key: 0, total: 0, min: 0, max: 0, average: 0}
27
+ report_lines = {}
28
+ report = sprintf("%s to %s", "#{from.strftime("%Y-%m-%d")}".bright,
29
+ "#{to.strftime("%Y-%m-%d")}".bright) +
30
+ sprintf(" (%s working days)\n", working_days.bright) +
31
+ sprintf("%24s", "Total".bright) +
32
+ sprintf("%26s", "Min ".bright) +
33
+ sprintf("%26s", "Max ".bright) +
34
+ sprintf("%29s", "Average\n".bright)
35
+ report << sprintf("%s\n", "Time".bright)
36
+ time_log.each do |key,value|
37
+ total, min, max, average = stats(value)
38
+ total = Syctime::separated_time_string(total, ":")
39
+ min = Syctime::separated_time_string(min, ":")
40
+ max = Syctime::separated_time_string(max, ":")
41
+ average = Syctime::separated_time_string(average, ":")
42
+ set_max_value_sizes(key, total, min, max, average, value_size)
43
+ report_lines[key] = [total, min, max, average]
44
+ end
45
+
46
+ report << create_report_line_strings(report_lines, value_size)
47
+
48
+ report_lines = {}
49
+ value_size = {key: 0, total: 0, min: 0, max: 0, average: 0}
50
+
51
+ report << sprintf("%s\n", "Count".bright)
52
+ count_log.each do |key,value|
53
+ total, min, max, average = stats_count(value)
54
+ set_max_value_sizes(key, total, min, max, average, value_size)
55
+ report_lines[key] = [total, min, max, average]
56
+ end
57
+
58
+ report << create_report_line_strings(report_lines, value_size)
59
+
60
+ end
61
+
62
+ # Creates report line strings
63
+ def create_report_line_strings(lines, value_size)
64
+ report = ""
65
+ lines.each do |key, value|
66
+ total = value[0]
67
+ min = value[1]
68
+ max = value[2]
69
+ average = value[3]
70
+ report << report_line(key, total, min, max, average, value_size)
71
+ end
72
+ report
73
+ end
74
+
75
+ # Determines the max string size of the values
76
+ def set_max_value_sizes(key, total, min, max, average, value_size)
77
+ value_size[:key] = [value_size[:key], key.size].max
78
+ value_size[:total] = [value_size[:total], total.size].max
79
+ value_size[:min] = [value_size[:min], min.size].max
80
+ value_size[:max] = [value_size[:max], max.size].max
81
+ value_size[:average] = [value_size[:average], average.size].max
82
+ end
83
+
84
+ # Creates a report line for the report
85
+ def report_line(key, total, min, max, average, sizes={})
86
+ color = :green if key == 'task'
87
+ color = :yellow if key == 'unplanned' or @general_purpose_tasks.
88
+ find_index(key)
89
+ key = key[0..8]
90
+ report = sprintf(" %s#{' '*(10-key.size)}", key).color(color)
91
+ report << sprintf("%#{sizes[:total]}s#{' '*(10-sizes[:total]+8)}", total).
92
+ color(color)
93
+ report << sprintf("%#{sizes[:min]}s#{' '*(10-sizes[:min]+8)}", min).
94
+ color(color)
95
+ report << sprintf("%#{sizes[:max]}s#{' '*(10-sizes[:max]+8)}", max).
96
+ color(color)
97
+ report << sprintf("%#{sizes[:average]}s\n", average).color(color)
98
+ end
99
+
100
+ # Calculates the average of a task processing, work or meeting time
101
+ def average(data)
102
+ sum = 0
103
+ data.each do |d|
104
+ sum += time_for_string(d[1]) - time_for_string(d[0])
105
+ end
106
+ sum / data.size
107
+ end
108
+
109
+ # Calculates the minimum duration of task processing, work or meeting time
110
+ def min(data)
111
+ diffs = []
112
+ data.each do |d|
113
+ diffs << time_for_string(d[1]) - time_for_string(d[0])
114
+ end
115
+ diffs.min
116
+ end
117
+
118
+ # Calculates the maximum duration of task processing, work or meeting time
119
+ def max(data)
120
+ diffs = []
121
+ data.each do |d|
122
+ diffs << time_for_string(d[1]) - time_for_string(d[0])
123
+ end
124
+ diffs.max
125
+ end
126
+
127
+ # Calculates total, min, max and average time of task processing, work or
128
+ # meeting time
129
+ def stats(data)
130
+ diffs = []
131
+ data.each do |d|
132
+ diffs << time_for_string(d[1]) - time_for_string(d[0])
133
+ end
134
+ [diffs.inject(:+), diffs.min, diffs.max, diffs.inject(:+) / diffs.size]
135
+ end
136
+
137
+ # Calculates the total, min, max and average count of task processing,
138
+ # creation, update, done, delete
139
+ def stats_count(data)
140
+ count = []
141
+ data.each do |key,value|
142
+ count << value.to_i
143
+ end
144
+ [count.inject(:+), count.min, count.max, count.inject(:+) / count.size]
145
+ end
146
+
147
+ # Retrieves the log entries from the log file
148
+ def logs(file, from="", to=from)
149
+ times = []
150
+ time_data = {}
151
+ time_types = %w{work meeting task}
152
+ time_types << @general_purpose_tasks
153
+ time_types.flatten!
154
+ count_data = {}
155
+ count_types = %w{meeting task create done update delete}
156
+ count_types << @general_purpose_tasks
157
+ count_types.flatten!
158
+ IO.readlines(file).each do |line|
159
+ values = line.split(";")
160
+ time = time_for_string(values[4])
161
+ times << time
162
+ next if values[0] == "start"
163
+ unless from == ""
164
+ next unless Syctime::date_between?(time, from, to)
165
+ end
166
+ values[0] = values[3] if @general_purpose_tasks.find_index(values[3])
167
+ values[0] = "task" if values[0] == "stop"
168
+ if count_types.find_index(values[0])
169
+ time = time.strftime("%Y-%m-%d")
170
+ count_data[values[0]] = {} unless count_data[values[0]]
171
+ count_data[values[0]][time] = 0 unless count_data[values[0]][time]
172
+ count_data[values[0]][time] += 1
173
+ if @general_purpose_tasks.find_index(values[0])
174
+ count_data['unplanned'] = {} unless count_data['unplanned']
175
+ count_data['unplanned'][time] = 0 unless \
176
+ count_data['unplanned'][time]
177
+ count_data['unplanned'][time] += 1
178
+ end
179
+ end
180
+ if time_types.find_index(values[0])
181
+ time_data[values[0]] = [] unless time_data[values[0]]
182
+ time_data[values[0]] << [values[4],values[5]]
183
+ if @general_purpose_tasks.find_index(values[0])
184
+ time_data['unplanned'] = [] unless time_data['unplanned']
185
+ time_data['unplanned'] << [values[4],values[5]]
186
+ end
187
+ end
188
+ end
189
+ from = times.min if from == ""
190
+ to = times.max if to == ""
191
+ [from, to, time_data, count_data]
192
+ end
193
+
194
+ end
195
+
196
+ end
data/lib/syctask/task.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'fileutils'
2
2
  require 'rainbow'
3
3
  require_relative 'evaluator'
4
+ require_relative 'environment.rb'
5
+ require_relative 'task_tracker.rb'
4
6
 
5
7
  # Syctask provides functions for managing tasks in a task list
6
8
  module Syctask
@@ -25,9 +27,11 @@ module Syctask
25
27
  # ID of the task
26
28
  attr_reader :id
27
29
  # Duration specifies the planned time for processing the task
28
- attr_accessor :duration
30
+ attr_reader :duration
31
+ # Remaining time is the duration subtracted by the lead time since last plan
32
+ attr_reader :remaining
29
33
  # Lead time is the time this task has been processed
30
- attr_accessor :lead_time
34
+ attr_reader :lead_time
31
35
  # Creation date
32
36
  attr_reader :creation_date
33
37
  # Update date
@@ -45,6 +49,13 @@ module Syctask
45
49
  @options = options
46
50
  @options[:note] =
47
51
  "#{@creation_date}\n#{@options[:note]}\n" if @options[:note]
52
+ if @options[:follow_up] or @options[:due_date]
53
+ @duration = 2 * 15 * 60
54
+ @remaining = 2 * 15 * 60
55
+ else
56
+ @duration = 0
57
+ @remaining = 0
58
+ end
48
59
  @id = id
49
60
  end
50
61
 
@@ -69,6 +80,11 @@ module Syctask
69
80
  # supplemented with the new values and not overridden.
70
81
  def update(options)
71
82
  @update_date = Time.now.strftime("%Y-%m-%d - %H:%M:%S")
83
+ if options[:duration]
84
+ set_duration(options.delete(:duration).to_i * 15 * 60)
85
+ elsif options[:follow_up] or options[:due_date]
86
+ set_duration(2 * 15 * 60) if @duration.nil?
87
+ end
72
88
  options.keys.each do |key|
73
89
  new_value = options[key]
74
90
 
@@ -95,6 +111,28 @@ module Syctask
95
111
  !@updated_date.nil?
96
112
  end
97
113
 
114
+ # Sets the duration that this task is planned for processing. Assigns to
115
+ # remaining the duration time
116
+ def set_duration(duration)
117
+ @duration = duration
118
+ @remaining = duration
119
+ end
120
+
121
+ # Updates the lead time. Adds the lead time to @lead_time and calculates
122
+ # @remaining
123
+ def update_lead_time(lead_time)
124
+ if @lead_time
125
+ @lead_time += lead_time
126
+ else
127
+ @lead_time = lead_time
128
+ end
129
+ if @remaining
130
+ @remaining -= lead_time
131
+ else
132
+ @remaining = @duration.to_i - lead_time
133
+ end
134
+ end
135
+
98
136
  # Marks the task as done. When done than the done date is set. Optionally a
99
137
  # note can be provided.
100
138
  def done(note="")
@@ -102,6 +140,7 @@ module Syctask
102
140
  if note
103
141
  options[:note] = "#{@done_date}\n#{note}\n#{@options[:note]}"
104
142
  end
143
+ Syctask::log_task("done", self)
105
144
  end
106
145
 
107
146
  # Checks if this task is done. Returns true if done otherwise false
@@ -109,6 +148,23 @@ module Syctask
109
148
  !@done_date.nil?
110
149
  end
111
150
 
151
+ # Checks if task is scheduled for today. Returns true if follow up or due
152
+ # date is today otherwise false.
153
+ def today?
154
+ evaluator = Evaluator.new
155
+ today = Time.now.strftime("%Y-%m-%d")
156
+ evaluator.compare_dates(@options[:follow_up], today) or \
157
+ evaluator.compare_dates(@options[:due_date], today)
158
+ end
159
+
160
+ # Checks whether the task is currently tracked. Returns true if so otherwise
161
+ # false
162
+ def tracked?
163
+ tracker = Syctask::TaskTracker.new
164
+ task = tracker.tracked_task
165
+ task.nil? ? false : task == self
166
+ end
167
+
112
168
  # Compares the provided elements in the filter with the correspondent
113
169
  # elements in the task. When all comparissons match than true is returned.
114
170
  # If one comparisson does not match false is returned. If filter is empty
@@ -1,10 +1,15 @@
1
1
  require 'fileutils'
2
+ require 'rainbow'
2
3
  require_relative '../sycutil/console.rb'
3
4
  require_relative 'task_service.rb'
5
+ require_relative 'environment.rb'
4
6
 
5
7
  module Syctask
6
8
  # String that is prompted during planning
7
9
  PROMPT_STRING = '(a)dd, (c)omplete, (s)kip, (q)uit: '
10
+ # String that is prompted during inspect
11
+ INSPECT_STRING = '(e)dit, (d)one, de(l)ete, (p)lan, (c)omplete, (s)kip, '+
12
+ '(q)uit: '
8
13
  # String that is prompted during prioritization
9
14
  PRIORITIZE_STRING = 'Task 1 has (h)igher or (l)ower priority, or (q)uit: '
10
15
 
@@ -12,7 +17,7 @@ module Syctask
12
17
  # be prioritized to determine the most to the least important tasks.
13
18
  class TaskPlanner
14
19
  # The task where the planned tasks are saved to
15
- WORK_DIR = File.expand_path("~/.tasks")
20
+ WORK_DIR = Syctask::SYC_DIR #File.expand_path("~/.tasks")
16
21
 
17
22
  # Creates a new TaskPlanner
18
23
  def initialize
@@ -41,9 +46,71 @@ module Syctask
41
46
  choice = @console.prompt PROMPT_STRING
42
47
  case choice
43
48
  when 'a'
44
- print "Duration (1 = 15 minutes, return 30 minutes): "
45
- duration = gets.chomp
46
- task.duration = duration.empty? ? 2 : duration
49
+ duration = 0
50
+ until duration > 0
51
+ print "Duration (1 = 15 minutes, RETURN defaults to 30 minutes): "
52
+ answer = gets.chomp
53
+ duration = answer.empty? ? 2 : answer.to_i
54
+ end
55
+ task.set_duration(units_to_time(duration))
56
+ task.options[:follow_up] = date
57
+ @service.save(task.dir, task)
58
+ planned << task
59
+ count += 1
60
+ when 'c'
61
+ re_display = true
62
+ redo
63
+ when 's'
64
+ #do nothing
65
+ when 'q'
66
+ break
67
+ end
68
+ end
69
+ save_tasks(planned)
70
+ count
71
+ end
72
+
73
+ # Inspect allows to edit, delete and mark tasks as done
74
+ def inspect_tasks(tasks, date=Time.now.strftime("%Y-%m-%d"))
75
+ already_planned = self.get_tasks(date)
76
+ count = 0
77
+ re_display = false
78
+ planned = []
79
+ tasks.each do |task|
80
+ next if already_planned.find_index {|t| t == task}
81
+ unless re_display
82
+ task.print_pretty
83
+ else
84
+ task.print_pretty(true)
85
+ re_display = false
86
+ end
87
+ choice = @console.prompt INSPECT_STRING
88
+ case choice
89
+ when 'e'
90
+ task_file = "#{task.dir}/#{task.id}.task"
91
+ system "vi #{task_file}" if File.exists? task_file
92
+ redo
93
+ when 'd'
94
+ puts "Enter a note or hit <RETURN>"
95
+ note = gets.chomp
96
+ task.done(note)
97
+ @service.save(task.dir, task)
98
+ STDOUT.puts sprintf("--> Marked task %d as done",
99
+ task.id).color(:green)
100
+ when 'l'
101
+ print "Confirm delete task (Y/n)? "
102
+ answer = gets.chomp
103
+ count = @service.delete(task.dir, {id: task.id.to_s}) if answer == "Y"
104
+ puts sprintf("--> Deleted %d task%s",
105
+ count, count == 1 ? "" : "s").color(:green)
106
+ when 'p'
107
+ duration = 0
108
+ until duration > 0
109
+ print "Duration (1 = 15 minutes, RETURN defaults to 30 minutes): "
110
+ answer = gets.chomp
111
+ duration = answer.empty? ? 2 : answer.to_i
112
+ end
113
+ task.set_duration(units_to_time(duration))
47
114
  task.options[:follow_up] = date
48
115
  @service.save(task.dir, task)
49
116
  planned << task
@@ -61,19 +128,25 @@ module Syctask
61
128
  count
62
129
  end
63
130
 
64
- # Order tasks in the provided IDs sequence at the specified date. If not
65
- # IDs are provided than rest of tasks is appended to the end of the plan.
66
- # Returns the count of ordered tasks and the count of the rest of the tasks.
67
- def order_tasks(date, ids)
131
+ # Order tasks in the provided IDs sequence at the specified date. If not all
132
+ # IDs are provided than rest of tasks is appended to the end of the plan. If
133
+ # a position (last, first and a number) is provided the ordered tasks are
134
+ # inserted at the specified position. Returns the count of ordered tasks,
135
+ # the count of the rest of the tasks and the position where the ordered
136
+ # tasks have been inserted.
137
+ def order_tasks(date, ids, pos=0)
68
138
  tasks = get_tasks(date)
139
+ pos = "0" if pos.class == String and pos.downcase == 'first'
140
+ pos = tasks.size.to_s if pos.class == String and pos.downcase == 'last'
69
141
  ordered = []
70
142
  ids.each do |id|
71
143
  index = tasks.find_index {|t| t.id == id.to_i}
72
144
  ordered << tasks.delete_at(index) if index
73
145
  end
74
- ordered << tasks
75
- save_tasks(ordered.flatten!, true)
76
- [ordered.size - tasks.size, tasks.size]
146
+ pos = [pos.to_i.abs,tasks.size].min
147
+ tasks.insert(pos, ordered)
148
+ save_tasks(tasks.flatten!, true)
149
+ [ordered.size, tasks.size, pos]
77
150
  end
78
151
 
79
152
  # Prioritize tasks by pair wise comparisson. Each task is compared to the
@@ -119,13 +192,15 @@ module Syctask
119
192
  save_tasks(planned, true)
120
193
  end
121
194
 
122
- # Moves the specified tasks to the specified date. Returns the count of
123
- # moved files
195
+ # Moves the specified tasks to the specified date. Sets the remaining timer
196
+ # to at least 15 minutes and sets the duration to the remaining timer's
197
+ # values. Returns the count of moved files
124
198
  def move_tasks(filter={}, from_date=Time.now.strftime("%Y-%m-%d"), to_date)
125
199
  return 0 if from_date == to_date
126
200
  moved = get_tasks(from_date, filter)
127
201
  moved.each do |task|
128
202
  task.options[:follow_up] = to_date
203
+ task.set_duration([task.remaining, 900].max)
129
204
  @service.save(task.dir, task)
130
205
  end
131
206
  add_tasks(moved, to_date)
@@ -170,6 +245,12 @@ module Syctask
170
245
 
171
246
  private
172
247
 
248
+ # Calculates the time for time units. One time unit equals to 900 seconds or
249
+ # 15 minutes. The return value is in seconds
250
+ def units_to_time(units)
251
+ units * 15 * 60
252
+ end
253
+
173
254
  # Creates a file where the planned tasks are saved to
174
255
  def make_todo_today_file(date)
175
256
  file_name = Time.now.strftime("#{date}_planned_tasks")