plansheet 0.13.2 → 0.23.0
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.
- checksums.yaml +4 -4
- data/Gemfile +2 -1
- data/Gemfile.lock +20 -6
- data/Guardfile +12 -5
- data/exe/plansheet +37 -7
- data/lib/plansheet/pool.rb +107 -0
- data/lib/plansheet/project/stringify.rb +3 -1
- data/lib/plansheet/project/yaml.rb +48 -10
- data/lib/plansheet/project.rb +146 -36
- data/lib/plansheet/sheet.rb +12 -7
- data/lib/plansheet/version.rb +1 -1
- data/lib/plansheet.rb +2 -22
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 209e9e64f6e8f0ceba2e1182e60106ead59d0943331c940a9a38ff788ab2cd4f
|
4
|
+
data.tar.gz: 559296847d7777c6d339fd941f43dc8b106b00b75efa354a95ff78adcfc715b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d883a334412977859444a6db5c33885ced07fabcfa8665aa0331be6609fc94bb7c3d15f4e5fa6a8b4c01bf7e537b4c281b88c90ac30337fcc3c326ef828dfb29
|
7
|
+
data.tar.gz: 4033cae733fd661360022a705b9d3098d07431db2d2377406af95eeb52fc60ace9ba881803557e8146c8bd48524e3f07cd0ab5eff9859b83633c51fc41115306
|
data/Gemfile
CHANGED
@@ -7,9 +7,10 @@ gemspec
|
|
7
7
|
|
8
8
|
group :development, optional: true do
|
9
9
|
gem "guard", "~> 2.18"
|
10
|
-
gem "guard-
|
10
|
+
gem "guard-rake", "~> 1.0"
|
11
11
|
gem "minitest", "~> 5.0"
|
12
12
|
gem "rake", "~> 13.0"
|
13
|
+
gem "rdoc"
|
13
14
|
|
14
15
|
gem "rubocop", "~> 1.21"
|
15
16
|
gem "rubocop-minitest"
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
plansheet (0.
|
4
|
+
plansheet (0.23.0)
|
5
5
|
dc-kwalify (~> 1.0)
|
6
|
+
rgl (= 0.5.8)
|
6
7
|
|
7
8
|
GEM
|
8
9
|
remote: https://rubygems.org/
|
@@ -12,6 +13,7 @@ GEM
|
|
12
13
|
dc-kwalify (1.0.0)
|
13
14
|
ffi (1.15.5)
|
14
15
|
formatador (1.1.0)
|
16
|
+
generator (0.0.1)
|
15
17
|
guard (2.18.0)
|
16
18
|
formatador (>= 0.2.4)
|
17
19
|
listen (>= 2.7, < 4.0)
|
@@ -21,10 +23,10 @@ GEM
|
|
21
23
|
pry (>= 0.13.0)
|
22
24
|
shellany (~> 0.0)
|
23
25
|
thor (>= 0.18.1)
|
24
|
-
guard-
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
guard-rake (1.0.0)
|
27
|
+
guard
|
28
|
+
rake
|
29
|
+
lazy_priority_queue (0.1.1)
|
28
30
|
listen (3.7.1)
|
29
31
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
30
32
|
rb-inotify (~> 0.9, >= 0.9.10)
|
@@ -41,13 +43,21 @@ GEM
|
|
41
43
|
pry (0.14.1)
|
42
44
|
coderay (~> 1.1)
|
43
45
|
method_source (~> 1.0)
|
46
|
+
psych (4.0.4)
|
47
|
+
stringio
|
44
48
|
rainbow (3.1.1)
|
45
49
|
rake (13.0.6)
|
46
50
|
rb-fsevent (0.11.1)
|
47
51
|
rb-inotify (0.10.1)
|
48
52
|
ffi (~> 1.0)
|
53
|
+
rdoc (6.4.0)
|
54
|
+
psych (>= 4.0.0)
|
49
55
|
regexp_parser (2.4.0)
|
50
56
|
rexml (3.2.5)
|
57
|
+
rgl (0.5.8)
|
58
|
+
lazy_priority_queue (~> 0.1.0)
|
59
|
+
rexml (~> 3.2, >= 3.2.4)
|
60
|
+
stream (~> 0.5.3)
|
51
61
|
rubocop (1.29.1)
|
52
62
|
parallel (~> 1.10)
|
53
63
|
parser (>= 3.1.0.0)
|
@@ -65,6 +75,9 @@ GEM
|
|
65
75
|
rubocop (~> 1.0)
|
66
76
|
ruby-progressbar (1.11.0)
|
67
77
|
shellany (0.0.1)
|
78
|
+
stream (0.5.4)
|
79
|
+
generator
|
80
|
+
stringio (3.0.2)
|
68
81
|
thor (1.2.1)
|
69
82
|
unicode-display_width (2.1.0)
|
70
83
|
|
@@ -73,10 +86,11 @@ PLATFORMS
|
|
73
86
|
|
74
87
|
DEPENDENCIES
|
75
88
|
guard (~> 2.18)
|
76
|
-
guard-
|
89
|
+
guard-rake (~> 1.0)
|
77
90
|
minitest (~> 5.0)
|
78
91
|
plansheet!
|
79
92
|
rake (~> 13.0)
|
93
|
+
rdoc
|
80
94
|
rubocop (~> 1.21)
|
81
95
|
rubocop-minitest
|
82
96
|
rubocop-rake
|
data/Guardfile
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
directories(%w[lib test].select { |d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist") })
|
3
|
+
directories(%w[. exe bin lib test].select { |d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist") })
|
4
4
|
|
5
|
-
guard :
|
6
|
-
watch(
|
7
|
-
watch(
|
8
|
-
watch(
|
5
|
+
guard :rake, task: "default" do
|
6
|
+
watch("Gemfile")
|
7
|
+
watch("Rakefile")
|
8
|
+
watch("Guardfile")
|
9
|
+
watch(%r{^test/test_(.*)\.rb$})
|
10
|
+
watch("exe/plansheet")
|
11
|
+
watch("bin/console")
|
12
|
+
watch(%r{^test/test_(.*)\.rb$})
|
13
|
+
watch(%r{^lib/plansheet/(.*)\.rb$})
|
14
|
+
watch(%r{^lib/plansheet/project/(.*)\.rb$})
|
15
|
+
watch(%r{^lib/plansheet\.rb$})
|
9
16
|
end
|
data/exe/plansheet
CHANGED
@@ -13,10 +13,22 @@ parser.on(
|
|
13
13
|
"--sort",
|
14
14
|
"Sort project files"
|
15
15
|
)
|
16
|
+
parser.on(
|
17
|
+
"--irb",
|
18
|
+
"Open IRB console after loading projects"
|
19
|
+
)
|
16
20
|
parser.on(
|
17
21
|
"--cli",
|
18
22
|
"CLI dump of projects (WIP)"
|
19
23
|
)
|
24
|
+
parser.on(
|
25
|
+
"--stats",
|
26
|
+
"Various stats (WIP)"
|
27
|
+
)
|
28
|
+
parser.on(
|
29
|
+
"--calendar",
|
30
|
+
"List of projects ordered by due date"
|
31
|
+
)
|
20
32
|
parser.on(
|
21
33
|
"--location_filter LOCATION",
|
22
34
|
"location filter for CLI dump (WIP)"
|
@@ -25,18 +37,36 @@ options = {}
|
|
25
37
|
parser.parse!(into: options)
|
26
38
|
|
27
39
|
config = Plansheet.load_config
|
40
|
+
pool = Plansheet::Pool.new({ projects_dir: config["projects_dir"],
|
41
|
+
sort_order: config["sort_order"] })
|
28
42
|
|
29
43
|
if options[:sheet] || options.empty?
|
30
|
-
|
31
|
-
|
44
|
+
require "plansheet/sheet"
|
32
45
|
Dir.mkdir config["output_dir"] unless Dir.exist? config["output_dir"]
|
33
|
-
|
34
|
-
|
46
|
+
Plansheet::Sheet.new("#{config["output_dir"]}/projects.md", pool.projects)
|
47
|
+
elsif options[:irb]
|
48
|
+
binding.irb # rubocop:disable Lint/Debugger
|
49
|
+
elsif options[:stats]
|
50
|
+
puts "# of projects: #{pool.projects.count}"
|
51
|
+
puts "# of tasks: #{pool.projects.sum { |x| x&.tasks&.count || 0 }}"
|
52
|
+
puts "# of locations: #{pool.projects.collect(&:location).flatten.delete_if(&:nil?).uniq.count}"
|
53
|
+
puts "combined time estimate: #{pool.projects.sum { |x| x.time_estimate_minutes || 0 }} minutes"
|
35
54
|
elsif options[:sort]
|
36
|
-
|
55
|
+
# Pool sorts projects, this now just matches old behaviour
|
56
|
+
pool.write_projects
|
57
|
+
elsif options[:calendar]
|
58
|
+
# TODO: add a project filter method
|
59
|
+
project_arr = pool.projects
|
60
|
+
project_arr.delete_if { |x| x.status == "dropped" || x.status == "done" }
|
61
|
+
project_arr.delete_if { |x| x.due.nil? }
|
62
|
+
project_arr.sort_by!(&:due)
|
63
|
+
project_arr.each do |proj|
|
64
|
+
puts proj
|
65
|
+
puts "\n"
|
66
|
+
end
|
37
67
|
elsif options[:cli]
|
38
|
-
|
39
|
-
project_arr.
|
68
|
+
# TODO: add a project filter method
|
69
|
+
project_arr = pool.projects
|
40
70
|
project_arr.delete_if { |x| x.status == "dropped" || x.status == "done" }
|
41
71
|
project_arr.select! { |x| x.location == options[:location_filter] } if options[:location_filter]
|
42
72
|
project_arr.each do |proj|
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rgl/adjacency"
|
4
|
+
require "rgl/topsort"
|
5
|
+
|
6
|
+
module Plansheet
|
7
|
+
# The "pool" is the aggregated collection of projects, calendar events, etc.
|
8
|
+
class Pool
|
9
|
+
attr_accessor :projects
|
10
|
+
|
11
|
+
DEFAULT_COMPARISON_ORDER = %w[
|
12
|
+
completeness
|
13
|
+
dependency
|
14
|
+
priority
|
15
|
+
defer
|
16
|
+
due
|
17
|
+
time_roi
|
18
|
+
status
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
def initialize(config, debug: false)
|
22
|
+
@projects_dir = config[:projects_dir]
|
23
|
+
@sort_order = config[:sort_order]
|
24
|
+
# @completed_projects_dir = config(:completed_projects_dir)
|
25
|
+
|
26
|
+
# This bit of trickiness is because we don't know what the sort order is
|
27
|
+
# until runtime. I'm sure this design decision definitely won't bite me
|
28
|
+
# in the future ;-) Fortunately, it's also not a problem that can't be
|
29
|
+
# walked back from.
|
30
|
+
# rubocop:disable Lint/OrAssignmentToConstant
|
31
|
+
Plansheet::Pool::POOL_COMPARISON_ORDER ||= config[:sort_order] if config[:sort_order]
|
32
|
+
puts "using config sort order" if config[:sort_order]
|
33
|
+
Plansheet::Pool::POOL_COMPARISON_ORDER ||= Plansheet::Pool::DEFAULT_COMPARISON_ORDER
|
34
|
+
# rubocop:enable Lint/OrAssignmentToConstant
|
35
|
+
require_relative "project"
|
36
|
+
|
37
|
+
load_projects_dir(@projects_dir) unless debug
|
38
|
+
sort_projects if @projects
|
39
|
+
end
|
40
|
+
|
41
|
+
def sort_projects
|
42
|
+
@projects ||= []
|
43
|
+
@projects.sort!
|
44
|
+
# lookup_hash returns the index of a project
|
45
|
+
lookup_hash = Hash.new nil
|
46
|
+
|
47
|
+
# initialize the lookups
|
48
|
+
@projects.each_index do |i|
|
49
|
+
lookup_hash[@projects[i].name] = i
|
50
|
+
end
|
51
|
+
|
52
|
+
pg = RGL::DirectedAdjacencyGraph.new
|
53
|
+
pg.add_vertices @projects
|
54
|
+
@projects.each_index do |proj_index|
|
55
|
+
next if @projects[proj_index].dropped_or_done?
|
56
|
+
|
57
|
+
@projects[proj_index]&.dependencies&.each do |dep|
|
58
|
+
di = lookup_hash[dep]
|
59
|
+
if di
|
60
|
+
# Don't add edges for dropped/done projects, they'll be sorted out
|
61
|
+
# later
|
62
|
+
next if @projects[di].dropped_or_done?
|
63
|
+
|
64
|
+
pg.add_edge(@projects[di], @projects[proj_index])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# The topological sort of pg is the correct dependency order of the
|
70
|
+
# projects
|
71
|
+
@projects = pg.topsort_iterator.to_a.flatten.uniq
|
72
|
+
|
73
|
+
# TODO: second sort doesn't deal with problems where deferred task gets
|
74
|
+
# pushed below.
|
75
|
+
@projects.sort!
|
76
|
+
end
|
77
|
+
|
78
|
+
def project_namespaces
|
79
|
+
@projects.collect(&:namespace).uniq.sort
|
80
|
+
end
|
81
|
+
|
82
|
+
def projects_in_namespace(namespace)
|
83
|
+
@projects.select { |x| x.namespace == namespace }
|
84
|
+
end
|
85
|
+
|
86
|
+
def write_projects
|
87
|
+
# TODO: This leaves potential for duplicate projects where empty files
|
88
|
+
# are involved once completed project directories are a thing - will need
|
89
|
+
# to keep a list of project files to delete
|
90
|
+
project_namespaces.each do |ns|
|
91
|
+
pyf = ProjectYAMLFile.new "#{@projects_dir}/#{ns}.yml"
|
92
|
+
pyf.projects = projects_in_namespace(ns)
|
93
|
+
pyf.write
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def load_projects_dir(dir)
|
98
|
+
project_arr = []
|
99
|
+
projects = Dir.glob("*yml", base: dir)
|
100
|
+
projects.each do |l|
|
101
|
+
project_arr << ProjectYAMLFile.new(File.join(dir, l)).load_file
|
102
|
+
end
|
103
|
+
|
104
|
+
@projects = project_arr.flatten!
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -4,9 +4,13 @@ require "yaml"
|
|
4
4
|
require "date"
|
5
5
|
require "pathname"
|
6
6
|
|
7
|
+
require "kwalify"
|
8
|
+
|
7
9
|
module Plansheet
|
8
10
|
# Once there's some stability in plansheet and dc-kwalify, will pre-load this
|
9
11
|
# to save the later YAML.load
|
12
|
+
YAML_TIME_REGEX = "/\\d+[mh] ?\d*m?/" # TODO: regex is bad, adjust for better handling of pretty time
|
13
|
+
YAML_DATE_REGEX = "/\\d+[dw]/"
|
10
14
|
PROJECT_YAML_SCHEMA = <<~YAML
|
11
15
|
desc: dc-tasks project schema
|
12
16
|
type: seq
|
@@ -16,11 +20,10 @@ module Plansheet
|
|
16
20
|
"project":
|
17
21
|
desc: Project name
|
18
22
|
type: str
|
19
|
-
|
23
|
+
unique: true
|
20
24
|
"namespace":
|
21
|
-
desc:
|
25
|
+
desc: Project name
|
22
26
|
type: str
|
23
|
-
required: yes
|
24
27
|
"priority":
|
25
28
|
desc: Project priority
|
26
29
|
type: str
|
@@ -51,14 +54,42 @@ module Plansheet
|
|
51
54
|
"time_estimate":
|
52
55
|
desc: The estimated amount of time before a project is completed
|
53
56
|
type: str
|
57
|
+
pattern: #{YAML_TIME_REGEX}
|
58
|
+
"daily_time_roi":
|
59
|
+
desc: The estimated amount of time saved daily by completing this project
|
60
|
+
type: str
|
61
|
+
pattern: #{YAML_TIME_REGEX}
|
62
|
+
"weekly_time_roi":
|
63
|
+
desc: The estimated amount of time saved daily by completing this project
|
64
|
+
type: str
|
65
|
+
pattern: #{YAML_TIME_REGEX}
|
66
|
+
"yearly_time_roi":
|
67
|
+
desc: The estimated amount of time saved daily by completing this project
|
68
|
+
type: str
|
69
|
+
pattern: #{YAML_TIME_REGEX}
|
70
|
+
"day_of_week":
|
71
|
+
desc: recurring day of week project
|
72
|
+
type: str
|
73
|
+
enum:
|
74
|
+
- Sunday
|
75
|
+
- Monday
|
76
|
+
- Tuesday
|
77
|
+
- Wednesday
|
78
|
+
- Thursday
|
79
|
+
- Friday
|
80
|
+
- Saturday
|
54
81
|
"frequency":
|
55
|
-
desc: The amount of time before a recurring project moves to ready status again from when it was last done (
|
82
|
+
desc: The amount of time before a recurring project moves to ready status again from when it was last done, with a set due date (eg. a bill becomes due)
|
83
|
+
type: str
|
84
|
+
pattern: #{YAML_DATE_REGEX}
|
85
|
+
"last_for":
|
86
|
+
desc: "The amount of time before a recurring project moves to ready status again from when it was last done, with a set defer date (eg. inflating a bike tire). If your project 'can't wait' a day or two, you should use frequency."
|
56
87
|
type: str
|
57
|
-
pattern:
|
88
|
+
pattern: #{YAML_DATE_REGEX}
|
58
89
|
"lead_time":
|
59
90
|
desc: The amount of time before a recurring project is "due" moved to ready where the project (sort of a deferral mechanism) (WIP)
|
60
91
|
type: str
|
61
|
-
pattern:
|
92
|
+
pattern: #{YAML_DATE_REGEX}
|
62
93
|
"due":
|
63
94
|
desc: Due date of the task
|
64
95
|
type: date
|
@@ -114,17 +145,19 @@ module Plansheet
|
|
114
145
|
PROJECT_SCHEMA = YAML.safe_load(PROJECT_YAML_SCHEMA)
|
115
146
|
|
116
147
|
class ProjectYAMLFile
|
117
|
-
|
148
|
+
attr_accessor :projects
|
118
149
|
|
119
150
|
def initialize(path)
|
120
151
|
@path = path
|
121
152
|
# TODO: this won't GC, inline validation instead?
|
153
|
+
end
|
122
154
|
|
155
|
+
def load_file
|
123
156
|
# Handle pre-Ruby 3.1 psych versions (this is brittle)
|
124
157
|
@raw = if Psych::VERSION.split(".")[0].to_i >= 4
|
125
|
-
YAML.load_file(path, permitted_classes: [Date])
|
158
|
+
YAML.load_file(@path, permitted_classes: [Date])
|
126
159
|
else
|
127
|
-
YAML.load_file(path)
|
160
|
+
YAML.load_file(@path)
|
128
161
|
end
|
129
162
|
|
130
163
|
validate_schema
|
@@ -133,6 +166,7 @@ module Plansheet
|
|
133
166
|
proj["namespace"] = namespace
|
134
167
|
Project.new proj
|
135
168
|
end
|
169
|
+
@projects
|
136
170
|
end
|
137
171
|
|
138
172
|
def namespace
|
@@ -155,8 +189,12 @@ module Plansheet
|
|
155
189
|
@projects.sort!
|
156
190
|
end
|
157
191
|
|
192
|
+
def write
|
193
|
+
File.write @path, yaml_dump
|
194
|
+
end
|
195
|
+
|
158
196
|
def yaml_dump
|
159
|
-
YAML.dump(@projects.map
|
197
|
+
YAML.dump(@projects.map { |x| x.to_h.delete_if { |k, _| k == "namespace" } })
|
160
198
|
end
|
161
199
|
end
|
162
200
|
end
|
data/lib/plansheet/project.rb
CHANGED
@@ -5,6 +5,13 @@ require "date"
|
|
5
5
|
require_relative "project/yaml"
|
6
6
|
require_relative "project/stringify"
|
7
7
|
|
8
|
+
# Needed for Project#time_estimate, would be much happier *not* patching Array
|
9
|
+
class Array
|
10
|
+
def nil_if_empty
|
11
|
+
count.zero? ? nil : self
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
8
15
|
module Plansheet
|
9
16
|
PROJECT_STATUS_PRIORITY = {
|
10
17
|
"wip" => 1,
|
@@ -17,11 +24,11 @@ module Plansheet
|
|
17
24
|
"done" => 8
|
18
25
|
}.freeze
|
19
26
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
"
|
24
|
-
|
27
|
+
# Pre-compute the next days-of-week
|
28
|
+
NEXT_DOW = 0.upto(6).to_h do |x|
|
29
|
+
d = Date.today + x
|
30
|
+
[d.strftime("%A"), d]
|
31
|
+
end.freeze
|
25
32
|
|
26
33
|
def self.parse_date_duration(str)
|
27
34
|
return Regexp.last_match(1).to_i if str.strip.match(/(\d+)[dD]/)
|
@@ -30,6 +37,29 @@ module Plansheet
|
|
30
37
|
raise "Can't parse time duration string #{str}"
|
31
38
|
end
|
32
39
|
|
40
|
+
def self.parse_time_duration(str)
|
41
|
+
if str.match(/(\d+h) (\d+m)/)
|
42
|
+
return (parse_time_duration(Regexp.last_match(1)) + parse_time_duration(Regexp.last_match(2)))
|
43
|
+
end
|
44
|
+
|
45
|
+
return Regexp.last_match(1).to_i if str.strip.match(/(\d+)m/)
|
46
|
+
return (Regexp.last_match(1).to_f * 60).to_i if str.strip.match(/(\d+\.?\d*)h/)
|
47
|
+
|
48
|
+
raise "Can't parse time duration string #{str}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.build_time_duration(minutes)
|
52
|
+
if minutes > 59
|
53
|
+
if (minutes % 60).zero?
|
54
|
+
"#{minutes / 60}h"
|
55
|
+
else
|
56
|
+
"#{minutes / 60}h #{minutes % 60}m"
|
57
|
+
end
|
58
|
+
else
|
59
|
+
"#{minutes}m"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
33
63
|
# The use of instance_variable_set/get probably seems a bit weird, but the
|
34
64
|
# intent is to avoid object allocation on non-existent project properties, as
|
35
65
|
# well as avoiding a bunch of copy-paste boilerplate when adding a new
|
@@ -39,26 +69,31 @@ module Plansheet
|
|
39
69
|
class Project
|
40
70
|
include Comparable
|
41
71
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
72
|
+
TIME_EST_REGEX = /\((\d+\.?\d*[mMhH])\)$/.freeze
|
73
|
+
TIME_EST_REGEX_NO_CAPTURE = /\(\d+\.?\d*[mMhH]\)$/.freeze
|
74
|
+
|
75
|
+
PROJECT_PRIORITY = {
|
76
|
+
"high" => 1,
|
77
|
+
"medium" => 2,
|
78
|
+
"low" => 3
|
79
|
+
}.freeze
|
80
|
+
|
81
|
+
COMPARISON_ORDER_SYMS = Plansheet::Pool::POOL_COMPARISON_ORDER.map { |x| "compare_#{x}".to_sym }.freeze
|
50
82
|
# NOTE: The order of these affects presentation!
|
51
83
|
# namespace is derived from file name
|
52
|
-
STRING_PROPERTIES = %w[
|
84
|
+
STRING_PROPERTIES = %w[priority status location notes time_estimate daily_time_roi weekly_time_roi yearly_time_roi
|
85
|
+
day_of_week frequency last_for lead_time].freeze
|
53
86
|
DATE_PROPERTIES = %w[due defer completed_on created_on starts_on last_done last_reviewed].freeze
|
54
87
|
ARRAY_PROPERTIES = %w[dependencies externals urls tasks done tags].freeze
|
55
88
|
|
56
89
|
ALL_PROPERTIES = STRING_PROPERTIES + DATE_PROPERTIES + ARRAY_PROPERTIES
|
57
90
|
|
58
|
-
attr_reader :name, *ALL_PROPERTIES
|
91
|
+
attr_reader :name, :priority_val, :time_estimate_minutes, *ALL_PROPERTIES
|
92
|
+
attr_accessor :namespace
|
59
93
|
|
60
94
|
def initialize(options)
|
61
95
|
@name = options["project"]
|
96
|
+
@namespace = options["namespace"]
|
62
97
|
|
63
98
|
ALL_PROPERTIES.each do |o|
|
64
99
|
instance_variable_set("@#{o}", options[o]) if options[o]
|
@@ -74,12 +109,51 @@ module Plansheet
|
|
74
109
|
# date/external commits/penalties for project failure, etc
|
75
110
|
#
|
76
111
|
# Assume all projects are low priority unless stated otherwise.
|
77
|
-
@
|
112
|
+
@priority_val = if @priority
|
113
|
+
PROJECT_PRIORITY[@priority]
|
114
|
+
else
|
115
|
+
PROJECT_PRIORITY["low"]
|
116
|
+
end
|
117
|
+
|
118
|
+
# Remove stale defer dates
|
119
|
+
remove_instance_variable("@defer") if @defer && (@defer < Date.today)
|
120
|
+
|
121
|
+
# Add a created_on field if it doesn't exist
|
122
|
+
instance_variable_set("@created_on", Date.today) unless @created_on
|
123
|
+
|
124
|
+
# Generate time estimate from tasks if specified
|
125
|
+
# Stomps time_estimate field
|
126
|
+
if @tasks
|
127
|
+
@time_estimate_minutes = @tasks&.select do |t|
|
128
|
+
t.match? TIME_EST_REGEX_NO_CAPTURE
|
129
|
+
end&.nil_if_empty&.map { |t| Plansheet::Project.task_time_estimate(t) }&.sum
|
130
|
+
elsif @time_estimate
|
131
|
+
# No tasks with estimates, but there's an explicit time_estimate
|
132
|
+
# Convert the field to minutes
|
133
|
+
@time_estimate_minutes = Plansheet.parse_time_duration(@time_estimate)
|
134
|
+
end
|
135
|
+
if @time_estimate_minutes # rubocop:disable Style/GuardClause
|
136
|
+
# Rewrite time_estimate field
|
137
|
+
@time_estimate = Plansheet.build_time_duration(@time_estimate_minutes)
|
138
|
+
|
139
|
+
yms = yearly_minutes_saved
|
140
|
+
@time_roi_payoff = yms.to_f / @time_estimate_minutes if yms
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def yearly_minutes_saved
|
145
|
+
if @daily_time_roi
|
146
|
+
Plansheet.parse_time_duration(@daily_time_roi) * 365
|
147
|
+
elsif @weekly_time_roi
|
148
|
+
Plansheet.parse_time_duration(@weekly_time_roi) * 52
|
149
|
+
elsif @yearly_time_roi
|
150
|
+
Plansheet.parse_time_duration(@yearly_time_roi)
|
151
|
+
end
|
78
152
|
end
|
79
153
|
|
80
154
|
def <=>(other)
|
81
155
|
ret_val = 0
|
82
|
-
|
156
|
+
COMPARISON_ORDER_SYMS.each do |method|
|
83
157
|
ret_val = send(method, other)
|
84
158
|
break if ret_val != 0
|
85
159
|
end
|
@@ -87,7 +161,15 @@ module Plansheet
|
|
87
161
|
end
|
88
162
|
|
89
163
|
def compare_priority(other)
|
90
|
-
|
164
|
+
priority_val <=> other.priority_val
|
165
|
+
end
|
166
|
+
|
167
|
+
def time_roi_payoff
|
168
|
+
@time_roi_payoff || 0
|
169
|
+
end
|
170
|
+
|
171
|
+
def compare_time_roi(other)
|
172
|
+
other.time_roi_payoff <=> time_roi_payoff
|
91
173
|
end
|
92
174
|
|
93
175
|
def compare_status(other)
|
@@ -115,19 +197,25 @@ module Plansheet
|
|
115
197
|
receiver <=> comparison
|
116
198
|
end
|
117
199
|
|
118
|
-
def
|
119
|
-
|
200
|
+
def dependency_of?(other)
|
201
|
+
other&.dependencies&.any? do |dep|
|
202
|
+
@name&.downcase == dep.downcase
|
203
|
+
end
|
204
|
+
end
|
120
205
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
end
|
125
|
-
elsif @dependencies.any? do |dep|
|
126
|
-
other.name.downcase == dep.downcase
|
127
|
-
end
|
128
|
-
return 1
|
206
|
+
def dependent_on?(other)
|
207
|
+
@dependencies&.any? do |dep|
|
208
|
+
other&.name&.downcase == dep.downcase
|
129
209
|
end
|
130
|
-
|
210
|
+
end
|
211
|
+
|
212
|
+
def compare_dependency(other)
|
213
|
+
# This approach might seem odd,
|
214
|
+
# but it's to handle circular dependencies
|
215
|
+
retval = 0
|
216
|
+
retval -= 1 if dependency_of?(other)
|
217
|
+
retval += 1 if dependent_on?(other)
|
218
|
+
retval
|
131
219
|
end
|
132
220
|
|
133
221
|
# Projects that are dropped or done are considered "complete", insofar as
|
@@ -173,7 +261,8 @@ module Plansheet
|
|
173
261
|
|
174
262
|
def subsequent_recurring_status
|
175
263
|
return "done" if @lead_time && defer > Date.today
|
176
|
-
return "done" if
|
264
|
+
return "done" if @last_for && defer > Date.today
|
265
|
+
return "done" if due && due > Date.today
|
177
266
|
|
178
267
|
task_based_status
|
179
268
|
end
|
@@ -186,41 +275,62 @@ module Plansheet
|
|
186
275
|
# Due date either explicit or recurring
|
187
276
|
def due
|
188
277
|
return @due if @due
|
189
|
-
return recurring_due_date if
|
278
|
+
return recurring_due_date if recurring_due?
|
190
279
|
|
191
280
|
nil
|
192
281
|
end
|
193
282
|
|
194
283
|
def recurring_due_date
|
195
284
|
if @last_done
|
196
|
-
@last_done + Plansheet.parse_date_duration(@frequency)
|
197
|
-
|
198
|
-
|
285
|
+
return @last_done + Plansheet.parse_date_duration(@frequency) if @frequency
|
286
|
+
|
287
|
+
if @day_of_week
|
288
|
+
return Date.today + 7 if @last_done == Date.today
|
289
|
+
return @last_done + 7 if @last_done < Date.today - 7
|
290
|
+
|
291
|
+
return NEXT_DOW[@day_of_week]
|
292
|
+
end
|
199
293
|
end
|
294
|
+
|
295
|
+
# Going to assume this is the first time, so due today
|
296
|
+
Date.today
|
200
297
|
end
|
201
298
|
|
202
299
|
def defer
|
203
300
|
return @defer if @defer
|
204
301
|
return lead_time_deferral if @lead_time && due
|
302
|
+
return last_for_deferral if @last_for
|
205
303
|
|
206
304
|
nil
|
207
305
|
end
|
208
306
|
|
307
|
+
def last_for_deferral
|
308
|
+
return @last_done + Plansheet.parse_date_duration(@last_for) if @last_done
|
309
|
+
end
|
310
|
+
|
209
311
|
def lead_time_deferral
|
210
312
|
[(due - Plansheet.parse_date_duration(@lead_time)),
|
211
313
|
Date.today].max
|
212
314
|
end
|
213
315
|
|
316
|
+
def recurring_due?
|
317
|
+
!@frequency.nil? || !@day_of_week.nil?
|
318
|
+
end
|
319
|
+
|
214
320
|
def recurring?
|
215
|
-
!@frequency.nil?
|
321
|
+
!@frequency.nil? || !@day_of_week.nil? || !@last_done.nil?
|
216
322
|
end
|
217
323
|
|
218
324
|
def dropped_or_done?
|
219
325
|
status == "dropped" || status == "done"
|
220
326
|
end
|
221
327
|
|
328
|
+
def self.task_time_estimate(str)
|
329
|
+
Plansheet.parse_time_duration(Regexp.last_match(1)) if str.match(TIME_EST_REGEX)
|
330
|
+
end
|
331
|
+
|
222
332
|
def to_h
|
223
|
-
h = { "project" => @name }
|
333
|
+
h = { "project" => @name, "namespace" => @namespace }
|
224
334
|
ALL_PROPERTIES.each do |prop|
|
225
335
|
h[prop] = instance_variable_get("@#{prop}") if instance_variable_defined?("@#{prop}")
|
226
336
|
end
|
data/lib/plansheet/sheet.rb
CHANGED
@@ -5,12 +5,10 @@ module Plansheet
|
|
5
5
|
# The Sheet class constructs a Markdown/LaTeX file for use with pandoc
|
6
6
|
class Sheet
|
7
7
|
def initialize(output_file, project_arr)
|
8
|
-
sorted_arr = project_arr.sort!
|
9
|
-
|
10
8
|
projects_str = String.new
|
11
9
|
projects_str << sheet_header
|
12
10
|
|
13
|
-
|
11
|
+
project_arr.each do |p|
|
14
12
|
projects_str << project_minipage(p)
|
15
13
|
end
|
16
14
|
puts "Writing to #{output_file}"
|
@@ -30,19 +28,26 @@ module Plansheet
|
|
30
28
|
|
31
29
|
def project_minipage(proj)
|
32
30
|
str = String.new
|
33
|
-
str << "\\begin{minipage}{
|
31
|
+
str << "\\begin{minipage}{6cm}\n"
|
34
32
|
str << project_header(proj)
|
35
33
|
proj&.tasks&.each do |t|
|
36
|
-
str << "$\\square$ #{t} \\\\\n"
|
34
|
+
str << "$\\square$ #{sanitize_string(t)} \\\\\n"
|
37
35
|
end
|
38
36
|
str << "\\end{minipage}\n"
|
39
37
|
str
|
40
38
|
end
|
41
39
|
|
40
|
+
def sanitize_string(str)
|
41
|
+
str.gsub("_", '\_')
|
42
|
+
end
|
43
|
+
|
42
44
|
def project_header(proj)
|
43
45
|
str = String.new
|
44
|
-
str << "#{proj.
|
45
|
-
str <<
|
46
|
+
str << "#{sanitize_string(proj.namespace)}: #{sanitize_string(proj.name)}\\\\\n"
|
47
|
+
str << proj.status.to_s
|
48
|
+
str << " - #{sanitize_string(proj.location)}" if proj.location
|
49
|
+
str << " due: #{proj.due}" if proj.due
|
50
|
+
str << " time: #{proj.time_estimate}" if proj.time_estimate
|
46
51
|
str << " \\\\\n"
|
47
52
|
str
|
48
53
|
end
|
data/lib/plansheet/version.rb
CHANGED
data/lib/plansheet.rb
CHANGED
@@ -1,36 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "plansheet/version"
|
4
|
-
require_relative "plansheet/
|
5
|
-
require_relative "plansheet/sheet"
|
4
|
+
require_relative "plansheet/pool"
|
6
5
|
require "yaml"
|
7
|
-
require "kwalify"
|
8
6
|
|
9
7
|
module Plansheet
|
10
8
|
class Error < StandardError; end
|
11
9
|
|
10
|
+
# TODO: config schema validation
|
12
11
|
def self.load_config
|
13
12
|
YAML.load_file "#{Dir.home}/.plansheet.yml"
|
14
13
|
rescue StandardError
|
15
14
|
abort "unable to load plansheet config file"
|
16
15
|
end
|
17
|
-
|
18
|
-
def self.resort_projects_in_dir(dir)
|
19
|
-
project_files = Dir.glob("#{dir}/*yml")
|
20
|
-
project_files.each do |f|
|
21
|
-
pyf = ProjectYAMLFile.new(f)
|
22
|
-
pyf.sort!
|
23
|
-
File.write(f, pyf.yaml_dump)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.load_projects_dir(dir)
|
28
|
-
project_arr = []
|
29
|
-
projects = Dir.glob("*yml", base: dir)
|
30
|
-
projects.each do |l|
|
31
|
-
project_arr << ProjectYAMLFile.new(File.join(dir, l)).projects
|
32
|
-
end
|
33
|
-
|
34
|
-
project_arr.flatten!
|
35
|
-
end
|
36
16
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: plansheet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.23.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Crosby
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dc-kwalify
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rgl
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.5.8
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.5.8
|
27
41
|
description: Convert YAML project files into a nice PDF
|
28
42
|
email:
|
29
43
|
- dave@dafyddcrosby.com
|
@@ -43,6 +57,7 @@ files:
|
|
43
57
|
- examples/backpack.yml
|
44
58
|
- exe/plansheet
|
45
59
|
- lib/plansheet.rb
|
60
|
+
- lib/plansheet/pool.rb
|
46
61
|
- lib/plansheet/project.rb
|
47
62
|
- lib/plansheet/project/stringify.rb
|
48
63
|
- lib/plansheet/project/yaml.rb
|