plansheet 0.12.3 → 0.17.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72d3799f5166153c1775c768f0734cf0e390adfd80893ef1ae1aa710b7888b0b
4
- data.tar.gz: a18fd6eeb6a787438ddcdff1e6b0f25a17688f05a7bccff82495520517754b19
3
+ metadata.gz: 4c094d7236b6603dcab810d785eab766754e0cd9f161a2cb2daf280962b41026
4
+ data.tar.gz: 2686468ff7aa21915ff73ff27c49990cd05c2dc4e75c139837a69c1580a49dd5
5
5
  SHA512:
6
- metadata.gz: c97c83093f8e70948a96392b5d02329128c5f1031a0645b35b9c42bebeabb82192207c3ff71671081792ef3d61e9277c219984f0f558a7b54c1fac632fa68445
7
- data.tar.gz: 6723401a02c2086976e1002e4f8d08855f25726d1610fd9f4e593ae3ceac707c47bb6a1114ec1b40f707295c1c627fd5b72cce5ebe45f84b78b24d8304107bcf
6
+ metadata.gz: 646ede4ae64522cfc33912eaefae96083de293800647700ec04b2569fe305d40bf7855a0780c63262db5fdaa5adf8472dba4790bd5f0b60228acc7a921b84417
7
+ data.tar.gz: 0c56e57fa79c225d9c3edf055e5c033e9bb9c5b63a49d2d1e7ae1bc17e3c1298e086af4c06d88d270fe9a6e83f3d6f67bba6c4f65f09afbcaf80ed46570f4a3f
data/Gemfile CHANGED
@@ -10,6 +10,7 @@ group :development, optional: true do
10
10
  gem "guard-minitest", "~> 2.4"
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.12.3)
4
+ plansheet (0.17.1)
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)
@@ -25,6 +27,7 @@ GEM
25
27
  guard-minitest (2.4.6)
26
28
  guard-compat (~> 1.2)
27
29
  minitest (>= 3.0)
30
+ lazy_priority_queue (0.1.1)
28
31
  listen (3.7.1)
29
32
  rb-fsevent (~> 0.10, >= 0.10.3)
30
33
  rb-inotify (~> 0.9, >= 0.9.10)
@@ -41,13 +44,21 @@ GEM
41
44
  pry (0.14.1)
42
45
  coderay (~> 1.1)
43
46
  method_source (~> 1.0)
47
+ psych (4.0.4)
48
+ stringio
44
49
  rainbow (3.1.1)
45
50
  rake (13.0.6)
46
51
  rb-fsevent (0.11.1)
47
52
  rb-inotify (0.10.1)
48
53
  ffi (~> 1.0)
54
+ rdoc (6.4.0)
55
+ psych (>= 4.0.0)
49
56
  regexp_parser (2.4.0)
50
57
  rexml (3.2.5)
58
+ rgl (0.5.8)
59
+ lazy_priority_queue (~> 0.1.0)
60
+ rexml (~> 3.2, >= 3.2.4)
61
+ stream (~> 0.5.3)
51
62
  rubocop (1.29.1)
52
63
  parallel (~> 1.10)
53
64
  parser (>= 3.1.0.0)
@@ -65,6 +76,9 @@ GEM
65
76
  rubocop (~> 1.0)
66
77
  ruby-progressbar (1.11.0)
67
78
  shellany (0.0.1)
79
+ stream (0.5.4)
80
+ generator
81
+ stringio (3.0.2)
68
82
  thor (1.2.1)
69
83
  unicode-display_width (2.1.0)
70
84
 
@@ -77,6 +91,7 @@ DEPENDENCIES
77
91
  minitest (~> 5.0)
78
92
  plansheet!
79
93
  rake (~> 13.0)
94
+ rdoc
80
95
  rubocop (~> 1.21)
81
96
  rubocop-minitest
82
97
  rubocop-rake
data/exe/plansheet CHANGED
@@ -17,6 +17,10 @@ parser.on(
17
17
  "--cli",
18
18
  "CLI dump of projects (WIP)"
19
19
  )
