kielce 2.0.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 +7 -0
- data/bin/kielce +4 -0
- data/lib/kielce.rb +87 -0
- data/lib/kielce/helpers.rb +29 -0
- data/lib/kielce/kielce.rb +113 -0
- data/lib/kielce/kielce_data.rb +151 -0
- data/lib/kielce/kielce_loader.rb +144 -0
- data/lib/kielce_plugins/schedule.rb +9 -0
- data/lib/kielce_plugins/schedule/#schedule.rb# +320 -0
- data/lib/kielce_plugins/schedule/assignment.rb +58 -0
- data/lib/kielce_plugins/schedule/schedule.rb +320 -0
- metadata +53 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 38341d1480aea6176a4d3d289f543a82c6846464268057cce43704b0c04fe811
|
4
|
+
data.tar.gz: 711eca88ec683a63c017c326ff9169e5c14f8456cc91a499cac68ce77071b79c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9359354d455c3e81e46d57f5a59da179137d60c016544cf57887677f67f5da578ccccf2e720a1c007703ddeaab093cd8c1403f417c947775220386722c9311ea
|
7
|
+
data.tar.gz: b62f1d98c6309e32e4b97f26337c4538b8ed6e4b750bd399fc61d925768ce0e4f1b451f3542c40d9d3c92f69afc9807e972d7a177a2611137d2b5bfdd31757d6
|
data/bin/kielce
ADDED
data/lib/kielce.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
require "kielce/helpers"
|
4
|
+
require "kielce/kielce_data"
|
5
|
+
require "kielce/kielce_loader"
|
6
|
+
require "kielce/kielce"
|
7
|
+
|
8
|
+
##############################################################################################
|
9
|
+
#
|
10
|
+
# Main (the run method)
|
11
|
+
#
|
12
|
+
# (c) 2020 Zachary Kurmas
|
13
|
+
#
|
14
|
+
##############################################################################################
|
15
|
+
|
16
|
+
module Kielce
|
17
|
+
VERSION = "2.0.0"
|
18
|
+
|
19
|
+
def self.run
|
20
|
+
|
21
|
+
# TODO:
|
22
|
+
#idea: If 1 param: Assume input and stdout
|
23
|
+
# If 2 params and 2nd param is file, assume input and output
|
24
|
+
# If 2+ params and 2nd param is dir, then process all files. Place in output dir. remove.erb from filename.
|
25
|
+
|
26
|
+
options = {
|
27
|
+
quiet: false,
|
28
|
+
}
|
29
|
+
|
30
|
+
OptionParser.new do |opts|
|
31
|
+
opts.banner = "Usage: kielce [options]"
|
32
|
+
|
33
|
+
opts.on("-q", "--[no-]quiet", "Run quietly") do |q|
|
34
|
+
options[:quiet] = q
|
35
|
+
end
|
36
|
+
end.parse!
|
37
|
+
|
38
|
+
$stderr.puts "KielceRB (version #{VERSION})" unless options[:quiet]
|
39
|
+
|
40
|
+
if ARGV.length == 0
|
41
|
+
$stderr.puts "Usage: kielce file_to_process"
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Get to work
|
47
|
+
#
|
48
|
+
|
49
|
+
file = ARGV[0]
|
50
|
+
$stderr.puts "Processing #{file}" unless options[:quiet]
|
51
|
+
|
52
|
+
# We use global variables $d and $k to make the data and +Kielce+ objects available to the
|
53
|
+
# ERB template engine. There was a bit of debate about this decision. The most popular
|
54
|
+
# alternative was to create a class named +DataContext+ with +data+ and +kielce+ accessors.
|
55
|
+
#
|
56
|
+
# Advantages to using global variables:
|
57
|
+
# (1) The resulting names were short (two characters) and eye-catching (because the first
|
58
|
+
# character is '$')
|
59
|
+
# (2) It keeps the design simpler by eliminating the need for an additional +DataContext+ class.
|
60
|
+
# (3) Users can create a custom class to use as a context without worring about conforiming to
|
61
|
+
# any specific interface
|
62
|
+
#
|
63
|
+
# Advantages to using a special +DataContext+ class:
|
64
|
+
# (1) Avoids polluting the global namespace.
|
65
|
+
|
66
|
+
begin
|
67
|
+
context = Object.new
|
68
|
+
$d = KielceLoader.load(file, context: context)
|
69
|
+
$k = Kielce.new(context)
|
70
|
+
result = $k.render(file)
|
71
|
+
puts result if $k.error_count == 0
|
72
|
+
rescue LoadingError => e
|
73
|
+
$stderr.puts e.message
|
74
|
+
exit 1
|
75
|
+
rescue IncompleteRenderError => e2
|
76
|
+
part2 = e2.source_file.nil? ? "" : "(included from #{e2.source_file})"
|
77
|
+
$stderr.puts "ERROR: Unable to read #{e2.file_name} #{part2}"
|
78
|
+
$stderr.puts "\t(#{e2.source_exception})"
|
79
|
+
exit 1
|
80
|
+
end
|
81
|
+
exit $k.error_count == 0 ? 0 : 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# For now, we just require all plugins here. Should we ever get to the point where there
|
86
|
+
# are enough plugins to cause a performance issue, we can do something more clever.
|
87
|
+
# require_relative 'kielce_plugins/schedule'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
##############################################################################################
|
2
|
+
#
|
3
|
+
# Helpers
|
4
|
+
#
|
5
|
+
# (c) 2020 Zachary Kurmas
|
6
|
+
#
|
7
|
+
##############################################################################################
|
8
|
+
|
9
|
+
class ::String
|
10
|
+
def link(text = nil)
|
11
|
+
$k.link(self, text)
|
12
|
+
#%Q(<a href="#{self}">#{text}</a>)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ::Hash
|
17
|
+
# Merges two hashes recursively. Specificaly, if the value of one item in the hash is itself a hash,
|
18
|
+
# merge that nested hash.
|
19
|
+
#
|
20
|
+
# Hash#merge by default returns a new Hash containing the key/value pairs of both hashes.
|
21
|
+
# If a key appears in both "self" and "second", the value in "second" takes precedence.
|
22
|
+
# Alternately, you can specify a block to be called if a key appears in both Hashes.
|
23
|
+
# The "merger" lambda below merges two nested hashes instead of simply choosing the version in
|
24
|
+
# "second"
|
25
|
+
def deep_merge(second)
|
26
|
+
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
27
|
+
self.merge(second, &merger)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
##############################################################################################
|
2
|
+
#
|
3
|
+
# Kielce
|
4
|
+
#
|
5
|
+
# Convenience methods available within the ERB templates.
|
6
|
+
#
|
7
|
+
# (c) 2020 Zachary Kurmas
|
8
|
+
#
|
9
|
+
##############################################################################################
|
10
|
+
|
11
|
+
require "erb"
|
12
|
+
|
13
|
+
module Kielce
|
14
|
+
class IncompleteRenderError < StandardError
|
15
|
+
attr_accessor :file_name, :source_file, :source_exception
|
16
|
+
|
17
|
+
def initialize(file_to_load, source, ex)
|
18
|
+
@file_name = file_to_load
|
19
|
+
@source_file = source
|
20
|
+
@source_exception = ex
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Kielce
|
25
|
+
attr_reader :error_count
|
26
|
+
@@render_count = 0
|
27
|
+
|
28
|
+
# Constructor
|
29
|
+
#
|
30
|
+
# @param context default context to use when calling +render+
|
31
|
+
def initialize(context)
|
32
|
+
@data_context = context
|
33
|
+
@error_count = 0
|
34
|
+
@file_stack = []
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generate an +href+ tag.
|
38
|
+
#
|
39
|
+
# This could be a class method. We make it
|
40
|
+
# an instance method so it can be used using the "$k.link" syntax.
|
41
|
+
def link(url, text_param = nil, code: nil, classes: nil)
|
42
|
+
class_list = classes.nil? ? "" : " class='#{classes}'"
|
43
|
+
|
44
|
+
# Make the text the same as the URL if no text given
|
45
|
+
text = text_param.nil? ? url : text_param
|
46
|
+
|
47
|
+
# use <code> if either (1) user requests it, or (2) no text_param given and no code param given
|
48
|
+
text = "<code>#{text}</code>" if code || (text_param.nil? && code.nil?)
|
49
|
+
|
50
|
+
"<a href='#{url}'#{class_list}>#{text}</a>"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Load +file+ and run ERB. The binding parameter determines
|
54
|
+
# the context the .erb code runs in. The context determines
|
55
|
+
# the variables and methods available. By default, kielce
|
56
|
+
# uses the same context for loading and rendering. However,
|
57
|
+
# users can override this behavior by providing different contexts
|
58
|
+
#
|
59
|
+
#
|
60
|
+
# @param the file
|
61
|
+
# @param b the binding that the template code runs in.
|
62
|
+
# @return a +String+ containing the rendered contents
|
63
|
+
def render(file, local_variables = {}, b = @data_context.instance_exec { binding })
|
64
|
+
|
65
|
+
local_variables.each_pair do |key, value|
|
66
|
+
b.local_variable_set(key, value)
|
67
|
+
end
|
68
|
+
|
69
|
+
# $stderr.puts "In render: #{b.inspect}"
|
70
|
+
result = "<!--- ERROR -->"
|
71
|
+
|
72
|
+
begin
|
73
|
+
content = File.read(file)
|
74
|
+
rescue Errno::ENOENT => e
|
75
|
+
# TODO Consider using e.backtrace_locations instead of @file_stack
|
76
|
+
# (Question: What exaclty do you want to see if render_relative is called
|
77
|
+
# by a method)
|
78
|
+
raise IncompleteRenderError.new(file, @file_stack.last, e)
|
79
|
+
end
|
80
|
+
@file_stack.push(file)
|
81
|
+
|
82
|
+
# The two nil parameters below are legacy settings that don't
|
83
|
+
# apply to Kielce. nil is the default value. We must specify
|
84
|
+
# nil, so we can set the fourth parameter (described below).
|
85
|
+
#
|
86
|
+
# It is possible for code inside an erb file to load and render
|
87
|
+
# another erb template. In order for such nested calls to work
|
88
|
+
# properly, each call must have a unique variable in which to
|
89
|
+
# store its output. This parameter is called "eoutvar". (If you
|
90
|
+
# don't specifiy eoutvar and make a nested call, the output
|
91
|
+
# can get messed up.)
|
92
|
+
@@render_count += 1
|
93
|
+
|
94
|
+
begin
|
95
|
+
erb = ERB.new(content, nil, nil, "render_out_#{@@render_count}")
|
96
|
+
erb.filename = file.to_s
|
97
|
+
result = erb.result(b)
|
98
|
+
rescue NoKeyError => e
|
99
|
+
line_num = e.backtrace_locations.select { |i| i.path == file }.first.lineno
|
100
|
+
$stderr.puts "Unrecognized key #{e.name} at #{file}:#{line_num}"
|
101
|
+
@error_count += 1
|
102
|
+
ensure
|
103
|
+
@file_stack.pop
|
104
|
+
end
|
105
|
+
result
|
106
|
+
end
|
107
|
+
|
108
|
+
def render_relative(file, local_variables = {}, b = @data_context.instance_exec { binding })
|
109
|
+
path = Pathname.new(File.absolute_path(@file_stack.last)).dirname
|
110
|
+
render(path.join(file), local_variables, b)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
##############################################################################################
|
2
|
+
#
|
3
|
+
# KielceData
|
4
|
+
#
|
5
|
+
# An object representing the data loaded from the various Kielce data files.
|
6
|
+
# The files return a Hash: a set of key-value pairs.
|
7
|
+
# +KielceData+ allows users to access the data using the more convenient method syntax (i.e.,
|
8
|
+
# $d.itemName instead of $d[:itemName] or $d['itemName])
|
9
|
+
# +KielceData+ is a child of +BasicObject+ instead of +Object+ so that there aren't conflicts
|
10
|
+
# with methods defined on +Object+ and +Kernel+ (+freeze+, +method+, +trust+, etc. )
|
11
|
+
#
|
12
|
+
# For convenience, +KielceData+ does provide a few key methods including +is_a+ and +inspect+
|
13
|
+
#
|
14
|
+
# (c) 2020 Zachary Kurmas
|
15
|
+
#
|
16
|
+
##############################################################################################
|
17
|
+
|
18
|
+
module Kielce
|
19
|
+
class NoKeyError < ::NameError
|
20
|
+
end
|
21
|
+
|
22
|
+
class InvalidKeyError < ::NameError
|
23
|
+
end
|
24
|
+
|
25
|
+
INVALID_KEYS = [:root, :method_missing, :inspect]
|
26
|
+
|
27
|
+
# Access KielceData instance variables.
|
28
|
+
# (We don't want public accessors on the KielceData object because the name
|
29
|
+
# we choose may potentially conflict with user data.)
|
30
|
+
class KielceDataAnalyzer
|
31
|
+
class << self
|
32
|
+
def root(obj)
|
33
|
+
obj.instance_eval { @xx_kielce_root }
|
34
|
+
end
|
35
|
+
|
36
|
+
def name(obj)
|
37
|
+
obj.instance_eval { @xx_kielce_obj_name }
|
38
|
+
end
|
39
|
+
|
40
|
+
def data(obj)
|
41
|
+
obj.instance_eval { @xx_kielce_data }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# By extending BasicObject instead of Object, we don't have to worry
|
47
|
+
# about user data conflicting with method names in Object or Kernel
|
48
|
+
#
|
49
|
+
# @xx_kielce_data is the hash passed to the constructor.
|
50
|
+
# @xx_kielce_obj_name is the key that maps to the value.
|
51
|
+
# @xx_kielce_root refers to the root object. It is used so that lambdas have access to
|
52
|
+
# the data.
|
53
|
+
#
|
54
|
+
# (The strange "xx_kielce_" naming is to avoid conflicts with user-chosen names)
|
55
|
+
class KielceData < BasicObject
|
56
|
+
@@error_output = $stderr
|
57
|
+
|
58
|
+
def self.error_output
|
59
|
+
@@error_output
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.error_output=(val)
|
63
|
+
@@error_output=val
|
64
|
+
end
|
65
|
+
|
66
|
+
def initialize(data, root = nil, obj_name = nil)
|
67
|
+
|
68
|
+
INVALID_KEYS.each do |key|
|
69
|
+
::Kernel.send(:raise, InvalidKeyError.new("Invalid Key: #{key} may not be used as a key.", key)) if data.has_key?(key)
|
70
|
+
end
|
71
|
+
|
72
|
+
@xx_kielce_obj_name = obj_name.nil? ? "" : obj_name
|
73
|
+
|
74
|
+
@xx_kielce_data = data
|
75
|
+
|
76
|
+
# Remember, root may be a BasicObject and, therefore, not define .nil
|
77
|
+
@xx_kielce_root = (root == nil) ? self : root
|
78
|
+
|
79
|
+
@xx_kielce_data.each do |key, value|
|
80
|
+
if value.is_a?(::Hash)
|
81
|
+
@xx_kielce_data[key] = ::Kielce::KielceData.new(value, @xx_kielce_root, "#{@xx_kielce_obj_name}#{key}.")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def root
|
87
|
+
@xx_kielce_root
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect
|
91
|
+
"KielceData: #{@xx_kielce_obj_name} #{@xx_kielce_data.inspect}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def is_a?(klass)
|
95
|
+
klass == ::Kielce::KielceData
|
96
|
+
end
|
97
|
+
|
98
|
+
# Provides the "magic" that allows users to access data using method syntax.
|
99
|
+
# This method is called whenever a method that doesn't exist is invoked. It
|
100
|
+
# then looks through the hash for a key with the same name as the method.
|
101
|
+
def method_missing(name, *args, **keyword_args, &block)
|
102
|
+
|
103
|
+
# $stderr.puts "Processing #{name} in #{@xx_kielce_obj_name}"
|
104
|
+
|
105
|
+
# Convert the name to a symbol. (It is probably already a symbol, but the extra .to_sym won't hurt)
|
106
|
+
# Then complian if there isn't a data object by that name.
|
107
|
+
name_sym = name.to_sym
|
108
|
+
full_name = "#{@xx_kielce_obj_name}#{name}"
|
109
|
+
unless @xx_kielce_data.has_key?(name_sym)
|
110
|
+
# The first ("message") parameter is currently unused. The message can be changed, if desired.
|
111
|
+
::Kernel.send(:raise, NoKeyError.new("Unrecognized Key: #{full_name}", full_name))
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get the requested data object.
|
115
|
+
# If the object is a lambda, execute the lambda.
|
116
|
+
# Otherwise, just return it.
|
117
|
+
item = @xx_kielce_data[name_sym]
|
118
|
+
if item.is_a?(::Proc)
|
119
|
+
if item.parameters.any? { |i| i.last == :root }
|
120
|
+
@@error_output.puts 'WARNING! Lambda parameter named root shadows instance method root.'
|
121
|
+
end
|
122
|
+
|
123
|
+
#$stderr.puts item.parameters.inspect
|
124
|
+
keyword_params = item.parameters.select { |i| i.first == :keyreq || i.first == :key }
|
125
|
+
num_keyword = keyword_params.size
|
126
|
+
num_args = item.parameters.size - num_keyword
|
127
|
+
|
128
|
+
#$stderr.puts "-----------"
|
129
|
+
#$stderr.puts args.inspect
|
130
|
+
#$stderr.puts keyword_args.inspect
|
131
|
+
#$stderr.puts "Num each: #{num_args} #{num_keyword}"
|
132
|
+
|
133
|
+
if num_args == 0 && num_keyword == 0
|
134
|
+
return instance_exec(&item)
|
135
|
+
elsif num_args > 0 && num_keyword == 0
|
136
|
+
return instance_exec(*args, &item)
|
137
|
+
elsif num_keyword > 0
|
138
|
+
return instance_exec(*args, **keyword_args, &item)
|
139
|
+
else
|
140
|
+
$stderr.puts "FAIL. Shouldn't get here!"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
if args.length != 0 || keyword_args.size != 0
|
145
|
+
@@error_output.puts "WARNING! #{full_name} is not a function and doesn't expect parameters."
|
146
|
+
end
|
147
|
+
|
148
|
+
@xx_kielce_data[name_sym]
|
149
|
+
end
|
150
|
+
end # class
|
151
|
+
end # module
|
@@ -0,0 +1,144 @@
|
|
1
|
+
##############################################################################################
|
2
|
+
#
|
3
|
+
# KielceLoader
|
4
|
+
#
|
5
|
+
# Methods to find and load Kielce data files.
|
6
|
+
#
|
7
|
+
# (c) 2020 Zachary Kurmas
|
8
|
+
#
|
9
|
+
##############################################################################################
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
|
13
|
+
module Kielce
|
14
|
+
|
15
|
+
class LoadingError < StandardError
|
16
|
+
end
|
17
|
+
|
18
|
+
class KielceLoader
|
19
|
+
|
20
|
+
# Load the kielce_data_*.rb files beginning from the directory that contains +start+.
|
21
|
+
#
|
22
|
+
# @param start the name of the file in the directory where the search for data files should begin
|
23
|
+
# @param current the current data object (which may contain data loaded from other files)
|
24
|
+
# @param context the context in which the data files will be executed.
|
25
|
+
# @return a +KielceData+ object containing the collective merged data from all kielce data files.
|
26
|
+
#
|
27
|
+
# Notice that +start+ need not be either a directory or a data file. Instead we use
|
28
|
+
# +start+'s full pathname to identify the directory where we will begin our search.
|
29
|
+
# We allow +start+ to be any file because, in most cases, it is simply the .erb file
|
30
|
+
# being processed -- thus, it's easiest/cleaner for the user to pass this filename
|
31
|
+
# as a parameter rather than convert it to a starting directory first.
|
32
|
+
#
|
33
|
+
def self.load(start, current: {}, context: Object.new, stop_dir: nil)
|
34
|
+
# File.absolute_path returns a String. We need a full Pathname object.
|
35
|
+
stop_path = nil
|
36
|
+
stop_path = Pathname.new(File.absolute_path(stop_dir)) unless stop_dir.nil?
|
37
|
+
start_path = Pathname.new(File.absolute_path(start))
|
38
|
+
start_path = start_path.parent unless start_path.directory?
|
39
|
+
KielceData.new(load_directory_raw(start_path, current: current, context: context, stop_dir: stop_path))
|
40
|
+
end
|
41
|
+
|
42
|
+
# loads a kielce_data_*.rb file and returns the file's raw data (the plain Ruby Hash)
|
43
|
+
#
|
44
|
+
# @param file the file to load
|
45
|
+
# @param current the current data object (which may contain data loaded from other files)
|
46
|
+
# @param context the context in which to execute the data file's code
|
47
|
+
# @return the updated data object.
|
48
|
+
#
|
49
|
+
def self.load_file_raw(file, current: {}, context: Object.new)
|
50
|
+
#$stderr.puts "Processing #{file}"
|
51
|
+
|
52
|
+
# (1) Kielce data files are assumed to return a single Hash.
|
53
|
+
# (2) The second parameter to eval, the "binding", is an object
|
54
|
+
# describing the context the code being evaluated will run in.
|
55
|
+
# Among other things, this context determines the local variables
|
56
|
+
# and methods avaiable to the code being evaluated. In order to
|
57
|
+
# prevent data files from manipuating this KielceLoader object this
|
58
|
+
# method's local variables, we create an empty object and use its
|
59
|
+
# binding. Note, however, that the code can still read and set global
|
60
|
+
# variables. Users can also provide a different, custom, context object.
|
61
|
+
# (3) To get the correct binding, we need to run the call to +binding+
|
62
|
+
# in the context of an instance method on the context object.
|
63
|
+
# The "obvious" way to do this is to add a method to the object
|
64
|
+
# that calls and returns binding. We decided not to do this for
|
65
|
+
# two reasons: (a) We didn't want to add any methods to the context object
|
66
|
+
# that may possibly conflict with the names chosen by users in the data
|
67
|
+
# files, and (b) Object is the default object used for context; but
|
68
|
+
# in theory, users of the library could chose a different object. We didn't want
|
69
|
+
# our chosen name to conflict with a method that may already exist on the user's
|
70
|
+
# chose context object.
|
71
|
+
# (4) The third parameter, +file+, allows Ruby to identify the source of any
|
72
|
+
# errors encountered during the evaluation process.
|
73
|
+
b = context.instance_eval { binding }
|
74
|
+
data = eval File.read(file), b, file
|
75
|
+
#$stderr.puts "Current is #{current}"
|
76
|
+
#$stderr.puts "Data is #{data}"
|
77
|
+
|
78
|
+
# If the file is empty, or has a nil return, just use an empty hash
|
79
|
+
data = {} if data.nil?
|
80
|
+
|
81
|
+
unless data.is_a?(Hash)
|
82
|
+
raise LoadingError, "ERROR: Data file #{file} did not return a Hash. It returned #{data.inspect}."
|
83
|
+
# $stderr.puts "ERROR: Datafile #{file} did not return a Hash. It returned \"#{data.inspect}\"."
|
84
|
+
exit 0
|
85
|
+
end
|
86
|
+
|
87
|
+
# These INVALID_KEYS correspond to methods on the KielceData object.
|
88
|
+
# These keys would be shadowed by these methods and produce strange errors.
|
89
|
+
# From a design perspective, I would prefer to do this check inside the KielceData class; but,
|
90
|
+
# at that point it's no longer possible to determine which file contained the invalid key.
|
91
|
+
# Thus, but repeating the check here, we can provide a better error message to the user.
|
92
|
+
INVALID_KEYS.each do |key|
|
93
|
+
if data.has_key?(key)
|
94
|
+
raise LoadingError, "ERROR: Data file #{file} uses the key \"#{key}\", which is not allowed."
|
95
|
+
exit 0
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# The deep_merge method will use the data from +data+ in the event of duplicate keys.
|
101
|
+
# helpers.rb opens the Hash class and adds this method.
|
102
|
+
x = current.deep_merge(data)
|
103
|
+
#$stderr.puts "Post-merge is #{x}"
|
104
|
+
x
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Search +dir+ and all parent directories for kielce data files, load them, and
|
109
|
+
# return the raw Hash containing the collective datqa.
|
110
|
+
#
|
111
|
+
# @param dir the directory to search (as a +Pathname+ object)
|
112
|
+
# @param current the current data object
|
113
|
+
# @param context the context in which to execute the data files' code
|
114
|
+
# @pram stop_dir the last (highest-level) directory to examine (as a +Pathname+ object)
|
115
|
+
# @return the updated "raw" data object (i.e., the raw Hash)
|
116
|
+
def self.load_directory_raw(dir, current: {}, context: Object.new, stop_dir: nil)
|
117
|
+
|
118
|
+
# $stderr.puts "Done yet? #{File.absolute_path(dir)} #{File.absolute_path(stop_dir)}"
|
119
|
+
|
120
|
+
# recurse until you get to either the "stop directory" or the root of the filesystem.
|
121
|
+
# beginning from the root of the file system means that entries "closer" to the
|
122
|
+
# file being processed replace entries higher up in the file system.
|
123
|
+
# unless File.absolute_path(dir) == File.absolute_path(stop_dir) || dir.root?
|
124
|
+
unless (!stop_dir.nil? && (dir.realpath == stop_dir.realpath)) || dir.realpath.root?
|
125
|
+
current = load_directory_raw(dir.parent, current: current, context: context, stop_dir: stop_dir)
|
126
|
+
end
|
127
|
+
|
128
|
+
# By default, process all files fitting the pattern kielce_data*.rb
|
129
|
+
# In addition, look in directories named KielceData for files matching the pattern.
|
130
|
+
# (The use of KielceData directories allows users to gather multiple data files up and
|
131
|
+
# place them "out of sight")
|
132
|
+
# +dir+ is a +pathname+ object. By placing it in a string interpolation, it is coverted to
|
133
|
+
# a +String+ using +Pathname#to_s+.
|
134
|
+
# On occasion, +dir+ may actually be regular file instead of a directory. In such cases, the
|
135
|
+
# glob below will simply fail to match anything.
|
136
|
+
# QZ001
|
137
|
+
data = Dir.glob("#{dir}/KielceData/kielce_data*.rb") + Dir.glob("#{dir}/kielce_data*.rb")
|
138
|
+
data.each do |file|
|
139
|
+
current = load_file_raw(file, current: current, context: context)
|
140
|
+
end
|
141
|
+
current
|
142
|
+
end
|
143
|
+
end # class
|
144
|
+
end # module
|
@@ -0,0 +1,320 @@
|
|
1
|
+
require 'rubyXL'
|
2
|
+
require_relative 'assignment'
|
3
|
+
|
4
|
+
# https://www.ablebits.com/office-addins-blog/2015/03/11/change-date-format-excel/
|
5
|
+
|
6
|
+
module KielcePlugins
|
7
|
+
module Schedule
|
8
|
+
|
9
|
+
class Schedule
|
10
|
+
|
11
|
+
#SCHEDULE_KEYS = [:week, :date, :topics, :notes, :reading, :milestones, :comments]
|
12
|
+
SCHEDULE_KEYS = [:week, :date, :topics, :reading, :milestones, :comments]
|
13
|
+
|
14
|
+
attr_accessor :assignments, :schedule_days
|
15
|
+
|
16
|
+
def initialize(filename)
|
17
|
+
workbook = RubyXL::Parser.parse(filename)
|
18
|
+
@assignments = build_assignments(build_rows(workbook['Assignments'], Assignment::ASSIGNMENT_KEYS))
|
19
|
+
@schedule_days = build_schedule_days(build_rows(workbook['Schedule'], SCHEDULE_KEYS))
|
20
|
+
end
|
21
|
+
|
22
|
+
def transform(value)
|
23
|
+
if value.is_a? String
|
24
|
+
# Replace link markup
|
25
|
+
value.gsub!(/\[\[([^\s]+)(\s+(\S.*)|\s*)\]\]/) do
|
26
|
+
text = $3.nil? ? "<code>#{$1}</code>" : $3
|
27
|
+
"<a href='#{$1}'>#{text}</a>"
|
28
|
+
end
|
29
|
+
|
30
|
+
value.gsub!(/<<(assign|due)\s+(\S.+)>>/) do
|
31
|
+
#$stderr.puts "Found assignment ref #{$1} --- #{$2} -- #{@assignments[$2].inspect}"
|
32
|
+
$stderr.puts "Assignment #{$2} not found" unless @assignments.has_key?($2)
|
33
|
+
|
34
|
+
text = @assignments[$2].title(:full, true)
|
35
|
+
if $1 == 'assign'
|
36
|
+
"Assign #{text}"
|
37
|
+
elsif $1 == 'due'
|
38
|
+
"<b>Due</b> #{text}"
|
39
|
+
else
|
40
|
+
$stderr.puts "Unexpected match #{$1}"
|
41
|
+
end
|
42
|
+
end # end gsub!
|
43
|
+
|
44
|
+
value.gsub!(/{{([^{}:]+):\s*([^{}]+)}}/) do
|
45
|
+
text = $1
|
46
|
+
link_rule = $2
|
47
|
+
|
48
|
+
if (link_rule =~ /(.*)\!(.+)/)
|
49
|
+
method = $1
|
50
|
+
param = $2
|
51
|
+
|
52
|
+
method = 'default' if method.empty?
|
53
|
+
#$stderr.puts "Found link rule =>#{method}<= #{method.empty?} =>#{param}<="
|
54
|
+
|
55
|
+
link = $d.course.notesTemplates.method_missing(method, param)
|
56
|
+
else
|
57
|
+
link = link_rule
|
58
|
+
end
|
59
|
+
"(<a href='#{link}'>#{text}</a>)"
|
60
|
+
end # end gsub
|
61
|
+
|
62
|
+
end # end if value is string
|
63
|
+
value
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_rows(worksheet, keys)
|
67
|
+
# Remove the first (header) row, and any empty rows.
|
68
|
+
# Also, remove any rows after "END"
|
69
|
+
first = true
|
70
|
+
done = false
|
71
|
+
rows = []
|
72
|
+
worksheet.each do |row|
|
73
|
+
done = true if !row.nil? && !row[0].nil? && row[0].value == "END"
|
74
|
+
|
75
|
+
unless first || row.nil? || done
|
76
|
+
rows << row
|
77
|
+
end
|
78
|
+
first = false
|
79
|
+
end
|
80
|
+
|
81
|
+
# For each row, build a Hash describing the row.
|
82
|
+
rows.map do |row|
|
83
|
+
row_hash = { original: {} }
|
84
|
+
keys.each_with_index do |item, index|
|
85
|
+
row_hash[:original][item] = row[index].nil? ? nil : row[index].value
|
86
|
+
row_hash[item] = row[index].nil? ? nil : transform(row[index].value)
|
87
|
+
end
|
88
|
+
row_hash
|
89
|
+
end # end map
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_assignments(rows)
|
93
|
+
assignment_hash = {}
|
94
|
+
rows.each { |row| assignment_hash[row[:id]] = Assignment.new(row) }
|
95
|
+
assignment_hash
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_schedule_days(schedule_rows)
|
99
|
+
array_keys = SCHEDULE_KEYS.slice(2, SCHEDULE_KEYS.length - 2)
|
100
|
+
current_week = nil
|
101
|
+
schedule_days = []
|
102
|
+
schedule_day = nil
|
103
|
+
|
104
|
+
schedule_rows.each do |row|
|
105
|
+
|
106
|
+
# if there is a date, start a new day
|
107
|
+
unless row[:date].nil?
|
108
|
+
|
109
|
+
# push day in progress into array
|
110
|
+
schedule_days << schedule_day unless schedule_day.nil?
|
111
|
+
|
112
|
+
# create a new schedule_day Hash
|
113
|
+
schedule_day = {
|
114
|
+
begin_week: false
|
115
|
+
}
|
116
|
+
|
117
|
+
unless row[:week].nil?
|
118
|
+
current_week = row[:week]
|
119
|
+
schedule_day[:begin_week] = true
|
120
|
+
end
|
121
|
+
|
122
|
+
schedule_day[:week] = current_week
|
123
|
+
schedule_day[:date] = row[:date]
|
124
|
+
array_keys.each { |key| schedule_day[key] = [] }
|
125
|
+
end
|
126
|
+
|
127
|
+
# push non-nil values onto the corresponding array
|
128
|
+
# array_keys.each { |key| schedule_day[key] << row[key] unless row[key].nil? }
|
129
|
+
array_keys.each do |key|
|
130
|
+
val = row[key]
|
131
|
+
|
132
|
+
# skip any completely empty cells (they produce a value of nil)
|
133
|
+
# Replace a single period with a whitespace. (Thus producing an empty cell in the table)
|
134
|
+
# Similarly, treat cells beginnign with // as a comment and produce an empty cell in the table)
|
135
|
+
unless row[key].nil?
|
136
|
+
val = " " if val == '.' || val =~ /^\s*\/\//
|
137
|
+
schedule_day[key] << val
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Look for assignments / due dates and add information to @assignment objects
|
142
|
+
original_milestones = row[:original][:milestones]
|
143
|
+
if !original_milestones.nil? && original_milestones =~ /<<due\s+(.*)>>/
|
144
|
+
#$stderr.puts "Assignment #{$1} has due date of #{schedule_day[:date].strftime("%a. %-d %b.")}"
|
145
|
+
@assignments[$1].due = schedule_day[:date]
|
146
|
+
end
|
147
|
+
if !original_milestones.nil? && original_milestones =~ /<<assign\s+(.*)>>/
|
148
|
+
#$stderr.puts "Assignment #{$1} has assignment date of #{schedule_day[:date].strftime("%a. %-d %b.")}"
|
149
|
+
unless @assignments.has_key?($1)
|
150
|
+
$stderr.puts "Key #{$1} not found in #{@assignments.keys.inspect}"
|
151
|
+
end
|
152
|
+
@assignments[$1].assigned = schedule_day[:date]
|
153
|
+
end
|
154
|
+
end # end each row
|
155
|
+
|
156
|
+
schedule_days << schedule_day unless schedule_day.nil?
|
157
|
+
|
158
|
+
schedule_days
|
159
|
+
end
|
160
|
+
|
161
|
+
def timeline_table
|
162
|
+
table = []
|
163
|
+
table << <<TABLE
|
164
|
+
<table class='kielceSchedule'>
|
165
|
+
<tr>
|
166
|
+
<th>Week</th>
|
167
|
+
<th>Date</th>
|
168
|
+
<th>Topics</th>
|
169
|
+
<!-- <th>Notes</th> -->
|
170
|
+
<th>Reading</th>
|
171
|
+
<th>Milestones</th>
|
172
|
+
</tr>
|
173
|
+
TABLE
|
174
|
+
|
175
|
+
first = true
|
176
|
+
@schedule_days.each do |schedule_day|
|
177
|
+
table << '<tr>'
|
178
|
+
|
179
|
+
if schedule_day[:begin_week]
|
180
|
+
unless first
|
181
|
+
# Add a blank row of horizontal lines
|
182
|
+
table << '<td></td><td></td><td></td><td></td><td></td><td></td></tr>'
|
183
|
+
table << "<tr class='week_end'><td></td><td></td><td></td><td></td><td></td><td></td></tr>"
|
184
|
+
table << '<tr>'
|
185
|
+
end
|
186
|
+
first = false
|
187
|
+
week_value = schedule_day[:week]
|
188
|
+
else
|
189
|
+
week_value = ''
|
190
|
+
end
|
191
|
+
|
192
|
+
table << " <td class='week_column'>#{week_value}</td>"
|
193
|
+
formatted_date = schedule_day[:date].strftime("%a. %-d %b.")
|
194
|
+
table << " <td class='date_column'>#{formatted_date}</td>"
|
195
|
+
table << " <td class='topics_column'>#{schedule_day[:topics].join("<br>")}</td>"
|
196
|
+
# table << " <td class='topics_column'>#{schedule_day[:notes].join("<br>")}</td>"
|
197
|
+
table << " <td class='reading_column'>#{schedule_day[:reading].join("<br>")}</td>"
|
198
|
+
table << " <td class='milestones_column'>#{schedule_day[:milestones].join("<br>")}</td>"
|
199
|
+
table << "</tr>"
|
200
|
+
end
|
201
|
+
table << "</table>"
|
202
|
+
table.join("\n")
|
203
|
+
end
|
204
|
+
|
205
|
+
def timeline_style
|
206
|
+
<<STYLE
|
207
|
+
table.kielceSchedule {
|
208
|
+
border-collapse: separate;
|
209
|
+
border-spacing: 2px;
|
210
|
+
}
|
211
|
+
|
212
|
+
.kielceSchedule tr th {
|
213
|
+
text-align: left;
|
214
|
+
}
|
215
|
+
|
216
|
+
.kielceSchedule tr td {
|
217
|
+
vertical-align: top;
|
218
|
+
padding-right: 10px;
|
219
|
+
}
|
220
|
+
|
221
|
+
.week_column, .date_column {
|
222
|
+
white-space: nowrap;
|
223
|
+
}
|
224
|
+
|
225
|
+
.kielceSchedule tr th, .date_column, .topics_column, .notes_column, .reading_column, .milestones_column, .week_end td {
|
226
|
+
border-bottom: 1px solid;
|
227
|
+
}
|
228
|
+
STYLE
|
229
|
+
end
|
230
|
+
|
231
|
+
def timeline_page
|
232
|
+
<<PAGE
|
233
|
+
<html>
|
234
|
+
<head>
|
235
|
+
<style>
|
236
|
+
#{timeline_style}
|
237
|
+
</style>
|
238
|
+
</head>
|
239
|
+
<body>
|
240
|
+
#{timeline_table}
|
241
|
+
</body>
|
242
|
+
</html>
|
243
|
+
PAGE
|
244
|
+
end
|
245
|
+
|
246
|
+
def assignment_style
|
247
|
+
<<STYLE
|
248
|
+
.kielceAssignmentTable {
|
249
|
+
border-spacing: 35px 0;
|
250
|
+
}
|
251
|
+
|
252
|
+
.kielceAssignmentTable tr th {
|
253
|
+
text-align: left;
|
254
|
+
}
|
255
|
+
|
256
|
+
.kielceAssignmentTable tr td {
|
257
|
+
vertical-align: top;
|
258
|
+
}
|
259
|
+
|
260
|
+
.kielceAssignmentTable_due {
|
261
|
+
white-space: nowrap;
|
262
|
+
}
|
263
|
+
|
264
|
+
.kielceAssignmentTable_title {
|
265
|
+
white-space: nowrap;
|
266
|
+
}
|
267
|
+
|
268
|
+
.exam {
|
269
|
+
background-color: lightgreen;
|
270
|
+
}
|
271
|
+
STYLE
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
def assignment_list
|
276
|
+
list = <<TABLE
|
277
|
+
<table class='kielceAssignmentTable'>
|
278
|
+
<tr>
|
279
|
+
<th>Due</th>
|
280
|
+
<th>Name</th>
|
281
|
+
<th>Details</th>
|
282
|
+
</tr>
|
283
|
+
TABLE
|
284
|
+
|
285
|
+
by_date = @assignments.values.reject { |item| item.due.nil? || item.type == 'Lab' }.sort_by { |a| a.due }
|
286
|
+
by_date.each do |assignment|
|
287
|
+
list += ' <tr>'
|
288
|
+
list += " <td class='kielceAssignmentTable_due'>#{assignment.due.strftime("%a. %-d %b.")}</td>\n"
|
289
|
+
list += " <td class='kielceAssignmentTable_title'>#{assignment.title(:full, true)}</td>\n"
|
290
|
+
list += " <td class='kielceAssignmentTable_details'>#{assignment.details}</td>\n"
|
291
|
+
list += " </tr>\n\n"
|
292
|
+
end
|
293
|
+
list += "</table>\n"
|
294
|
+
end
|
295
|
+
|
296
|
+
def lab_list
|
297
|
+
list = <<TABLE
|
298
|
+
<table class='kielceAssignmentTable'>
|
299
|
+
<tr>
|
300
|
+
<th>Date</th>
|
301
|
+
<th>Name</th>
|
302
|
+
<th>Details</th>
|
303
|
+
</tr>
|
304
|
+
TABLE
|
305
|
+
assigned_labs = @assignments.values.select { |item| item.type == 'Lab' && !item.assigned.nil? }
|
306
|
+
by_date = assigned_labs.sort { |a, b| a.assigned <=> b.assigned }
|
307
|
+
by_date.each do |assignment|
|
308
|
+
list += ' <tr>'
|
309
|
+
list += " <td class='kielceAssignmentTable_due'>#{assignment.assigned.strftime("%a. %-d %b.")}</td>\n"
|
310
|
+
list += " <td class='kielceAssignmentTable_title'>#{assignment.title(:full, true)}</td>\n"
|
311
|
+
list += " <td class='kielceAssignmentTable_details'>#{assignment.details}</td>\n"
|
312
|
+
list += " </tr>\n\n"
|
313
|
+
end
|
314
|
+
list += "</table>\n"
|
315
|
+
list
|
316
|
+
end
|
317
|
+
end # end Schedule
|
318
|
+
end # module
|
319
|
+
end # end KielcePlugins
|
320
|
+
#puts Schedule.new(ARGV[0]).timeline_page
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module KielcePlugins
|
2
|
+
module Schedule
|
3
|
+
|
4
|
+
class Assignment
|
5
|
+
|
6
|
+
ASSIGNMENT_KEYS = [:id, :title, :type, :number, :link, :details]
|
7
|
+
|
8
|
+
SHORT_TYPE = {
|
9
|
+
homework: 'HW',
|
10
|
+
project: 'P',
|
11
|
+
lab: 'L'
|
12
|
+
}
|
13
|
+
|
14
|
+
# create a getter for each key (some getters overridden below)
|
15
|
+
ASSIGNMENT_KEYS.each { |key| attr_reader key }
|
16
|
+
|
17
|
+
attr_accessor :due, :assigned
|
18
|
+
|
19
|
+
def initialize(row)
|
20
|
+
ASSIGNMENT_KEYS.each { |key| instance_variable_set "@#{key.to_s}".to_sym, row[key] }
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_type?
|
24
|
+
!@type.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def type
|
28
|
+
@type.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def short_type
|
32
|
+
return '' unless has_type?
|
33
|
+
key = @type.downcase.to_sym
|
34
|
+
$stderr.puts "Unknown type #{@type}" unless SHORT_TYPE.has_key?(key)
|
35
|
+
SHORT_TYPE[key]
|
36
|
+
end
|
37
|
+
|
38
|
+
def title(style = :original, linked=false)
|
39
|
+
case style
|
40
|
+
when :original
|
41
|
+
text = @title
|
42
|
+
when :short_type
|
43
|
+
type_num = has_type? ? "#{short_type}#{@number}: " : ''
|
44
|
+
text = "#{type_num}#{@title}"
|
45
|
+
when :full
|
46
|
+
type_string = has_type? ? "#{@type} #{@number}: " : ''
|
47
|
+
text = "#{type_string}#{@title}"
|
48
|
+
else
|
49
|
+
$stderr.puts "Unknown style #{sytle}"
|
50
|
+
text = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# build a link, if a link provided
|
54
|
+
(linked && !@link.nil?) ? "<a href='#{@link}'>#{text}</a>" : text
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,320 @@
|
|
1
|
+
require 'rubyXL'
|
2
|
+
require_relative 'assignment'
|
3
|
+
|
4
|
+
# https://www.ablebits.com/office-addins-blog/2015/03/11/change-date-format-excel/
|
5
|
+
|
6
|
+
module KielcePlugins
|
7
|
+
module Schedule
|
8
|
+
|
9
|
+
class Schedule
|
10
|
+
|
11
|
+
#SCHEDULE_KEYS = [:week, :date, :topics, :notes, :reading, :milestones, :comments]
|
12
|
+
SCHEDULE_KEYS = [:week, :date, :topics, :reading, :milestones, :comments]
|
13
|
+
|
14
|
+
attr_accessor :assignments, :schedule_days
|
15
|
+
|
16
|
+
def initialize(filename)
|
17
|
+
workbook = RubyXL::Parser.parse(filename)
|
18
|
+
@assignments = build_assignments(build_rows(workbook['Assignments'], Assignment::ASSIGNMENT_KEYS))
|
19
|
+
@schedule_days = build_schedule_days(build_rows(workbook['Schedule'], SCHEDULE_KEYS))
|
20
|
+
end
|
21
|
+
|
22
|
+
def transform(value)
|
23
|
+
if value.is_a? String
|
24
|
+
# Replace link markup
|
25
|
+
value.gsub!(/\[\[([^\s]+)(\s+(\S.*)|\s*)\]\]/) do
|
26
|
+
text = $3.nil? ? "<code>#{$1}</code>" : $3
|
27
|
+
"<a href='#{$1}'>#{text}</a>"
|
28
|
+
end
|
29
|
+
|
30
|
+
value.gsub!(/<<(assign|due)\s+(\S.+)>>/) do
|
31
|
+
#$stderr.puts "Found assignment ref #{$1} --- #{$2} -- #{@assignments[$2].inspect}"
|
32
|
+
$stderr.puts "Assignment #{$2} not found" unless @assignments.has_key?($2)
|
33
|
+
|
34
|
+
text = @assignments[$2].title(:full, true)
|
35
|
+
if $1 == 'assign'
|
36
|
+
"Assign #{text}"
|
37
|
+
elsif $1 == 'due'
|
38
|
+
"<b>Due</b> #{text}"
|
39
|
+
else
|
40
|
+
$stderr.puts "Unexpected match #{$1}"
|
41
|
+
end
|
42
|
+
end # end gsub!
|
43
|
+
|
44
|
+
value.gsub!(/{{([^{}:]+):\s*([^{}]+)}}/) do
|
45
|
+
text = $1
|
46
|
+
link_rule = $2
|
47
|
+
|
48
|
+
if (link_rule =~ /(.*)\!(.+)/)
|
49
|
+
method = $1
|
50
|
+
param = $2
|
51
|
+
|
52
|
+
method = 'default' if method.empty?
|
53
|
+
#$stderr.puts "Found link rule =>#{method}<= #{method.empty?} =>#{param}<="
|
54
|
+
|
55
|
+
link = $d.course.notesTemplates.method_missing(method, param)
|
56
|
+
else
|
57
|
+
link = link_rule
|
58
|
+
end
|
59
|
+
"(<a href='#{link}'>#{text}</a>)"
|
60
|
+
end # end gsub
|
61
|
+
|
62
|
+
end # end if value is string
|
63
|
+
value
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_rows(worksheet, keys)
|
67
|
+
# Remove the first (header) row, and any empty rows.
|
68
|
+
# Also, remove any rows after "END"
|
69
|
+
first = true
|
70
|
+
done = false
|
71
|
+
rows = []
|
72
|
+
worksheet.each do |row|
|
73
|
+
done = true if !row.nil? && !row[0].nil? && row[0].value == "END"
|
74
|
+
|
75
|
+
unless first || row.nil? || done
|
76
|
+
rows << row
|
77
|
+
end
|
78
|
+
first = false
|
79
|
+
end
|
80
|
+
|
81
|
+
# For each row, build a Hash describing the row.
|
82
|
+
rows.map do |row|
|
83
|
+
row_hash = { original: {} }
|
84
|
+
keys.each_with_index do |item, index|
|
85
|
+
row_hash[:original][item] = row[index].nil? ? nil : row[index].value
|
86
|
+
row_hash[item] = row[index].nil? ? nil : transform(row[index].value)
|
87
|
+
end
|
88
|
+
row_hash
|
89
|
+
end # end map
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_assignments(rows)
|
93
|
+
assignment_hash = {}
|
94
|
+
rows.each { |row| assignment_hash[row[:id]] = Assignment.new(row) }
|
95
|
+
assignment_hash
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_schedule_days(schedule_rows)
|
99
|
+
array_keys = SCHEDULE_KEYS.slice(2, SCHEDULE_KEYS.length - 2)
|
100
|
+
current_week = nil
|
101
|
+
schedule_days = []
|
102
|
+
schedule_day = nil
|
103
|
+
|
104
|
+
schedule_rows.each do |row|
|
105
|
+
|
106
|
+
# if there is a date, start a new day
|
107
|
+
unless row[:date].nil?
|
108
|
+
|
109
|
+
# push day in progress into array
|
110
|
+
schedule_days << schedule_day unless schedule_day.nil?
|
111
|
+
|
112
|
+
# create a new schedule_day Hash
|
113
|
+
schedule_day = {
|
114
|
+
begin_week: false
|
115
|
+
}
|
116
|
+
|
117
|
+
unless row[:week].nil?
|
118
|
+
current_week = row[:week]
|
119
|
+
schedule_day[:begin_week] = true
|
120
|
+
end
|
121
|
+
|
122
|
+
schedule_day[:week] = current_week
|
123
|
+
schedule_day[:date] = row[:date]
|
124
|
+
array_keys.each { |key| schedule_day[key] = [] }
|
125
|
+
end
|
126
|
+
|
127
|
+
# push non-nil values onto the corresponding array
|
128
|
+
# array_keys.each { |key| schedule_day[key] << row[key] unless row[key].nil? }
|
129
|
+
array_keys.each do |key|
|
130
|
+
val = row[key]
|
131
|
+
|
132
|
+
# skip any completely empty cells (they produce a value of nil)
|
133
|
+
# Replace a single period with a whitespace. (Thus producing an empty cell in the table)
|
134
|
+
# Similarly, treat cells beginnign with // as a comment and produce an empty cell in the table)
|
135
|
+
unless row[key].nil?
|
136
|
+
val = " " if val == '.' || val =~ /^\s*\/\//
|
137
|
+
schedule_day[key] << val
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Look for assignments / due dates and add information to @assignment objects
|
142
|
+
original_milestones = row[:original][:milestones]
|
143
|
+
if !original_milestones.nil? && original_milestones =~ /<<due\s+(.*)>>/
|
144
|
+
#$stderr.puts "Assignment #{$1} has due date of #{schedule_day[:date].strftime("%a. %-d %b.")}"
|
145
|
+
@assignments[$1].due = schedule_day[:date]
|
146
|
+
end
|
147
|
+
if !original_milestones.nil? && original_milestones =~ /<<assign\s+(.*)>>/
|
148
|
+
#$stderr.puts "Assignment #{$1} has assignment date of #{schedule_day[:date].strftime("%a. %-d %b.")}"
|
149
|
+
unless @assignments.has_key?($1)
|
150
|
+
$stderr.puts "Key #{$1} not found in #{@assignments.keys.inspect}"
|
151
|
+
end
|
152
|
+
@assignments[$1].assigned = schedule_day[:date]
|
153
|
+
end
|
154
|
+
end # end each row
|
155
|
+
|
156
|
+
schedule_days << schedule_day unless schedule_day.nil?
|
157
|
+
|
158
|
+
schedule_days
|
159
|
+
end
|
160
|
+
|
161
|
+
def timeline_table
|
162
|
+
table = []
|
163
|
+
table << <<TABLE
|
164
|
+
<table class='kielceSchedule'>
|
165
|
+
<tr>
|
166
|
+
<th>Week</th>
|
167
|
+
<th>Date</th>
|
168
|
+
<th>Topics</th>
|
169
|
+
<!-- <th>Notes</th> -->
|
170
|
+
<th>Reading</th>
|
171
|
+
<th>Milestones</th>
|
172
|
+
</tr>
|
173
|
+
TABLE
|
174
|
+
|
175
|
+
first = true
|
176
|
+
@schedule_days.each do |schedule_day|
|
177
|
+
table << '<tr>'
|
178
|
+
|
179
|
+
if schedule_day[:begin_week]
|
180
|
+
unless first
|
181
|
+
# Add a blank row of horizontal lines
|
182
|
+
table << '<td></td><td></td><td></td><td></td><td></td><td></td></tr>'
|
183
|
+
table << "<tr class='week_end'><td></td><td></td><td></td><td></td><td></td><td></td></tr>"
|
184
|
+
table << '<tr>'
|
185
|
+
end
|
186
|
+
first = false
|
187
|
+
week_value = schedule_day[:week]
|
188
|
+
else
|
189
|
+
week_value = ''
|
190
|
+
end
|
191
|
+
|
192
|
+
table << " <td class='week_column'>#{week_value}</td>"
|
193
|
+
formatted_date = schedule_day[:date].strftime("%a. %-d %b.")
|
194
|
+
table << " <td class='date_column'>#{formatted_date}</td>"
|
195
|
+
table << " <td class='topics_column'>#{schedule_day[:topics].join("<br>")}</td>"
|
196
|
+
# table << " <td class='topics_column'>#{schedule_day[:notes].join("<br>")}</td>"
|
197
|
+
table << " <td class='reading_column'>#{schedule_day[:reading].join("<br>")}</td>"
|
198
|
+
table << " <td class='milestones_column'>#{schedule_day[:milestones].join("<br>")}</td>"
|
199
|
+
table << "</tr>"
|
200
|
+
end
|
201
|
+
table << "</table>"
|
202
|
+
table.join("\n")
|
203
|
+
end
|
204
|
+
|
205
|
+
def timeline_style
|
206
|
+
<<STYLE
|
207
|
+
table.kielceSchedule {
|
208
|
+
border-collapse: separate;
|
209
|
+
border-spacing: 2px;
|
210
|
+
}
|
211
|
+
|
212
|
+
.kielceSchedule tr th {
|
213
|
+
text-align: left;
|
214
|
+
}
|
215
|
+
|
216
|
+
.kielceSchedule tr td {
|
217
|
+
vertical-align: top;
|
218
|
+
padding-right: 10px;
|
219
|
+
}
|
220
|
+
|
221
|
+
.week_column, .date_column {
|
222
|
+
white-space: nowrap;
|
223
|
+
}
|
224
|
+
|
225
|
+
.kielceSchedule tr th, .date_column, .topics_column, .notes_column, .reading_column, .milestones_column, .week_end td {
|
226
|
+
border-bottom: 1px solid;
|
227
|
+
}
|
228
|
+
STYLE
|
229
|
+
end
|
230
|
+
|
231
|
+
def timeline_page
|
232
|
+
<<PAGE
|
233
|
+
<html>
|
234
|
+
<head>
|
235
|
+
<style>
|
236
|
+
#{timeline_style}
|
237
|
+
</style>
|
238
|
+
</head>
|
239
|
+
<body>
|
240
|
+
#{timeline_table}
|
241
|
+
</body>
|
242
|
+
</html>
|
243
|
+
PAGE
|
244
|
+
end
|
245
|
+
|
246
|
+
def assignment_style
|
247
|
+
<<STYLE
|
248
|
+
.kielceAssignmentTable {
|
249
|
+
border-spacing: 35px 0;
|
250
|
+
}
|
251
|
+
|
252
|
+
.kielceAssignmentTable tr th {
|
253
|
+
text-align: left;
|
254
|
+
}
|
255
|
+
|
256
|
+
.kielceAssignmentTable tr td {
|
257
|
+
vertical-align: top;
|
258
|
+
}
|
259
|
+
|
260
|
+
.kielceAssignmentTable_due {
|
261
|
+
white-space: nowrap;
|
262
|
+
}
|
263
|
+
|
264
|
+
.kielceAssignmentTable_title {
|
265
|
+
white-space: nowrap;
|
266
|
+
}
|
267
|
+
|
268
|
+
.exam {
|
269
|
+
background-color: lightgreen;
|
270
|
+
}
|
271
|
+
STYLE
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
def assignment_list
|
276
|
+
list = <<TABLE
|
277
|
+
<table class='kielceAssignmentTable'>
|
278
|
+
<tr>
|
279
|
+
<th>Due</th>
|
280
|
+
<th>Name</th>
|
281
|
+
<th>Details</th>
|
282
|
+
</tr>
|
283
|
+
TABLE
|
284
|
+
|
285
|
+
by_date = @assignments.values.reject { |item| item.due.nil? || item.type == 'Lab' }.sort_by { |a| a.due }
|
286
|
+
by_date.each do |assignment|
|
287
|
+
list += ' <tr>'
|
288
|
+
list += " <td class='kielceAssignmentTable_due'>#{assignment.due.strftime("%a. %-d %b.")}</td>\n"
|
289
|
+
list += " <td class='kielceAssignmentTable_title'>#{assignment.title(:full, true)}</td>\n"
|
290
|
+
list += " <td class='kielceAssignmentTable_details'>#{assignment.details}</td>\n"
|
291
|
+
list += " </tr>\n\n"
|
292
|
+
end
|
293
|
+
list += "</table>\n"
|
294
|
+
end
|
295
|
+
|
296
|
+
def lab_list
|
297
|
+
list = <<TABLE
|
298
|
+
<table class='kielceAssignmentTable'>
|
299
|
+
<tr>
|
300
|
+
<th>Date</th>
|
301
|
+
<th>Name</th>
|
302
|
+
<th>Details</th>
|
303
|
+
</tr>
|
304
|
+
TABLE
|
305
|
+
assigned_labs = @assignments.values.select { |item| item.type == 'Lab' && !item.assigned.nil? }
|
306
|
+
by_date = assigned_labs.sort { |a, b| a.assigned <=> b.assigned }
|
307
|
+
by_date.each do |assignment|
|
308
|
+
list += ' <tr>'
|
309
|
+
list += " <td class='kielceAssignmentTable_due'>#{assignment.assigned.strftime("%a. %-d %b.")}</td>\n"
|
310
|
+
list += " <td class='kielceAssignmentTable_title'>#{assignment.title(:full, true)}</td>\n"
|
311
|
+
list += " <td class='kielceAssignmentTable_details'>#{assignment.details}</td>\n"
|
312
|
+
list += " </tr>\n\n"
|
313
|
+
end
|
314
|
+
list += "</table>\n"
|
315
|
+
list
|
316
|
+
end
|
317
|
+
end # end Schedule
|
318
|
+
end # module
|
319
|
+
end # end KielcePlugins
|
320
|
+
#puts Schedule.new(ARGV[0]).timeline_page
|
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kielce
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Zachary Kurmas
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-01-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: 'An ERB-based templating engine for generating course documents. '
|
14
|
+
email:
|
15
|
+
executables:
|
16
|
+
- kielce
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/kielce
|
21
|
+
- lib/kielce.rb
|
22
|
+
- lib/kielce/helpers.rb
|
23
|
+
- lib/kielce/kielce.rb
|
24
|
+
- lib/kielce/kielce_data.rb
|
25
|
+
- lib/kielce/kielce_loader.rb
|
26
|
+
- lib/kielce_plugins/schedule.rb
|
27
|
+
- lib/kielce_plugins/schedule/#schedule.rb#
|
28
|
+
- lib/kielce_plugins/schedule/assignment.rb
|
29
|
+
- lib/kielce_plugins/schedule/schedule.rb
|
30
|
+
homepage: https://github.com/kurmasz/KielceRB
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata: {}
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubygems_version: 3.0.8
|
50
|
+
signing_key:
|
51
|
+
specification_version: 4
|
52
|
+
summary: An ERB-based templating engine for generating course documents.
|
53
|
+
test_files: []
|