plansheet 0.12.3 → 0.17.1

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 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