20
+ parser.on(
21
+ "--calendar",
22
+ "List of projects ordered by due date"
23
+ )
20
24
  parser.on(
21
25
  "--location_filter LOCATION",
22
26
  "location filter for CLI dump (WIP)"
@@ -25,18 +29,29 @@ options = {}
25
29
  parser.parse!(into: options)
26
30
 
27
31
  config = Plansheet.load_config
32
+ pool = Plansheet::Pool.new({ projects_dir: config["projects_dir"],
33
+ sort_order: config["sort_order"] })
28
34
 
29
35
  if options[:sheet] || options.empty?
30
- project_arr = Plansheet.load_projects_dir config["projects_dir"]
31
-
36
+ require "plansheet/sheet"
32
37
  Dir.mkdir config["output_dir"] unless Dir.exist? config["output_dir"]
33
-
34
- Plansheet::Sheet.new("#{config["output_dir"]}/projects.md", project_arr)
38
+ Plansheet::Sheet.new("#{config["output_dir"]}/projects.md", pool.projects)
35
39
  elsif options[:sort]
36
- Plansheet.resort_projects_in_dir config["projects_dir"]
40
+ # Pool sorts projects, this now just matches old behaviour
41
+ pool.write_projects
42
+ elsif options[:calendar]
43
+ # TODO: add a project filter method
44
+ project_arr = pool.projects
45
+ project_arr.delete_if { |x| x.status == "dropped" || x.status == "done" }
46
+ project_arr.delete_if { |x| x.due.nil? }
47
+ project_arr.sort_by!(&:due)
48
+ project_arr.each do |proj|
49
+ puts proj
50
+ puts "\n"
51
+ end
37
52
  elsif options[:cli]
38
- project_arr = Plansheet.load_projects_dir config["projects_dir"]
39
- project_arr.sort!
53
+ # TODO: add a project filter method
54
+ project_arr = pool.projects
40
55
  project_arr.delete_if { |x| x.status == "dropped" || x.status == "done" }
41
56
  project_arr.select! { |x| x.location == options[:location_filter] } if options[:location_filter]
42
57
  project_arr.each do |proj|
@@ -0,0 +1,104 @@
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
+ status
18
+ ].freeze
19
+
20
+ def initialize(config)
21
+ @projects_dir = config[:projects_dir]
22
+ @sort_order = config[:sort_order]
23
+ # @completed_projects_dir = config(:completed_projects_dir)
24
+
25
+ # This bit of trickiness is because we don't know what the sort order is
26
+ # until runtime. I'm sure this design decision definitely won't bite me
27
+ # in the future ;-) Fortunately, it's also not a problem that can't be
28
+ # walked back from.
29
+ if config[:sort_order]
30
+ self.class.const_set("POOL_COMPARISON_ORDER", config[:sort_order])
31
+ else
32
+ self.class.const_set("POOL_COMPARISON_ORDER", Plansheet::Pool::DEFAULT_COMPARISON_ORDER)
33
+ end
34
+ require_relative "project"
35
+ load_projects_dir(@projects_dir)
36
+ sort_projects
37
+ end
38
+
39
+ def sort_projects
40
+ @projects.sort!
41
+ # lookup_hash returns the index of a project
42
+ lookup_hash = Hash.new nil
43
+
44
+ # initialize the lookups
45
+ @projects.each_index do |i|
46
+ lookup_hash[@projects[i].name] = i
47
+ end
48
+
49
+ pg = RGL::DirectedAdjacencyGraph.new
50
+ pg.add_vertices @projects
51
+ @projects.each_index do |proj_index|
52
+ next if @projects[proj_index].dropped_or_done?
53
+
54
+ @projects[proj_index]&.dependencies&.each do |dep|
55
+ di = lookup_hash[dep]
56
+ if di
57
+ # Don't add edges for dropped/done projects, they'll be sorted out
58
+ # later
59
+ next if @projects[di].dropped_or_done?
60
+
61
+ pg.add_edge(@projects[di], @projects[proj_index])
62
+ end
63
+ end
64
+ end
65
+
66
+ # The topological sort of pg is the correct dependency order of the
67
+ # projects
68
+ @projects = pg.topsort_iterator.to_a.flatten.uniq
69
+
70
+ # TODO: second sort doesn't deal with problems where deferred task gets
71
+ # pushed below.
72
+ @projects.sort!
73
+ end
74
+
75
+ def project_namespaces
76
+ @projects.collect(&:namespace).uniq.sort
77
+ end
78
+
79
+ def projects_in_namespace(namespace)
80
+ @projects.select { |x| x.namespace == namespace }
81
+ end
82
+
83
+ def write_projects
84
+ # TODO: This leaves potential for duplicate projects where empty files
85
+ # are involved once completed project directories are a thing - will need
86
+ # to keep a list of project files to delete
87
+ project_namespaces.each do |ns|
88
+ pyf = ProjectYAMLFile.new "#{@projects_dir}/#{ns}.yml"
89
+ pyf.projects = projects_in_namespace(ns)
90
+ pyf.write
91
+ end
92
+ end
93
+
94
+ def load_projects_dir(dir)
95
+ project_arr = []
96
+ projects = Dir.glob("*yml", base: dir)
97
+ projects.each do |l|
98
+ project_arr << ProjectYAMLFile.new(File.join(dir, l)).load_file
99
+ end
100
+
101
+ @projects = project_arr.flatten!
102
+ end
103
+ end
104
+ end
@@ -4,7 +4,9 @@ module Plansheet
4
4
  class Project
