alma_course_loader 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +63 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +586 -0
- data/Rakefile +10 -0
- data/alma_course_loader.gemspec +33 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/course_loader_diff +11 -0
- data/lib/alma_course_loader.rb +8 -0
- data/lib/alma_course_loader/cli/course_loader.rb +129 -0
- data/lib/alma_course_loader/cli/diff.rb +85 -0
- data/lib/alma_course_loader/diff.rb +173 -0
- data/lib/alma_course_loader/filter.rb +272 -0
- data/lib/alma_course_loader/reader.rb +298 -0
- data/lib/alma_course_loader/version.rb +3 -0
- data/lib/alma_course_loader/writer.rb +116 -0
- metadata +162 -0
@@ -0,0 +1,272 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module AlmaCourseLoader
|
5
|
+
# Methods for parsing filter specification strings
|
6
|
+
# The primary method is parse_filter(str, extractors)
|
7
|
+
module FilterParser
|
8
|
+
# The regular expression describing a filter string: [!][extractor[op]]value
|
9
|
+
FILTER = Regexp.new('\s*(?<negate>!)?\s*' \
|
10
|
+
'(' \
|
11
|
+
'(?<extractor>[^\s]*)\s+' \
|
12
|
+
'(?<method><=?|>=?|==|!=|~|=~|!~|keyin|valuein|in)\s+' \
|
13
|
+
')?' \
|
14
|
+
'(?<value>.*)').freeze
|
15
|
+
|
16
|
+
# Filter operators mapped to method names
|
17
|
+
#
|
18
|
+
# The operators in filter expressions are converted to methods called on
|
19
|
+
# the filter value. Note that this means that the < and > operators are
|
20
|
+
# inverted ('field < filter-value' is equivalent to 'filter-value.>(field)')
|
21
|
+
# Operators not specified explicitly in FILTER_MAP are called directly as
|
22
|
+
# methods.
|
23
|
+
#
|
24
|
+
# The hash value is either:
|
25
|
+
# - a Symbol representing the method name to call on the filter value;
|
26
|
+
# - an array [method-name-symbol, negated] containing the method name symbol
|
27
|
+
# and a Boolean indicating whether the method is implicitly negated,
|
28
|
+
# e.g. !~ is !match(); the default is no implicit negation.
|
29
|
+
#
|
30
|
+
FILTER_MAP = {
|
31
|
+
:~ => :match,
|
32
|
+
:=~ => :match,
|
33
|
+
:!~ => [:match, true],
|
34
|
+
:< => :>,
|
35
|
+
:<= => :>=,
|
36
|
+
:>= => :<=,
|
37
|
+
:> => :<,
|
38
|
+
:in => :include?,
|
39
|
+
:keyin => :key?,
|
40
|
+
:valuein => :value?
|
41
|
+
}.freeze
|
42
|
+
|
43
|
+
# Parses a filter string and returns the parsed filter string components
|
44
|
+
# @param str [String] the filter string: [extractor][+|-]value
|
45
|
+
# where value is a JSON string specifying the value(s) to match,
|
46
|
+
# extractor is the optional name of an entry in the extractors hash,
|
47
|
+
# @param extractors [Hash<Symbol, Proc|Method>] a hash of named field
|
48
|
+
# extractors referenced by the filter string; this may include a nil
|
49
|
+
# key which specifies the default extractor if not given in the filter
|
50
|
+
# string
|
51
|
+
# @return [Array] the parsed filter string components:
|
52
|
+
# values, method, extractor, negate: [Object, Symbol, Proc, Boolean]
|
53
|
+
# @raise [ArgumentError] if the filter string is invalid
|
54
|
+
def parse_filter(str = nil, extractors = nil)
|
55
|
+
# Parse the filter string
|
56
|
+
raise ArgumentError, 'expected filter' if str.nil? || str.empty?
|
57
|
+
match = FILTER.match(str)
|
58
|
+
raise ArgumentError, "invalid filter: #{str}" if match.nil?
|
59
|
+
# Get the filter value(s)
|
60
|
+
value = Private.parse_value(match)
|
61
|
+
# Get the method used to compare the filter value(s)
|
62
|
+
method, method_negate = parse_method(match, value)
|
63
|
+
# Get the effective negate flag
|
64
|
+
negate = Private.negate_flag(match[:negate] == '!', method_negate)
|
65
|
+
# Get the named extractor from the hash or block
|
66
|
+
extractor = Private.parse_extractor(match, extractors)
|
67
|
+
# Return the parsed filter string components
|
68
|
+
[value, method, extractor, negate]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Validates and returns the method symbol and negation flag
|
72
|
+
# @param match [MatchData] the parsed filter string
|
73
|
+
# @param values [Object] the filter value(s)
|
74
|
+
# @return [Array] the method symbol and negate flag
|
75
|
+
# @raise [ArgumentError] if the specified method is not supported by the
|
76
|
+
# filter values
|
77
|
+
def parse_method(match, values)
|
78
|
+
method_info = Private.method_symbol(match, values)
|
79
|
+
method = method_info[0]
|
80
|
+
unless values.respond_to?(method)
|
81
|
+
raise ArgumentError, "invalid method: #{values.class.name}\##{method}"
|
82
|
+
end
|
83
|
+
method_info
|
84
|
+
end
|
85
|
+
|
86
|
+
# Private helpers
|
87
|
+
module Private
|
88
|
+
# Returns true if the value parameter is a collection
|
89
|
+
# @param value [Object] the value to test
|
90
|
+
# @return [Boolean] true if the value is a collection, false otherwise
|
91
|
+
def self.collection?(value)
|
92
|
+
value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(Set)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns a Proc implementing the filter match
|
96
|
+
# @param values [Object] the filter values
|
97
|
+
# @param method [Symbol] the method to call against the filter values
|
98
|
+
# @param negate [Boolean] if true, negate the result of the method
|
99
|
+
# @return [Proc] the Proc implementing the filter
|
100
|
+
def self.matcher(values, method = nil, negate = false)
|
101
|
+
method, negate = method_symbol(method, values) if method.nil?
|
102
|
+
proc do |value|
|
103
|
+
result = values.send(method, value)
|
104
|
+
negate ? !result : result
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns the method symbol or appropriate default if not specified
|
109
|
+
# If no method is specified in the filter string, the default is :include?
|
110
|
+
# for Arrays, Hashes and Sets, :match for regular expressions and :== for
|
111
|
+
# everything else.
|
112
|
+
# @param match [MatchData] the parsed filter string
|
113
|
+
# @param values [Object] the filter value(s)
|
114
|
+
# @return [Array] the method symbol and negation flag (false|true)
|
115
|
+
def self.method_symbol(match, values)
|
116
|
+
method = match[:method]
|
117
|
+
# Return the default if no method is specified
|
118
|
+
return method_symbol_default(values) if method.nil? || method.empty?
|
119
|
+
method = method.to_sym
|
120
|
+
# Return the method as specified unless it's mapped
|
121
|
+
return method, false unless FILTER_MAP.key?(method)
|
122
|
+
# Otherwise return the mapped filter
|
123
|
+
method, negate = FILTER_MAP[method]
|
124
|
+
[method, negate.nil? ? false : negate]
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns a default method symbol based on the filter value type
|
128
|
+
# @param values [Object] the filter value(s)
|
129
|
+
# @return [Array] the default method symbol and negation flag (false|true)
|
130
|
+
def self.method_symbol_default(values)
|
131
|
+
return :include?, false if collection?(values)
|
132
|
+
return :match, false if values.is_a?(Regexp)
|
133
|
+
[:==, false]
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns the actual filter negate flag based on the requested filter
|
137
|
+
# negate flag and the compare method's negate flag
|
138
|
+
# @param filter_negate [Boolean] the requested filter negate flag
|
139
|
+
# @param method_negate [Boolean] the compare method's negate flag
|
140
|
+
# @return [Boolean] the effective negate flag
|
141
|
+
def self.negate_flag(filter_negate = false, method_negate = false)
|
142
|
+
# The effective negate flag is true only if exactly one of the arguments
|
143
|
+
# is true (filter_flag xor method_negate) since double negation cancels
|
144
|
+
filter_negate != method_negate
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns the extractor Proc specified by a parsed filter string
|
148
|
+
# @param match [MatchData] the parsed filter string
|
149
|
+
# @param extractors [Hash<Symbol, Proc|Method>] the extractors
|
150
|
+
# @raise [ArgumentError] if a specified extractor is invalid or there is
|
151
|
+
# no default extractor in extractors when required
|
152
|
+
def self.parse_extractor(match, extractors = nil)
|
153
|
+
raise ArgumentError, 'extractors required' \
|
154
|
+
if extractors.nil? || extractors.empty?
|
155
|
+
e = match[:extractor]
|
156
|
+
extractor = extractors[e ? e.to_sym : nil]
|
157
|
+
return extractor unless extractor.nil?
|
158
|
+
raise ArgumentError, "invalid extractor: #{e}" if e
|
159
|
+
raise ArgumentError, 'no default extractor'
|
160
|
+
end
|
161
|
+
|
162
|
+
# Returns a parsed regular expression
|
163
|
+
# @param regexp [String] the regular expression from the value string
|
164
|
+
# @return [Regexp] the parsed regular expression
|
165
|
+
# @raise [ArgumentError] if the regular expression cannot be parsed
|
166
|
+
def self.parse_regexp(regexp)
|
167
|
+
# Remove enclosing / if present
|
168
|
+
first = regexp.start_with?('/') ? 1 : 0
|
169
|
+
last = regexp.end_with?('/') ? -2 : -1
|
170
|
+
Regexp.new(regexp[first..last])
|
171
|
+
rescue RegexpError
|
172
|
+
raise ArgumentError, "invalid regular expression: #{regexp}"
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns the parsed value string
|
176
|
+
# @param match [MatchData] the parsed filter string
|
177
|
+
# @return [Object] the filter value(s)
|
178
|
+
# @raise [ArgumentError] if the value string cannot be parsed
|
179
|
+
def self.parse_value(match)
|
180
|
+
value = match[:value]
|
181
|
+
return nil if value.nil?
|
182
|
+
# Parse strings starting with / as regular expressions
|
183
|
+
return parse_regexp(value) if value.start_with?('/')
|
184
|
+
# Parse all other strings as JSON
|
185
|
+
JSON.parse(value)
|
186
|
+
rescue JSON::ParserError
|
187
|
+
raise ArgumentError, "invalid value: #{value}"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Implements a course filter
|
193
|
+
class Filter
|
194
|
+
extend FilterParser
|
195
|
+
|
196
|
+
# @!attribute [rw] extractor
|
197
|
+
# @return [Proc, Method] a block accepting a year, course and cohort and
|
198
|
+
# returning the value used in filter matching
|
199
|
+
attr_accessor :extractor
|
200
|
+
|
201
|
+
# @!attribute [rw] method
|
202
|
+
# @return [Symbol] the method to call against the values
|
203
|
+
attr_accessor :method
|
204
|
+
|
205
|
+
# @!attribute [rw] negate
|
206
|
+
# @return [Boolean] if true, the filter result is negated
|
207
|
+
attr_accessor :negate
|
208
|
+
|
209
|
+
# @!attribute [rw] values
|
210
|
+
# @return [Object] the value or collection to match against
|
211
|
+
attr_accessor :values
|
212
|
+
|
213
|
+
# Parses a filter string and returns a Filter instance
|
214
|
+
# @param str [String] the filter string: [extractor][+|-]value
|
215
|
+
# where value is a JSON string specifying the value(s) to match,
|
216
|
+
# extractor is the optional name of an entry in the extractors hash,
|
217
|
+
# @param extractors [Hash<Symbol, Proc|Method>] a hash of named field
|
218
|
+
# extractors referenced by the filter string; this may include a nil
|
219
|
+
# key which specifies the default extractor if not given in the filter
|
220
|
+
# string
|
221
|
+
# @raise [ArgumentError] if the filter string is invalid
|
222
|
+
def self.parse(str = nil, extractors = nil)
|
223
|
+
new(*parse_filter(str, extractors))
|
224
|
+
end
|
225
|
+
|
226
|
+
# Initialises a new Filter instance
|
227
|
+
# @param values [Object] the value or collection to match against
|
228
|
+
# @param method [Symbol] the method symbol applied to the filter values
|
229
|
+
# @param extractor [Proc, Method] a block accepting a year, course and
|
230
|
+
# cohort and returning the value used in filter matching.
|
231
|
+
# This may be also passed as a code block.
|
232
|
+
# @param negate [Boolean] if true, negate the result of the filter
|
233
|
+
# @raise [ArgumentError]
|
234
|
+
# @return [void]
|
235
|
+
def initialize(values = nil, method = nil, extractor = nil, negate = false,
|
236
|
+
&block)
|
237
|
+
self.method = method
|
238
|
+
self.negate = negate
|
239
|
+
self.extractor = extractor || block
|
240
|
+
self.values = values
|
241
|
+
@matcher = matcher_proc(method)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Applies the filter to a course/cohort
|
245
|
+
# @param year [Object] the course year
|
246
|
+
# @param course [Object] the course
|
247
|
+
# @param cohort [Object] the course cohort
|
248
|
+
# @return [Boolean] true if the course passes the filter, false otherwise
|
249
|
+
def call(year = nil, course = nil, cohort = nil)
|
250
|
+
# Get the value for matching
|
251
|
+
value = extractor.call(year, course, cohort)
|
252
|
+
# Test against the filter values (true => match, false => no match)
|
253
|
+
result = @matcher.call(value)
|
254
|
+
# Return the result with appropriate negation
|
255
|
+
negate ? !result : result
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
|
260
|
+
# Returns a Proc instance which calls the specified method on the filter
|
261
|
+
# value object
|
262
|
+
# @param method [Method, Proc, Symbol] the method (Method/Proc arguments are
|
263
|
+
# returned as-is)
|
264
|
+
# @return [Proc] the Proc instance
|
265
|
+
def matcher_proc(method)
|
266
|
+
# Return Method/Proc arguments unchanged
|
267
|
+
return method if method.is_a?(Method) || method.is_a?(Proc)
|
268
|
+
# Otherwise return a Proc which invokes method on the filter values
|
269
|
+
proc { |value| values.send(method, value) }
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,298 @@
|
|
1
|
+
require 'alma_course_loader/version'
|
2
|
+
|
3
|
+
module AlmaCourseLoader
|
4
|
+
# The abstract base class for course readers.
|
5
|
+
#
|
6
|
+
# A course reader is responsible for reading course data from a data source
|
7
|
+
# and iterating over each course element. A course element is either the
|
8
|
+
# course itself or, if the course is divided into cohorts, a single cohort
|
9
|
+
# of the course.
|
10
|
+
#
|
11
|
+
# It is expected that courses are retrieved by year, and the iterators accept
|
12
|
+
# a list of years for which courses are required. The implementation and
|
13
|
+
# type of years, courses and cohorts is deferred to the subclasses.
|
14
|
+
#
|
15
|
+
# Filters may be defined which determine whether a single course element is
|
16
|
+
# processed or ignored. The course element must pass all filters before it
|
17
|
+
# is processed.
|
18
|
+
#
|
19
|
+
# The years and filters controlling course element iteration can be passed
|
20
|
+
# to the constructor, which defines defaults for subsequent iterators, or to
|
21
|
+
# the #each method, which defines the criteria for that iteration. When
|
22
|
+
# creating a reader which will be passed to a writer, always define the
|
23
|
+
# iterator criteria in the reader constructor, as the writer uses the
|
24
|
+
# default iterator.
|
25
|
+
#
|
26
|
+
# There are two classes of iterators. The first invokes its block with the
|
27
|
+
# course details: yield(year, course, cohort, instructors)
|
28
|
+
# The second, used by the Writer class, invokes its block with an array of
|
29
|
+
# data suitable for writing as a row in a course loader CSV file: yield(row)
|
30
|
+
#
|
31
|
+
# @abstract Subclasses must implement the following methods:
|
32
|
+
# #courses
|
33
|
+
# returns a list of course objects
|
34
|
+
# #course_cohorts(course)
|
35
|
+
# returns a list of cohorts for the course or nil if cohorts are not used
|
36
|
+
# #current_academic_year
|
37
|
+
# returns the current academic year
|
38
|
+
# #instructors(year, course, cohort)
|
39
|
+
# returns a list of instructors for the course
|
40
|
+
# #row_data(array, year, course, cohort, instructors)
|
41
|
+
# configures an array of formatted course information suitable for writing
|
42
|
+
# to an Alma course loader CSV file
|
43
|
+
#
|
44
|
+
# @example Create course filters
|
45
|
+
# # Define extractors to retrieve values from a course element for matching
|
46
|
+
# # Extractors are called with the year, course and cohort arguments
|
47
|
+
# get_code = proc { |year, course, cohort| course.code }
|
48
|
+
# get_title = proc { |year, course, cohort| course.title }
|
49
|
+
#
|
50
|
+
# # Define a list of course codes and a regular expression matching titles
|
51
|
+
# codes = ['COMPSCI101', 'PHYSICS101', 'MAGIC101']
|
52
|
+
# titles = Regexp.compile('dissertation|(extended|ma) essay|test', true)
|
53
|
+
#
|
54
|
+
# # Define a filter which passes only the specified course codes
|
55
|
+
# include_codes = AlmaCourseLoader::Filter.new(codes, :include, get_code)
|
56
|
+
#
|
57
|
+
# # Define a filter which passes all codes except those specified
|
58
|
+
# exclude_codes = AlmaCourseLoader::Filter.new(codes, :exclude, get_code)
|
59
|
+
#
|
60
|
+
# # Define a filter which passes all titles matching the regular expression
|
61
|
+
# include_titles = AlmaCourseLoader::Filter.new(titles, :include, get_title)
|
62
|
+
#
|
63
|
+
# # Define a filter which passes all titles except those matching the regexp
|
64
|
+
# exclude_titles = AlmaCourseLoader::Filter.new(titles, :exclude, get_title)
|
65
|
+
#
|
66
|
+
# @example Create a reader with default selection criteria
|
67
|
+
# # Filters are passed in an array, course elements must pass all filters
|
68
|
+
# reader = Reader.new(2016, 2017, filters: [exclude_codes, include_titles])
|
69
|
+
#
|
70
|
+
# @example Iterate course elements using default selection criteria
|
71
|
+
# reader.each { |year, course, cohort, instructors| ... }
|
72
|
+
#
|
73
|
+
# @example Iterate course elements using specific selection criteria
|
74
|
+
# # Use an empty array to override filters. filters: nil will use the
|
75
|
+
# # default filters.
|
76
|
+
# reader.each(2012, filters: []) { |year, course, cohort, instructors| ... }
|
77
|
+
#
|
78
|
+
# @example Iterate course CSV rows
|
79
|
+
# reader.each_row { |row_array| ... }
|
80
|
+
# reader.each_row(2012, filters: []) { |row_array| ... }
|
81
|
+
#
|
82
|
+
class Reader
|
83
|
+
# Initialises a new Reader instance
|
84
|
+
# Positional parameters are the default years to iterate over
|
85
|
+
# @param current_year [Object] the current academic year
|
86
|
+
# @param filters [Array<AlmaCourseLoader::Filter>] default course filters
|
87
|
+
def initialize(*years, current_year: nil, filters: nil)
|
88
|
+
@current_academic_year = current_year || current_academic_year
|
89
|
+
@filters = filters
|
90
|
+
@years = years.nil? || years.empty? ? [@current_academic_year] : years
|
91
|
+
end
|
92
|
+
|
93
|
+
# Iterate over the courses for specified years. Only courses which pass
|
94
|
+
# the filters are passed to the block.
|
95
|
+
# Positional parameters are years
|
96
|
+
# @param filters [Array<AlmaCourseLoader::Filter>] course filters
|
97
|
+
# @return [void]
|
98
|
+
# @yield [year, course, cohort, year, instructors] Passes the course to the
|
99
|
+
# block
|
100
|
+
# @yieldparam year [Object] the course year
|
101
|
+
# @yieldparam course [Object] the course
|
102
|
+
# @yieldparam cohort [Object] the course cohort
|
103
|
+
# @yieldparam instructors [Array<Object>] the course instructors
|
104
|
+
def each(*years, filters: nil, &block)
|
105
|
+
# Process courses for each year
|
106
|
+
years = @years if years.nil? || years.empty?
|
107
|
+
years.each { |year| each_course_in_year(year, filters: filters, &block) }
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
111
|
+
# Iterates over the cohorts in a course, or just the course itself if
|
112
|
+
# cohort processing is disabled. Only courses/cohorts which pass the filters
|
113
|
+
# are passed to the block.
|
114
|
+
# @param year [Object] the course year
|
115
|
+
# @param course [Object] the course
|
116
|
+
# @param filters [Array<AlmaCourseLoader::Filter>] course filters
|
117
|
+
# @return [void]
|
118
|
+
# @yield [year, course, cohort, year, instructors] Passes the course to the
|
119
|
+
# block
|
120
|
+
# @yieldparam year [Object] the course year
|
121
|
+
# @yieldparam course [Object] the course
|
122
|
+
# @yieldparam cohort [Object] the course cohort
|
123
|
+
# @yieldparam instructors [Array<Object>] the course instructors
|
124
|
+
def each_cohort_in_course(year, course, filters: nil, &block)
|
125
|
+
cohorts = course_cohorts(year, course)
|
126
|
+
if cohorts.nil?
|
127
|
+
# Process the course
|
128
|
+
process_course(year, course, nil, filters, &block)
|
129
|
+
else
|
130
|
+
cohorts.each do |cohort|
|
131
|
+
# Process each cohort of the course
|
132
|
+
process_course(year, course, cohort, filters, &block)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Iterates over the cohorts in a course, or just the course itself if
|
138
|
+
# cohort processing is disabled. Only courses/cohorts which pass the filters
|
139
|
+
# are passed to the block.
|
140
|
+
# @param year [Object] the course year
|
141
|
+
# @param course [Object] the course
|
142
|
+
# @param filters [Array<AlmaCourseLoader::Filter>] course filters
|
143
|
+
# @return [void]
|
144
|
+
# @yield [row] Passes the course to the block
|
145
|
+
# @yieldparam row [Array<String>] the course as a CSV row (array)
|
146
|
+
def each_cohort_in_course_row(year, course, filters: nil, &block)
|
147
|
+
each_cohort_in_course(year, course, filters: filters) do |*args|
|
148
|
+
row(*args, &block)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Iterate over the courses for the specified year. Only courses which pass
|
153
|
+
# the filters are passed to the block.
|
154
|
+
# @param year [Object] the year
|
155
|
+
# @param filters [Array<AlmaCourseLoader::Filter>] filters selecting courses
|
156
|
+
# @return [void]
|
157
|
+
# @yield [year, course, cohort, year, instructors] Passes the course to the
|
158
|
+
# block
|
159
|
+
# @yieldparam year [Object] the course year
|
160
|
+
# @yieldparam course [Object] the course
|
161
|
+
# @yieldparam cohort [Object] the course cohort
|
162
|
+
# @yieldparam instructors [Array<Object>] the course instructors
|
163
|
+
def each_course_in_year(year, filters: nil, &block)
|
164
|
+
# Simplify the test for filter existence
|
165
|
+
filters ||= @filters
|
166
|
+
filters = nil if filters.is_a?(Array) && filters.empty?
|
167
|
+
# Get all courses for the year
|
168
|
+
courses(year).each do |course|
|
169
|
+
each_cohort_in_course(year, course, filters: filters, &block)
|
170
|
+
end
|
171
|
+
nil
|
172
|
+
end
|
173
|
+
|
174
|
+
# Iterate over the courses for the specified year. Only courses which pass
|
175
|
+
# the filters are passed to the block.
|
176
|
+
# @param year [Object] the year
|
177
|
+
# @param filters [Array<AlmaCourseLoader::Filter>] course filters
|
178
|
+
# @return [void]
|
179
|
+
# @yield [row] Passes the course to the block
|
180
|
+
# @yieldparam row [Array<String>] the course as a CSV row (array)
|
181
|
+
def each_course_in_year_row(year, filters: nil, &block)
|
182
|
+
each_course_in_year(year, filters: filters) { |*args| row(*args, &block) }
|
183
|
+
end
|
184
|
+
|
185
|
+
# Iterate over the courses for specified years. Only courses which pass
|
186
|
+
# the filters are passed to the block.
|
187
|
+
# Positional parameters are years
|
188
|
+
# @param filters [Array<AlmaCourseLoader::Filter>] course filters
|
189
|
+
# @return [void]
|
190
|
+
# @yield [row] Passes the course to the block
|
191
|
+
# @yieldparam row [Array<String>] the course as a CSV row (array)
|
192
|
+
def each_row(*years, filters: nil, &block)
|
193
|
+
each(*years, filters: filters) { |*args| row(*args, &block) }
|
194
|
+
end
|
195
|
+
|
196
|
+
protected
|
197
|
+
|
198
|
+
# Returns a list of courses for the specified year
|
199
|
+
# @abstract Subclasses must implement this method.
|
200
|
+
# @param year [Object] the course year
|
201
|
+
# @return [Array<Object>] the courses for the year
|
202
|
+
def courses(year)
|
203
|
+
[]
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns a list of cohorts for the specified course
|
207
|
+
# @abstract Subclasses should implement this method. If courses are not
|
208
|
+
# divided into cohorts, this method must return nil.
|
209
|
+
# @param course [Object] the course
|
210
|
+
# @return [Array<Object>, nil] the cohorts for the course, or nil to disable
|
211
|
+
# cohort processing
|
212
|
+
def course_cohorts(year, course)
|
213
|
+
nil
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the current academic year
|
217
|
+
# @abstract Subclasses should implement this method
|
218
|
+
# @return [Object] the current academic year
|
219
|
+
def current_academic_year
|
220
|
+
nil
|
221
|
+
end
|
222
|
+
|
223
|
+
# Applies filters to the course/cohort to determine whether to process
|
224
|
+
# @param year [Object] the course year
|
225
|
+
# @param course [Object] the course
|
226
|
+
# @param cohort [Object] the course cohort
|
227
|
+
# @return [Boolean] true if the course is to be processed, false if not
|
228
|
+
def filter(year, course, cohort, filters)
|
229
|
+
# Return true if no filters are specified
|
230
|
+
return true if filters.nil? || filters.empty?
|
231
|
+
# Return false if any of the filters returns false
|
232
|
+
filters.each { |f| return false unless f.call(year, course, cohort) }
|
233
|
+
# At this point all filters returned true
|
234
|
+
true
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns a list of instructors for the course/cohort
|
238
|
+
# @abstract Subclasses should implement this method
|
239
|
+
# @param year [Object] the course year
|
240
|
+
# @param course [Object] the course
|
241
|
+
# @param cohort [Object] the course cohort
|
242
|
+
# @return [Array<Object>] the course instructors
|
243
|
+
def instructors(year, course, cohort)
|
244
|
+
[]
|
245
|
+
end
|
246
|
+
|
247
|
+
# Filters and processes a course
|
248
|
+
# @param year [Object] the course year
|
249
|
+
# @param course [Object] the course
|
250
|
+
# @param cohort [Object] the LUSI course cohort
|
251
|
+
# @param filters [Array<AlmaCourseLoader::Filter>] the course filters
|
252
|
+
# @return [Boolean] true if the course was processed, false otherwise
|
253
|
+
# @yield Passes the course to the block
|
254
|
+
# @yieldparam year [Object] the course year
|
255
|
+
# @yieldparam course [Object] the course
|
256
|
+
# @yieldparam cohort [Object] the course cohort
|
257
|
+
# @yieldparam instructors [Array<Object>] the course instructors
|
258
|
+
def process_course(year, course, cohort = nil, filters = nil)
|
259
|
+
# The course must pass all filters
|
260
|
+
return false unless filter(year, course, cohort, filters)
|
261
|
+
# Get the course instructors
|
262
|
+
course_instructors = instructors(year, course, cohort)
|
263
|
+
# Pass the details to the block
|
264
|
+
yield(year, course, cohort, course_instructors) if block_given?
|
265
|
+
# Indicate that the course was processed
|
266
|
+
true
|
267
|
+
end
|
268
|
+
|
269
|
+
# Returns a CSV row (array) for a specific course/cohort
|
270
|
+
# @param year [Object] the course year
|
271
|
+
# @param course [Object] the course
|
272
|
+
# @param cohort [Object] the course cohort
|
273
|
+
# @param instructors [Array<Object>] the
|
274
|
+
# course instructor enrolments
|
275
|
+
# @yield Passes the CSV row array to the block
|
276
|
+
# @yieldparam row [Array<String>] the CSV row array
|
277
|
+
def row(year = nil, course = nil, cohort = nil, instructors = nil)
|
278
|
+
# Create and populate the CSV row
|
279
|
+
data = Array.new(31)
|
280
|
+
row_data(data, year, course, cohort, instructors)
|
281
|
+
# Pass the row to the block
|
282
|
+
yield(data) if block_given?
|
283
|
+
# Return the row
|
284
|
+
data
|
285
|
+
end
|
286
|
+
|
287
|
+
# Populates the CSV row (array) for a specific course/cohort
|
288
|
+
# @abstract Subclasses must implement this method
|
289
|
+
# @param data [Array<String>] the CSV row (array)
|
290
|
+
# @param year [Object] the course year
|
291
|
+
# @param course [Object] the course
|
292
|
+
# @param cohort [Object] the course cohort
|
293
|
+
# @param instructors [Array<Object>] the course instructors
|
294
|
+
def row_data(data, year, course, cohort, instructors)
|
295
|
+
data
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|