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.
- data/README.rdoc +159 -15
- data/bin/console_timer +75 -0
- data/bin/syctask +246 -68
- data/lib/syctask.rb +4 -1
- data/lib/syctask/environment.rb +427 -2
- data/lib/syctask/schedule.rb +84 -21
- data/lib/syctask/settings.rb +43 -0
- data/lib/syctask/statistics.rb +196 -0
- data/lib/syctask/task.rb +58 -2
- data/lib/syctask/task_planner.rb +94 -13
- data/lib/syctask/task_scheduler.rb +5 -1
- data/lib/syctask/task_service.rb +55 -15
- data/lib/syctask/task_tracker.rb +27 -17
- data/lib/syctask/times.rb +29 -0
- data/lib/syctask/version.rb +1 -1
- data/lib/syctime/time_util.rb +46 -7
- data/lib/sycutil/console_timer.rb +75 -0
- metadata +215 -136
data/lib/syctask.rb
CHANGED
@@ -5,9 +5,12 @@ require 'syctask/task_service.rb'
|
|
5
5
|
require 'syctask/task_scheduler.rb'
|
6
6
|
require 'syctask/task_planner.rb'
|
7
7
|
require 'syctask/schedule.rb'
|
8
|
-
require 'sycutil/console.rb'
|
9
8
|
require 'syctask/task_tracker.rb'
|
9
|
+
require 'syctask/environment.rb'
|
10
|
+
require 'sycutil/console.rb'
|
10
11
|
require 'syctime/time_util.rb'
|
12
|
+
require 'syctask/settings.rb'
|
13
|
+
require 'syctask/statistics.rb'
|
11
14
|
|
12
15
|
# Add requires for other files you add to your project here, so
|
13
16
|
# you just need to require this one file in your bin file
|
data/lib/syctask/environment.rb
CHANGED
@@ -1,6 +1,431 @@
|
|
1
1
|
module Syctask
|
2
2
|
|
3
|
-
#
|
4
|
-
|
3
|
+
# System directory of syctask
|
4
|
+
SYC_DIR = File.expand_path('~/.syc/syctask')
|
5
|
+
# ID file where the last issued ID is saved
|
6
|
+
ID = SYC_DIR + "/id"
|
7
|
+
# File that contains all issued IDs
|
8
|
+
IDS = SYC_DIR + "/ids"
|
9
|
+
# File with tags
|
10
|
+
TAGS = SYC_DIR + "/tags"
|
11
|
+
# File with the general purpose tasks
|
12
|
+
DEFAULT_TASKS = SYC_DIR + "/default_tasks"
|
13
|
+
# File that holds the default task directory
|
14
|
+
DEFAULT_TASKS_DIR = SYC_DIR + "/default_tasks_dir"
|
15
|
+
# Log file that logs all activities of syctask like creation of tasks
|
16
|
+
TASKS_LOG = SYC_DIR + "/tasks.log"
|
17
|
+
# File that holds the tracked task
|
18
|
+
TRACKED_TASK = SYC_DIR + "/tracked_tasks"
|
19
|
+
# If files are re-indexed during re-indexing these tasks are save here
|
20
|
+
RIDX_LOG = SYC_DIR + "/reindex.log"
|
21
|
+
|
22
|
+
# Reads the default task directory from the DEFAULT_TASKS_DIR file if it
|
23
|
+
# exists. If it exist but doesn't contain a valid directory ~/.tasks is
|
24
|
+
# returned as default tasks directory
|
25
|
+
dir = File.read(DEFAULT_TASKS_DIR) if File.exists? DEFAULT_TASKS_DIR
|
26
|
+
# User specified default working directory
|
27
|
+
work_dir = dir if not dir.nil? and not dir.empty? and File.exists? dir
|
28
|
+
# Set eather user defined work directory or default
|
29
|
+
WORK_DIR = work_dir.nil? ? File.expand_path('~/.tasks') : work_dir
|
30
|
+
|
31
|
+
# Logs a task regarding create, update, done, delete
|
32
|
+
def log_task(type, task)
|
33
|
+
File.open(TASKS_LOG, 'a') do |file|
|
34
|
+
log_entry = "#{type.to_s};"
|
35
|
+
log_entry += "#{task.id};#{task.dir};"
|
36
|
+
log_entry += "#{task.title.gsub(';', '\'semicolon\'')};"
|
37
|
+
log_entry += "#{Time.now};"
|
38
|
+
log_entry += "#{Time.now}"
|
39
|
+
file.puts log_entry
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Logs the work time
|
44
|
+
def log_work_time(type, work_time)
|
45
|
+
today = Time.now
|
46
|
+
begins = Time.local(today.year,
|
47
|
+
today.mon,
|
48
|
+
today.day,
|
49
|
+
work_time[0],
|
50
|
+
work_time[1],
|
51
|
+
0)
|
52
|
+
ends = Time.local(today.year,
|
53
|
+
today.mon,
|
54
|
+
today.day,
|
55
|
+
work_time[2],
|
56
|
+
work_time[3],
|
57
|
+
0)
|
58
|
+
entry = "#{type};-1;;work;#{begins};#{ends}\n"
|
59
|
+
logs = File.read(TASKS_LOG)
|
60
|
+
return if logs.scan(entry)[0]
|
61
|
+
time_pat = "#{today.strftime("%Y-%m-%d")} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4}"
|
62
|
+
pattern = %r{#{type};-1;;work;#{time_pat};#{time_pat}\n}
|
63
|
+
log = logs.scan(pattern)[0]
|
64
|
+
if log and logs.sub!(log, entry)
|
65
|
+
File.write(TASKS_LOG, logs)
|
66
|
+
else
|
67
|
+
File.open(TASKS_LOG, 'a') {|f| f.puts entry}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Logs meeting times
|
72
|
+
def log_meetings(type, busy_time, meetings)
|
73
|
+
today = Time.now
|
74
|
+
logs = File.read(TASKS_LOG)
|
75
|
+
time_pat = "#{today.strftime("%Y-%m-%d")} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4}"
|
76
|
+
pattern = %r{#{type};-2;;.*?;#{time_pat};#{time_pat}\n}
|
77
|
+
logs.gsub!(pattern, "")
|
78
|
+
busy_time.each_with_index do |busy,i|
|
79
|
+
begins = Time.local(today.year,today.mon,today.day,busy[0],busy[1],0)
|
80
|
+
ends = Time.local(today.year,today.mon,today.day,busy[2],busy[3],0)
|
81
|
+
meeting = meetings[i] ? meetings[i] : "Meeting #{i}"
|
82
|
+
logs << "#{type};-2;;#{meeting};#{begins};#{ends}\n"
|
83
|
+
end
|
84
|
+
File.write(TASKS_LOG, logs)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Checks whether all files are available that are needed for syctask's
|
88
|
+
# operation
|
89
|
+
def check_environment
|
90
|
+
FileUtils.mkdir_p WORK_DIR unless File.exists? WORK_DIR
|
91
|
+
unless viable?
|
92
|
+
unless get_files(File.expand_path("~"), "*.task").empty?
|
93
|
+
# Backup ARGV content
|
94
|
+
args = []
|
95
|
+
ARGV.each {|arg| args << arg} unless ARGV.empty?
|
96
|
+
ARGV.clear
|
97
|
+
puts
|
98
|
+
puts "Warning:"
|
99
|
+
puts "-------"
|
100
|
+
puts "There are missing system files of syc-task, even though tasks "+
|
101
|
+
"are available."
|
102
|
+
puts "If you have upgraded from version 0.0.7 or below than this is "+
|
103
|
+
"due to a changed\nfile structure. For changes in version "+
|
104
|
+
"greater 0.0.7 see"
|
105
|
+
puts "--> https://rubygems.org/gems/syc-task"
|
106
|
+
puts "Or you have accidentially deleted system files. In both cases "+
|
107
|
+
"re-indexing\nwill recover syc-task."
|
108
|
+
print "Do you want to recover syc-task (y/n)? "
|
109
|
+
answer = gets.chomp
|
110
|
+
exit -1 unless answer.downcase == "y"
|
111
|
+
reindex_tasks(File.expand_path("~"))
|
112
|
+
puts "Successfully recovered syc-task"
|
113
|
+
puts "-> A log file of re-indexed tasks can be found at\n"+
|
114
|
+
"#{RIDX_LOG}" if File.exists? RIDX_LOG
|
115
|
+
print "Press any key to continue "
|
116
|
+
gets
|
117
|
+
# Restore ARGV content
|
118
|
+
args.each {|arg| ARGV << arg} unless args.empty?
|
119
|
+
else
|
120
|
+
FileUtils.mkdir_p SYC_DIR unless File.exists? SYC_DIR
|
121
|
+
File.write(ID, "0")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Checks if system files are available that are needed for running syc-task.
|
127
|
+
# Returns true if neccessary system files are available, otherwise false.
|
128
|
+
def viable?
|
129
|
+
File.exists? SYC_DIR and File.exists? ID
|
130
|
+
end
|
131
|
+
|
132
|
+
# Re-indexing of tasks is done when tasks are available but SYC_DIR or ID file
|
133
|
+
# is missing. The ID file contains the last issued task ID. The ID file is
|
134
|
+
# referenced for obtaining the next ID for a new task. Re-indexing is done as
|
135
|
+
# follows:
|
136
|
+
# * Retrieve all tasks in and below the given directory *root*
|
137
|
+
# * Determine the highest ID number and add it to the ID file
|
138
|
+
# * Determine all tasks that don't have a unique ID
|
139
|
+
# * Re-index all tasks not having a unique ID and rename the file names
|
140
|
+
# accordingly
|
141
|
+
# * Adjust the IDs in the planned_tasks, tasks.log and tracked_tasks files
|
142
|
+
# * Copy all system files planned_tasks, time_schedule, tasks.log, id to the
|
143
|
+
# SYC_DIR directory if not already in the SYC_DIR directory. This should
|
144
|
+
# only be if upgrading from version 0.0.7 and below.
|
145
|
+
def reindex_tasks(root)
|
146
|
+
FileUtils.mkdir_p SYC_DIR unless File.exists? SYC_DIR
|
147
|
+
new_id = {}
|
148
|
+
to_be_renamed = {}
|
149
|
+
root = File.expand_path(root)
|
150
|
+
puts "-> Collect task files..."
|
151
|
+
task_files = task_files(root)
|
152
|
+
puts "-> Restore ID counter..."
|
153
|
+
initialize_id(task_files)
|
154
|
+
print "-> Start re-indexing now..."
|
155
|
+
collect_by_id(task_files).each do |id, files|
|
156
|
+
next if files.size < 2
|
157
|
+
files.each_with_index do |file,i|
|
158
|
+
next if i == 0 # need to re-index only second and following tasks
|
159
|
+
result = reindex_task(file)
|
160
|
+
# associate old id to new id and dir name
|
161
|
+
if new_id[result[:old_id]].nil?
|
162
|
+
new_id[result[:old_id]] = {result[:dirname] => result[:new_id]}
|
163
|
+
else
|
164
|
+
new_id[result[:old_id]][result[:dirname]] = result[:new_id]
|
165
|
+
end
|
166
|
+
# assign tmp_file to new_file for later renaming
|
167
|
+
to_be_renamed[result[:tmp_file]] = result[:new_file]
|
168
|
+
# document the re-indexing of tasks
|
169
|
+
log_reindexing(result[:old_id], result[:new_id], result[:new_file])
|
170
|
+
end
|
171
|
+
end
|
172
|
+
to_be_renamed.each {|old_name,new_name| File.rename(old_name, new_name)}
|
173
|
+
puts
|
174
|
+
puts "-> Update task log file"
|
175
|
+
update_tasks_log(root, new_id)
|
176
|
+
puts "-> Update planned tasks files"
|
177
|
+
update_planned_tasks(root, new_id)
|
178
|
+
puts "-> Move schedule files..."
|
179
|
+
move_time_schedule_files(root)
|
180
|
+
puts "-> Update tracked task file..."
|
181
|
+
update_tracked_task(root)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Re-indexes the tasks' IDs and renames the task files to match the new ID.
|
185
|
+
# The orginal file is deleted. Returns old_id, new_id, tmp_file_name and
|
186
|
+
# new_file_name. The task is save with the tmp_file_name in case the new ID
|
187
|
+
# and hence the new_file_name exists already from a not yet re-indexed task.
|
188
|
+
# After all tasks are re-indexed the tmp_file_names have to be renamed to the
|
189
|
+
# new_file_names. The renaming is in the responsibility of the calling method.
|
190
|
+
def reindex_task(file)
|
191
|
+
print "."
|
192
|
+
task = File.read(file)
|
193
|
+
old_id = task.scan(/(?<=^id: )\d+$/)[0]
|
194
|
+
new_id = next_id.to_s
|
195
|
+
task.gsub!(/(?<=^id: )\d+$/, new_id)
|
196
|
+
dirname = File.dirname(file)
|
197
|
+
new_file = "#{dirname}/#{new_id}.task"
|
198
|
+
tmp_file = "#{new_file}_"
|
199
|
+
File.write(tmp_file, task)
|
200
|
+
File.delete(file)
|
201
|
+
{old_id: old_id,
|
202
|
+
new_id: new_id,
|
203
|
+
tmp_file: tmp_file,
|
204
|
+
new_file: new_file,
|
205
|
+
dirname: dirname}
|
206
|
+
end
|
207
|
+
|
208
|
+
# Determines the greatest task ID out of the provided tasks and saves it to
|
209
|
+
# the ID file
|
210
|
+
def initialize_id(tasks)
|
211
|
+
pattern = %r{(?<=\/)\d+(?=\.task)}
|
212
|
+
tasks.sort_by! {|t| t.scan(pattern)[0].to_i}
|
213
|
+
save_id(tasks[tasks.size-1].scan(pattern)[0].to_i)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Saves the ids to ids file
|
217
|
+
def save_ids(id, file)
|
218
|
+
entry = "#{id},#{file}"
|
219
|
+
return if File.exists? IDS and not File.read(IDS).scan(entry).empty?
|
220
|
+
File.open(IDS, 'a') {|f| f.puts entry}
|
221
|
+
end
|
222
|
+
|
223
|
+
# Save the id to the ID file. Returns the id when save was successful
|
224
|
+
def save_id(id)
|
225
|
+
File.write(ID,id)
|
226
|
+
id
|
227
|
+
end
|
228
|
+
|
229
|
+
# Retrieve the next unassigned task id
|
230
|
+
def next_id
|
231
|
+
id = File.read(ID).to_i + 1
|
232
|
+
save_id(id)
|
233
|
+
id
|
234
|
+
end
|
235
|
+
|
236
|
+
# Logs if a task is re-indexed
|
237
|
+
def log_reindexing(old_id, new_id, file)
|
238
|
+
entry = "#{old_id},#{new_id},#{file}"
|
239
|
+
return if File.exists? RIDX_LOG and not File.read(RIDX_LOG).
|
240
|
+
scan(entry).empty?
|
241
|
+
File.open(RIDX_LOG, 'a') {|f| f.puts entry}
|
242
|
+
end
|
243
|
+
|
244
|
+
# Updates the tasks.log file if tasks are re-indexed with the task's new ids
|
245
|
+
def update_tasks_log(dir, new_ids)
|
246
|
+
tasks_log_files(dir).each do |file|
|
247
|
+
logs = File.readlines(file)
|
248
|
+
logs.each_with_index do |log,i|
|
249
|
+
type = log.scan(/^.*?(?=;)/)[0]
|
250
|
+
logs[i] = log.sub!("-",";") if log.scan(/(?<=^#{type};)\d+-/)[0]
|
251
|
+
old_id = log.scan(/(?<=^#{type};)\d+(?=;)/)[0]
|
252
|
+
next unless new_ids[old_id]
|
253
|
+
task_dir = log.scan(/(?<=^#{type};#{old_id};).*?(?=;)/)[0]
|
254
|
+
next unless new_ids[old_id][task_dir]
|
255
|
+
logs[i] = log.sub("#{old_id};#{task_dir}",
|
256
|
+
"#{new_ids[old_id][task_dir]};#{task_dir}")
|
257
|
+
end
|
258
|
+
if file == TASKS_LOG
|
259
|
+
File.write(TASKS_LOG, logs.join)
|
260
|
+
else
|
261
|
+
#TODO only append a line if it is not already available in TASKS_LOG
|
262
|
+
File.open(TASKS_LOG, 'a') {|f| f.puts logs.join}
|
263
|
+
FileUtils.rm file
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# TODO delete
|
269
|
+
def update_tasks_log_old(dir, old_id, new_id, file)
|
270
|
+
old_entry = "#{old_id}-#{File.dirname(file)}"
|
271
|
+
# Append '/' to dir name so already updated task is not subsequently updated
|
272
|
+
new_entry = "#{new_id}-#{File.dirname(file)}/"
|
273
|
+
@tasks_log_files = tasks_log_files(dir) if @tasks_log_files.nil?
|
274
|
+
@tasks_log_files.each do |f|
|
275
|
+
tasks_log = File.read(f).gsub(old_entry, new_entry)
|
276
|
+
File.write(f, tasks_log)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# Replaces the old ids with the new ids in the planned tasks files. A planned
|
281
|
+
# tasks file has the form '2013-03-03_planned_tasks' and lives until syctask's
|
282
|
+
# version 0.0.7 in ~/.tasks directory. From version 0.1.0 on the planned tasks
|
283
|
+
# files live in the ~/.syc/syctask directory. So the calling method has the
|
284
|
+
# responsibility to copy or move the planned tasks files after they have been
|
285
|
+
# updated to the new planned tasks directory.
|
286
|
+
def update_planned_tasks(dir, new_ids)
|
287
|
+
planned_tasks_files(dir).each do |file|
|
288
|
+
tasks = File.readlines(file)
|
289
|
+
tasks.each_with_index do |task,i|
|
290
|
+
task_dir, old_id = task.chomp.split(',')
|
291
|
+
next unless new_ids[old_id]
|
292
|
+
next unless new_ids[old_id][task_dir]
|
293
|
+
tasks[i] = "#{task_dir},#{new_ids[old_id][task_dir]}"
|
294
|
+
end
|
295
|
+
File.write("#{SYC_DIR}/#{File.basename(file)}", tasks.join("\n"))
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# TODO delete
|
300
|
+
def update_planned_tasks_old(dir, old_id, new_id, file)
|
301
|
+
old_entry = "#{File.dirname(file)},#{old_id}"
|
302
|
+
# Append '/' to dir name so already updated task is not subsequently updated
|
303
|
+
new_entry = "#{File.dirname(file)}/,#{new_id}"
|
304
|
+
@planned_tasks_files = planned_tasks_files(dir) if @planned_tasks_files.nil?
|
305
|
+
@planned_tasks_files.each do |file|
|
306
|
+
planned_tasks = File.read(file).gsub(old_entry, new_entry)
|
307
|
+
File.write(file, planned_tasks)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Updates tracked_tasks file if task has been re-indexed with new ID
|
312
|
+
def update_tracked_task(dir)
|
313
|
+
@tracked = get_files(dir, "tracked_tasks") if @tracked.nil?
|
314
|
+
return if @tracked.empty?
|
315
|
+
task = File.read(@tracked[0])
|
316
|
+
if File.exists? RIDX_LOG
|
317
|
+
old_id = task.scan(/(?<=id: )\d+$/)
|
318
|
+
old_dir = task.scan(/(?<=dir: ).*$/)
|
319
|
+
return if old_id.empty? or old_dir.empty?
|
320
|
+
pattern = %r{(?<=#{old_id[0]},)\d+(?=,#{old_dir[0]}\/\d+\.task)}
|
321
|
+
new_id = File.read(RIDX_LOG).scan(pattern)
|
322
|
+
task.gsub!("id: #{old_id}", "id: #{new_id}")
|
323
|
+
end
|
324
|
+
File.write(TRACKED_TASK, task)
|
325
|
+
FileUtils.rm @tracked[0] unless TRACKED_TASK == @tracked[0]
|
326
|
+
end
|
327
|
+
|
328
|
+
# Extracts tasks that have no unique id
|
329
|
+
def collect_by_id(tasks)
|
330
|
+
extract = {}
|
331
|
+
tasks.each do |task|
|
332
|
+
id = task.scan(/(?<=\/)\d+(?=\.task$)/)[0]
|
333
|
+
extract[id].nil? ? extract[id] = [task] : extract[id] << task
|
334
|
+
end
|
335
|
+
extract
|
336
|
+
end
|
337
|
+
|
338
|
+
# Retrieves all task files in and below the provided dir. Returns an array of
|
339
|
+
# task files
|
340
|
+
def task_files(dir)
|
341
|
+
get_files(dir, "*.task").keep_if {|file| file.match /\d+\.task$/}
|
342
|
+
end
|
343
|
+
|
344
|
+
# Retrieves all planned task files in and below the given directory
|
345
|
+
def planned_tasks_files(dir)
|
346
|
+
pattern = %r{\d{4}-\d{2}-\d{2}_planned_tasks}
|
347
|
+
get_files(dir, "*planned_tasks").keep_if {|f| f.match(pattern)}
|
348
|
+
end
|
349
|
+
|
350
|
+
# Retrieves all schedule files in and below the given directory
|
351
|
+
def time_schedule_files(dir)
|
352
|
+
pattern = %r{\d{4}-\d{2}-\d{2}_time_schedule}
|
353
|
+
get_files(dir, "*time_schedule").keep_if {|f| f.match(pattern)}
|
354
|
+
end
|
355
|
+
|
356
|
+
# Retrieves als tasks.log files in and below the given directory
|
357
|
+
def tasks_log_files(dir)
|
358
|
+
get_files(dir, "tasks.log")
|
359
|
+
end
|
360
|
+
|
361
|
+
# Retrieves all files that meet the pattern in and below the given directory
|
362
|
+
def get_files(dir, pattern)
|
363
|
+
original_dir = File.expand_path(".")
|
364
|
+
Dir.chdir(dir)
|
365
|
+
files = Dir.glob("**/#{pattern}", File::FNM_DOTMATCH).map do |f|
|
366
|
+
File.expand_path(f)
|
367
|
+
end
|
368
|
+
Dir.chdir(original_dir)
|
369
|
+
files
|
370
|
+
end
|
371
|
+
|
372
|
+
# Retrieve all directories that contain tasks
|
373
|
+
def get_task_dirs(dir)
|
374
|
+
original_dir = File.expand_path(".")
|
375
|
+
Dir.chdir(dir)
|
376
|
+
dirs = Dir.glob("**/*.task", File::FNM_DOTMATCH).map do |f|
|
377
|
+
File.dirname(File.expand_path(f))
|
378
|
+
end
|
379
|
+
Dir.chdir(original_dir)
|
380
|
+
dirs.uniq
|
381
|
+
end
|
382
|
+
|
383
|
+
# Retrieves all directories that contain tasks and the count of contained
|
384
|
+
# tasks in and below the provided directory
|
385
|
+
def get_task_dirs_and_count(dir)
|
386
|
+
original_dir = File.expand_path(".")
|
387
|
+
Dir.chdir(dir)
|
388
|
+
dirs_and_count = Hash.new(0)
|
389
|
+
Dir.glob("**/*.task", File::FNM_DOTMATCH).each do |f|
|
390
|
+
dirname = File.dirname(File.expand_path(f))
|
391
|
+
dirs_and_count[dirname] += 1
|
392
|
+
end
|
393
|
+
Dir.chdir(original_dir)
|
394
|
+
dirs_and_count
|
395
|
+
end
|
396
|
+
|
397
|
+
# Moves the tasks.log file to the system directory if not there. Should only
|
398
|
+
# be if upgrading from version 0.0.7 and below
|
399
|
+
def move_task_log_file(dir)
|
400
|
+
@tasks_log_files = tasks_log_files(dir) if @tasks_log_files.nil?
|
401
|
+
@tasks_log_files.each do |f|
|
402
|
+
next if f == TASKS_LOG
|
403
|
+
tasks_log = File.read(f)
|
404
|
+
File.open(TASKS_LOG, 'a') {|t| t.puts tasks_log}
|
405
|
+
FileUtils.mv(f, "#{f}_#{Time.now.strftime("%y%m%d")}")
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# Moves the planned tasks file to the system directory if not there. Should
|
410
|
+
# only be if upgrading from version 0.0.7 and below
|
411
|
+
def move_planned_tasks_files(dir)
|
412
|
+
@planned_tasks_files = planned_tasks_files(dir) if @planned_tasks_files.nil?
|
413
|
+
@planned_tasks_files.each do |file|
|
414
|
+
to_file = "#{SYC_DIR}/#{File.basename(file)}"
|
415
|
+
next if file == to_file
|
416
|
+
FileUtils.mv file, to_file
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# Moves the schedule file to the system directory if not there. Should
|
421
|
+
# only be if upgrading from version 0.0.7 and below
|
422
|
+
def move_time_schedule_files(dir)
|
423
|
+
@time_schedule_files = time_schedule_files(dir) if @time_schedule_files.nil?
|
424
|
+
@time_schedule_files.each do |file|
|
425
|
+
to_file = "#{SYC_DIR}/#{File.basename(file)}"
|
426
|
+
next if file == to_file
|
427
|
+
FileUtils.mv file, to_file
|
428
|
+
end
|
429
|
+
end
|
5
430
|
|
6
431
|
end
|
data/lib/syctask/schedule.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
require_relative 'times.rb'
|
2
2
|
require_relative 'meeting.rb'
|
3
3
|
require_relative '../sycstring/string_util.rb'
|
4
|
+
require_relative '../syctime/time_util.rb'
|
5
|
+
|
4
6
|
include Sycstring
|
7
|
+
include Syctime
|
5
8
|
|
6
9
|
module Syctask
|
7
10
|
|
@@ -85,7 +88,10 @@ module Syctask
|
|
85
88
|
title = titles[index] ? titles[index] : "Meeting #{index}"
|
86
89
|
@meetings << Syctask::Meeting.new(busy, title)
|
87
90
|
end
|
88
|
-
raise Exception,
|
91
|
+
raise Exception,
|
92
|
+
"Busy times have to be within work time" unless within?(@meetings,
|
93
|
+
@starts,
|
94
|
+
@ends)
|
89
95
|
@tasks = tasks
|
90
96
|
end
|
91
97
|
|
@@ -95,8 +101,10 @@ module Syctask
|
|
95
101
|
assignments.each do |assignment|
|
96
102
|
number = assignment[0].upcase.ord - "A".ord
|
97
103
|
return false if number < 0 or number > @meetings.size
|
98
|
-
|
99
|
-
|
104
|
+
@meetings[number].tasks.clear
|
105
|
+
assignment[1].split(',').each do |id|
|
106
|
+
index = @tasks.find_index{|task| task.id == id.to_i}
|
107
|
+
@meetings[number].tasks << @tasks[index] if index and @tasks[index]
|
100
108
|
end
|
101
109
|
@meetings[number].tasks.uniq!
|
102
110
|
end
|
@@ -109,7 +117,13 @@ module Syctask
|
|
109
117
|
list << sprintf("%s", "--------\n").color(:red)
|
110
118
|
meeting_number = "A"
|
111
119
|
@meetings.each do |meeting|
|
112
|
-
|
120
|
+
hint = "-"
|
121
|
+
hint = "*" if time_between?(Time.now,
|
122
|
+
meeting.starts.time,
|
123
|
+
meeting.ends.time)
|
124
|
+
list << sprintf("%s %s %s\n", meeting_number,
|
125
|
+
hint,
|
126
|
+
meeting.title).color(:red)
|
113
127
|
meeting_number.next!
|
114
128
|
meeting.tasks.each do |task|
|
115
129
|
task_color = task.done? ? :green : :blue
|
@@ -156,14 +170,19 @@ module Syctask
|
|
156
170
|
# list
|
157
171
|
def graph
|
158
172
|
work_time, meeting_times = get_times
|
173
|
+
|
174
|
+
heading = sprintf("+++ %s - %s-%s +++", Time.now.strftime("%Y-%m-%d"),
|
175
|
+
@starts.time.strftime("%H:%M"),
|
176
|
+
@ends.time.strftime("%H:%M")).color(:blue)
|
177
|
+
|
159
178
|
time_line = "|---" * (work_time[1]-work_time[0]) + "|"
|
160
179
|
meeting_times.each do |time|
|
161
|
-
time_line[time[0]..time[1]] = '/' * (time[1] - time[0]
|
180
|
+
time_line[time[0]..time[1]-1] = '/' * (time[1] - time[0])
|
162
181
|
end
|
163
182
|
|
164
183
|
task_list, task_caption = assign_tasks_to_graph(time_line)
|
165
184
|
|
166
|
-
[meeting_list, meeting_caption,
|
185
|
+
[heading.center(80), meeting_list, meeting_caption,
|
167
186
|
colorize(time_line), time_caption,
|
168
187
|
task_caption, task_list]
|
169
188
|
end
|
@@ -214,23 +233,50 @@ module Syctask
|
|
214
233
|
# Assigns the tasks to the timeline in alternation x and o subsequent tasks.
|
215
234
|
# Returns the task list and the task caption
|
216
235
|
def assign_tasks_to_graph(time_line)
|
236
|
+
done_tasks = []
|
217
237
|
unscheduled_tasks = []
|
218
238
|
signs = ['x','o']
|
219
239
|
positions = {}
|
220
|
-
|
240
|
+
current_time = Time.now
|
221
241
|
unassigned_tasks.each.with_index do |task, index|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
242
|
+
if task.done? or not task.today?
|
243
|
+
done_tasks << task
|
244
|
+
next
|
245
|
+
else
|
246
|
+
round = task.remaining.to_i % 900 == 0 ? 0 : 0.5
|
247
|
+
duration = [(task.remaining.to_i/900+round).round, 1].max
|
248
|
+
position = [0, position_for_time(current_time)].max
|
249
|
+
end
|
250
|
+
free_time = scan_free(time_line, 1, position)
|
251
|
+
if free_time[0].nil?
|
226
252
|
unscheduled_tasks << task
|
227
253
|
next
|
228
254
|
end
|
229
|
-
|
230
|
-
|
231
|
-
|
255
|
+
0.upto(duration-1) do |i|
|
256
|
+
break unless free_time[i]
|
257
|
+
time_line[free_time[i]] = signs[index%2]
|
258
|
+
end
|
259
|
+
positions[free_time[0]] = task.id
|
232
260
|
end
|
233
261
|
|
262
|
+
unless done_tasks.empty?
|
263
|
+
end_position = position_for_time(current_time)
|
264
|
+
total_duration = 0
|
265
|
+
done_tasks.each_with_index do |task,index|
|
266
|
+
free_time = scan_free(time_line, 1, 0, end_position)
|
267
|
+
lead_time = task.duration.to_i - task.remaining.to_i + 0.0
|
268
|
+
max_duration = [free_time.size - (done_tasks.size - index - 1), 1].max
|
269
|
+
duration = [(lead_time/900).round, 1].max
|
270
|
+
total_duration += duration = [duration, max_duration].min
|
271
|
+
0.upto(duration-1) do |i|
|
272
|
+
break unless free_time[i]
|
273
|
+
time_line[free_time[i]] = signs[index%2]
|
274
|
+
end
|
275
|
+
positions[free_time[0]] = task.id if free_time[0]
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Create task list
|
234
280
|
max_id_size = 1
|
235
281
|
@tasks.each {|task| max_id_size = [task.id.to_s.size, max_id_size].max}
|
236
282
|
max_ord_size = (@tasks.size - 1).to_s.size
|
@@ -238,20 +284,26 @@ module Syctask
|
|
238
284
|
task_list = sprintf("%s", "Tasks\n").color(:blue)
|
239
285
|
task_list << sprintf("%s", "-----\n").color(:blue)
|
240
286
|
@tasks.each.with_index do |task, i|
|
241
|
-
if task.done?
|
287
|
+
if task.done? or not task.today?
|
242
288
|
color = :green
|
243
289
|
elsif unscheduled_tasks.find_index(task)
|
244
290
|
color = UNSCHEDULED_COLOR
|
245
291
|
else
|
246
292
|
color = WORK_COLOR
|
247
293
|
end
|
294
|
+
|
295
|
+
hint = "-"
|
296
|
+
hint = "~" unless task.today?
|
297
|
+
hint = "*" if task.tracked?
|
298
|
+
|
248
299
|
offset = max_ord_size + max_id_size + 5
|
249
300
|
title = split_lines(task.title, 80-offset)
|
250
301
|
title = title.chomp.gsub(/\n/, "\n#{' '*offset}")
|
251
|
-
task_list << sprintf("%#{max_ord_size}d: %#{max_id_size}s
|
252
|
-
i, task.id, title).color(color)
|
302
|
+
task_list << sprintf("%#{max_ord_size}d: %#{max_id_size}s %s %s\n",
|
303
|
+
i, task.id, hint, title).color(color)
|
253
304
|
end
|
254
305
|
|
306
|
+
# Create task caption
|
255
307
|
task_caption = ""
|
256
308
|
create_caption(positions).each do |caption|
|
257
309
|
task_caption << sprintf("%s\n", caption).color(WORK_COLOR)
|
@@ -267,6 +319,7 @@ module Syctask
|
|
267
319
|
counter = 0
|
268
320
|
lines = [""]
|
269
321
|
positions.each do |position,id|
|
322
|
+
next unless position
|
270
323
|
line_id = next_line(position,lines,counter)
|
271
324
|
legend = ' ' * [0, position - lines[line_id].size].max + id.to_s
|
272
325
|
lines[line_id] += legend
|
@@ -295,18 +348,28 @@ module Syctask
|
|
295
348
|
return lines.size - 1
|
296
349
|
end
|
297
350
|
|
351
|
+
# Determines the position within the time line for the given time. Each
|
352
|
+
# position represents a 15 minute duration. Minutes below 8 will be rounded
|
353
|
+
# down otherwise rounded up.
|
354
|
+
def position_for_time(time)
|
355
|
+
diff = @starts.diff(time)
|
356
|
+
# as the time line is always rounded down to the full hour we have to add
|
357
|
+
# the minutes with @starts.m / 15 to get the position for the current time
|
358
|
+
((diff[0] * 60 + diff[1]) / 15.0).round + @starts.m / 15
|
359
|
+
end
|
360
|
+
|
298
361
|
# Scans the schedule for free time where a task can be added to. Count
|
299
362
|
# specifies the length of the free time and the position where to start
|
300
363
|
# scanning within the graph
|
301
|
-
def scan_free(graph, count,
|
364
|
+
def scan_free(graph, count, starts, ends=graph.size)
|
302
365
|
pattern = /(?!\/)[\|-]{#{count}}(?<=-|\||\/)/
|
303
366
|
|
304
367
|
positions = []
|
305
|
-
index =
|
306
|
-
while index and index <
|
368
|
+
index = starts
|
369
|
+
while index and index < ends
|
307
370
|
index = graph.index(pattern, index)
|
308
371
|
if index
|
309
|
-
positions << index
|
372
|
+
positions << index if index < ends
|
310
373
|
index += 1
|
311
374
|
end
|
312
375
|
end
|