5
5
  def to_s
6
6
  str = String.new
7
- str << "# #{@name}\n"
7
+ str << "# "
8
+ str << "#{@namespace} - " if @namespace
9
+ str << "#{@name}\n"
8
10
  STRING_PROPERTIES.each do |o|
9
11
  str << stringify_string_property(o)
10
12
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  require "yaml"
4
4
  require "date"
5
+ require "pathname"
6
+
7
+ require "kwalify"
5
8
 
6
9
  module Plansheet
7
10
  # Once there's some stability in plansheet and dc-kwalify, will pre-load this
@@ -15,7 +18,9 @@ module Plansheet
15
18
  "project":
16
19
  desc: Project name
17
20
  type: str
18
- required: yes
21
+ "namespace":
22
+ desc: Project name
23
+ type: str
19
24
  "priority":
20
25
  desc: Project priority
21
26
  type: str
@@ -109,21 +114,33 @@ module Plansheet
109
114
  PROJECT_SCHEMA = YAML.safe_load(PROJECT_YAML_SCHEMA)
110
115
 
111
116
  class ProjectYAMLFile
112
- attr_reader :projects
117
+ attr_accessor :projects
113
118
 
114
119
  def initialize(path)
115
120
  @path = path
116
121
  # TODO: this won't GC, inline validation instead?
122
+ end
117
123
 
124
+ def load_file
118
125
  # Handle pre-Ruby 3.1 psych versions (this is brittle)
119
126
  @raw = if Psych::VERSION.split(".")[0].to_i >= 4
120
- YAML.load_file(path, permitted_classes: [Date])
127
+ YAML.load_file(@path, permitted_classes: [Date])
121
128
  else
122
- YAML.load_file(path)
129
+ YAML.load_file(@path)
123
130
  end
124
131
 
125
132
  validate_schema
126
- @projects = @raw.map { |proj| Project.new proj }
133
+ @raw ||= []
134
+ @projects = @raw.map do |proj|
135
+ proj["namespace"] = namespace
136
+ Project.new proj
137
+ end
138
+ @projects
139
+ end
140
+
141
+ def namespace
142
+ # TODO: yikes
143
+ ::Pathname.new(@path).basename.to_s.gsub(/\.yml$/, "")
127
144
  end
128
145
 
129
146
  def validate_schema
@@ -141,8 +158,12 @@ module Plansheet
141
158
  @projects.sort!
142
159
  end
143
160
 
161
+ def write
162
+ File.write @path, yaml_dump
163
+ end
164
+
144
165
  def yaml_dump
145
- YAML.dump(@projects.map(&:to_h))
166
+ YAML.dump(@projects.map { |x| x.to_h.delete_if { |k, _| k == "namespace" } })
146
167
  end
147
168
  end
148
169
  end
@@ -17,12 +17,6 @@ module Plansheet
17
17
  "done" => 8
18
18
  }.freeze
19
19
 
20
- PROJECT_PRIORITY = {
21
- "high" => 1,
22
- "medium" => 2,
23
- "low" => 3
24
- }.freeze
25
-
26
20
  def self.parse_date_duration(str)
27
21
  return Regexp.last_match(1).to_i if str.strip.match(/(\d+)[dD]/)
28
22
  return (Regexp.last_match(1).to_i * 7) if str.strip.match(/(\d+)[wW]/)
@@ -39,25 +33,27 @@ module Plansheet
39
33
  class Project
40
34
  include Comparable
41
35
 
42
- DEFAULT_COMPARISON_ORDER = %w[
43
- completeness
44
- dependency
45
- priority
46
- defer
47
- due
48
- status
49
- ].map { |x| "compare_#{x}".to_sym }.freeze
36
+ PROJECT_PRIORITY = {
37
+ "high" => 1,
38
+ "medium" => 2,
39
+ "low" => 3
40
+ }.freeze
41
+
42
+ COMPARISON_ORDER_SYMS = Plansheet::Pool::POOL_COMPARISON_ORDER.map { |x| "compare_#{x}".to_sym }.freeze
50
43
  # NOTE: The order of these affects presentation!
44
+ # namespace is derived from file name
51
45
  STRING_PROPERTIES = %w[priority status location notes time_estimate frequency lead_time].freeze
