kielce 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|