syc-task 0.0.7 → 0.1.15

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.
@@ -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")