52
46
  DATE_PROPERTIES = %w[due defer completed_on created_on starts_on last_done last_reviewed].freeze
53
47
  ARRAY_PROPERTIES = %w[dependencies externals urls tasks done tags].freeze
54
48
 
55
49
  ALL_PROPERTIES = STRING_PROPERTIES + DATE_PROPERTIES + ARRAY_PROPERTIES
56
50
 
57
- attr_reader :name, *ALL_PROPERTIES
51
+ attr_reader :name, :priority_val, *ALL_PROPERTIES
52
+ attr_accessor :namespace
58
53
 
59
54
  def initialize(options)
60
55
  @name = options["project"]
56
+ @namespace = options["namespace"]
61
57
 
62
58
  ALL_PROPERTIES.each do |o|
63
59
  instance_variable_set("@#{o}", options[o]) if options[o]
@@ -73,12 +69,16 @@ module Plansheet
73
69
  # date/external commits/penalties for project failure, etc
74
70
  #
75
71
  # Assume all projects are low priority unless stated otherwise.
76
- @priority ||= "low"
72
+ @priority_val = if @priority
73
+ PROJECT_PRIORITY[@priority]
74
+ else
75
+ PROJECT_PRIORITY["low"]
76
+ end
77
77
  end
78
78
 
79
79
  def <=>(other)
80
80
  ret_val = 0
81
- DEFAULT_COMPARISON_ORDER.each do |method|
81
+ COMPARISON_ORDER_SYMS.each do |method|
82
82
  ret_val = send(method, other)
83
83
  break if ret_val != 0
84
84
  end
@@ -86,7 +86,7 @@ module Plansheet
86
86
  end
87
87
 
88
88
  def compare_priority(other)
89
- PROJECT_PRIORITY[@priority] <=> PROJECT_PRIORITY[other.priority]
89
+ priority_val <=> other.priority_val
90
90
  end
91
91
 
92
92
  def compare_status(other)
@@ -114,19 +114,25 @@ module Plansheet
114
114
  receiver <=> comparison
115
115
  end
116
116
 
117
- def compare_dependency(other)
118
- return 0 if @dependencies.nil? && other.dependencies.nil?
119
-
120
- if @dependencies.nil?
121
- return -1 if other.dependencies.any? do |dep|
122
- @name.downcase == dep.downcase
123
- end
124
- elsif @dependencies.any? do |dep|
125
- other.name.downcase == dep.downcase
126
- end
127
- return 1
117
+ def dependency_of?(other)
118
+ other&.dependencies&.any? do |dep|
119
+ @name&.downcase == dep.downcase
128
120
  end
129
- 0
121
+ end
122
+
123
+ def dependent_on?(other)
124
+ @dependencies&.any? do |dep|
125
+ other&.name&.downcase == dep.downcase
126
+ end
127
+ end
128
+
129
+ def compare_dependency(other)
130
+ # This approach might seem odd,
131
+ # but it's to handle circular dependencies
132
+ retval = 0
133
+ retval -= 1 if dependency_of?(other)
134
+ retval += 1 if dependent_on?(other)
135
+ retval
130
136
  end
131
137
 
132
138
  # Projects that are dropped or done are considered "complete", insofar as
@@ -219,7 +225,7 @@ module Plansheet
219
225
  end
220
226
 
221
227
  def to_h
222
- h = { "project" => @name }
228
+ h = { "project" => @name, "namespace" => @namespace }
223
229
  ALL_PROPERTIES.each do |prop|
224
230
  h[prop] = instance_variable_get("@#{prop}") if instance_variable_defined?("@#{prop}")
225
231
  end
@@ -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
- sorted_arr.each do |p|
11
+ project_arr.each do |p|
14
12
  projects_str << project_minipage(p)
15
13
  end
16
14
  puts "Writing to #{output_file}"
@@ -41,8 +39,9 @@ module Plansheet
41
39
 
42
40
  def project_header(proj)
43
41
  str = String.new
44
- str << "#{proj.name} - #{proj.status}"
42
+ str << "#{proj.namespace}: #{proj.name} - #{proj.status}"
45
43
  str << " - #{proj.location}" if proj.location
44
+ str << " - due #{proj.due}" if proj.due
46
45
  str << " \\\\\n"
47
46
  str
48
47
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plansheet
4
- VERSION = "0.12.3"
4
+ VERSION = "0.17.1"
5
5
  end
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/project"
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.12.3
4
+ version: 0.17.1
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-06-06 00:00:00.000000000 Z
11
+ date: 2022-06-30 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