alma_course_loader 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|