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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6449461a6061323c327768cba599ee11ba6b8f9e
4
+ data.tar.gz: fd98f368196a65e9e429d8fe07071e694364b714
5
+ SHA512:
6
+ metadata.gz: 7708268cbb7aa054fff5e5978d7b1b6ed5c1c3c7c0210addf1f626ff3ef69efe7ff73bd796297392e8f1040d0fbeb21a25a1dedf1fb74a06ee669daae6491275
7
+ data.tar.gz: b63215b9c2d8f954608df91e47db4d374f2d6355691713602404db1c9bde13224dc6d9ffa2a7e04b2e96d3c724481471488251e25208af4d0ed493196db04bc0
@@ -0,0 +1,63 @@
1
+ # IntelliJ files
2
+ .idea/
3
+ *.iml
4
+ *.iws
5
+
6
+ # Created by https://www.gitignore.io/api/ruby
7
+
8
+ ### Ruby ###
9
+ *.gem
10
+ *.rbc
11
+ /.config
12
+ /coverage/
13
+ /InstalledFiles
14
+ /pkg/
15
+ /spec/reports/
16
+ /spec/examples.txt
17
+ /test/tmp/
18
+ /test/version_tmp/
19
+ /tmp/
20
+
21
+ # Used by dotenv library to load environment variables.
22
+ .env
23
+
24
+ ## Specific to RubyMotion:
25
+ .dat*
26
+ .repl_history
27
+ build/
28
+ *.bridgesupport
29
+ build-iPhoneOS/
30
+ build-iPhoneSimulator/
31
+
32
+ ## Specific to RubyMotion (use of CocoaPods):
33
+ #
34
+ # We recommend against adding the Pods directory to your .gitignore. However
35
+ # you should judge for yourself, the pros and cons are mentioned at:
36
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
37
+ #
38
+ # vendor/Pods/
39
+
40
+ ## Documentation cache and generated files:
41
+ /.yardoc/
42
+ /_yardoc/
43
+ /doc/
44
+ /rdoc/
45
+
46
+ ## Environment normalization:
47
+ /.bundle/
48
+ /vendor/bundle
49
+ /lib/bundler/man/
50
+
51
+ # for a library or gem, you might want to ignore these files since the code is
52
+ # intended to run in multiple environments; otherwise, check them in:
53
+ Gemfile.lock
54
+ .ruby-version
55
+ .ruby-gemset
56
+
57
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
58
+ .rvmrc
59
+
60
+ # Test output files
61
+ /test/fixtures/diff_test_cli.log
62
+ /test/fixtures/diff_test_delete.csv
63
+ /test/fixtures/diff_test_update.csv
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.14.6
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at Sh3d0fd00m. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in alma_course_loader.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 lbaajh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,586 @@
1
+ # AlmaCourseLoader
2
+
3
+ This gem provides a simple framework for generating Alma course loader files.
4
+ It provides a `Reader` class which serves as a basis for iterating over courses
5
+ from some data source, and a `Writer` class which uses the `Reader` to generate
6
+ a course loader file.
7
+
8
+ A command-line script `course_loader_diff` is also provided for comparing two
9
+ course loader files and generating further course loader files containing the
10
+ appropiate delete/update operations.
11
+
12
+ The implementation of classes and command-line scripts to generate course loader
13
+ files from specific data sources is left to clients of this gem.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'alma_course_loader'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install alma_course_loader
30
+
31
+ ## Configuring Course Loading
32
+
33
+ 1. Write a command-line script to create a course loader file from your course
34
+ manager or other data source. This gem provides helper classes to assist with
35
+ this - see *Writing a Course Loader* below.
36
+
37
+ 2. Schedule this script to run at regular intervals.
38
+
39
+ 3. Schedule the script `course_loader_diff` to run after the course loader
40
+ script to generate the deletions and updates to be processed by Alma.
41
+
42
+ 4. Schedule Alma course loader jobs to run after `course_loader_diff` to
43
+ process the generated files.
44
+
45
+ An example using `cron` to schedule a daily course update using the fictitious
46
+ course loader script `load_courses_from_cms` might be:
47
+ ```bash
48
+ # File locations
49
+ dir_data=/home/alma/course/data
50
+ dir_delete=/home/alma/course/delete
51
+ dir_update=/home/alma/course/update
52
+
53
+ # Use dates as filenames
54
+ today=$(date +%Y%m%d)
55
+ yday=$(date -d "-1 day" +%Y%m%d)
56
+
57
+ # Files
58
+ data_today=${dir_data}/$today
59
+ data_yday=${dir_data}/$yday
60
+ del=${dir_delete}/$today
61
+ log=/var/log/course/$today
62
+ upd=${dir_update}/$today
63
+
64
+ # Load courses from course management system daily at 1am
65
+ 00 01 * * * /opt/bin/load_courses_from_cms --out=$data_today
66
+
67
+ # Write changes to Alma course loader files, log verbosely to $log
68
+ 00 04 * * * /opt/bin/course_loader_diff --delete=$del --log=$log --update=$upd --verbose $data_yday $data_today
69
+ ```
70
+
71
+ ## Command-line Scripts
72
+
73
+ ### course_loader_diff
74
+
75
+ This script accepts two course loader files (the "current" or most-recently
76
+ created file, and the "previous" file preceding the current file) and outputs
77
+ the course entries which differ between the files. These files can be loaded
78
+ into Alma to perform the required changes.
79
+
80
+ The differences are written to three files:
81
+
82
+ * `create-file` contains new courses (those in `current-file` which are not in
83
+ `previous-file`) - by default these are applied using the *update* method
84
+ unless the `--rollover` flag is specified, which triggers updates using the
85
+ *rollover* method.
86
+
87
+ * `delete-file` contains deleted course (those in `previous-file` which are not
88
+ in `current-file`) - these are applied using the *delete* method.
89
+
90
+ * `update-file` contains courses which exist in both files but differ - these
91
+ are applied using the *update* method.
92
+
93
+ To allow course creation by *rollover* both input files should include the
94
+ rollover course code and section fields. If these fields are not present, all
95
+ courses will be created by *update* so associated reading lists will not be
96
+ copied.
97
+
98
+ `course_loader_diff` accepts the following command-line options:
99
+ ```bash
100
+ course_loader_diff -c create-file
101
+ -d delete-file
102
+ [-h | --help]
103
+ [-l | --log log-file]
104
+ [-r | --rollover]
105
+ -u update-file
106
+ [-v | --verbose]
107
+ previous-file current-file
108
+ ```
109
+
110
+ ##### `-c create-file | --create=create-file`
111
+
112
+ The output file of newly-created courses.
113
+
114
+ ##### `-d delete-file | --delete=delete-file`
115
+
116
+ The output file of deleted courses.
117
+
118
+ ##### `-h | --help`
119
+
120
+ Displays a help page for the command-line interface.
121
+
122
+ ##### `-l log-file | --log=log-file`
123
+
124
+ The activity log file (defaults to stdout).
125
+
126
+ ##### `-r | --rollover`
127
+
128
+ Causes newly-created courses to be created using the *rollover* method rather
129
+ than the *update* method as long as the course entry contains both the rollover
130
+ course and section fields. Courses which omit either of the rollover course
131
+ fields will be created using the *update* method.
132
+
133
+ ##### `-u update-file | --update=update-file`
134
+
135
+ The output file of updated courses.
136
+
137
+ ##### `-v | --verbose`
138
+
139
+ Causes the course loader entries to be included in the activity log, prefixed by
140
+ '<' (`previous-file`) and `>` (`current-file`).
141
+
142
+ ##### `previous-file`
143
+
144
+ The input file from a previous course loader run, e.g. yesterday.
145
+
146
+ ##### `current-file`
147
+
148
+ The input file from the latest course loader run, e.g. today.
149
+
150
+ Detailed usage is available from the command's help page:
151
+ ```bash
152
+ course_loader_diff -h
153
+ ```
154
+
155
+ ## Writing a Course Loader
156
+
157
+ This gem provides helper classes which may help to generate Alma course loader
158
+ files from any data source. It is not necessary to use these, as long as the
159
+ output of the course loader is a valid Alma course loader file representing the
160
+ source course data.
161
+
162
+ The helper classes abstract course loader file generation into a `Reader` which
163
+ iterates over the source data, a `Filter` which selects courses for processing
164
+ and a `Writer` which generates the Alma course loader file.
165
+
166
+ ### Reader
167
+
168
+ The following model is assumed:
169
+
170
+ * Courses are retrieved by year.
171
+
172
+ * Courses may optionally consist of a number of cohorts. If this is the case, a
173
+ *course element* is a single cohort of a specific course for a specific year. If
174
+ not, the course element is the course itself for a specific year.
175
+
176
+ * Course elements have one or more associated instructors.
177
+
178
+ `Reader` provides an abstract base class for iterating over course elements read
179
+ from any data source. The iterators accept a list of years for which courses are
180
+ required. The implementation details of years, courses and cohorts are deferred
181
+ to the subclasses.
182
+
183
+ #### Basic use
184
+
185
+ ```ruby
186
+ # Create a reader
187
+ reader = Reader.new
188
+
189
+ # Iterate over course elements
190
+ reader.each { |year, course, cohort, instructors| ... }
191
+
192
+ # Iterate over course elements as rows of the course loader CSV file
193
+ reader.each_row { |row| ... }
194
+ ```
195
+
196
+ The constructor and iterator methods accept course criteria as arguments.
197
+ Positional arguments are years for which courses are required. The `filter`
198
+ keyword argument may specify a list of filters to further refine the courses.
199
+
200
+ Course criteria passed to the constructor are used as defaults for subsequent
201
+ iterations. Criteria passed to the iterators override the defaults for that
202
+ use only.
203
+
204
+ ```ruby
205
+ # Create a reader with default critria
206
+ reader = AlmaCourseLoader::Reader.new(2015, 2016, filters: [f1, f2])
207
+
208
+ # Use the default criteria:
209
+ reader.each { |year, course, cohort, instructors| ... }
210
+ reader.each_row { |row| ... }
211
+
212
+ # Override the default years but use the default filters
213
+ reader.each(2013) { |year, course, cohort, instructors| ... }
214
+
215
+ # Override the default years and cancel the default filters
216
+ # the empty filter list is required to cancel the default filters
217
+ reader.each(2012, filters: []) { |row| ... }
218
+ ```
219
+
220
+ #### Filters
221
+
222
+ ##### Creating a filter
223
+
224
+ A `Filter` is an object which extracts a value from a course element and
225
+ matches it against a known value or set of values. If the match succeeds, the
226
+ filter returns `true` and the course element has *passed* the filter. If the
227
+ match fails, the filter returns `false` and the course element is rejected.
228
+
229
+ ###### Constructor
230
+
231
+ To construct a filter, pass in the value(s) to be matched against, the match
232
+ criterion (whether a match is considered a success or failure) and a code
233
+ block which extracts the match value from the course element.
234
+ ```ruby
235
+ # Extractor as a code block
236
+ filter = AlmaCourseLoader::Filter.new(values, criterion) { |year, course, cohort| ... }
237
+
238
+ # Extractor as a Proc
239
+ extactor = proc { |year, course, cohort| ... }
240
+ filter = AlmaCourseLoader::Filter.new(values, criterion, extractor)
241
+ ```
242
+ The match values can be:
243
+ * a single value (the values must stringwise match)
244
+ * an `Array`, `Hash` or `Set` (the extracted value must be in the values)
245
+ * a `Regexp` (the extracted value must match the regular expression)
246
+
247
+ The match criterion is either:
248
+ * `:exclude` (a match is a failure, i.e. the filter succeeds if it
249
+ excludes the extracted value)
250
+ * `:include` (a match is a success, i.e. the filter succeeds if it includes the
251
+ value)
252
+
253
+ The extractor is a `Proc` or code block which accepts the year, course and
254
+ cohort and returns a value to be matched against the filter's values.
255
+
256
+ ###### Parsing
257
+
258
+ A `Filter` can also be created by parsing a filter specification string:
259
+
260
+ ```ruby
261
+ filter = Filter.parse(filter_s, extractors)
262
+ ```
263
+
264
+ where `filter_s` is the filter specification string (see *Filter specification
265
+ strings* below) and `extractors` is a `Hash` mapping `Symbol` (extractor names)
266
+ to extractor `Proc` instances.
267
+
268
+ ##### Filter specification strings
269
+
270
+ The general form of a filter specification string is:
271
+
272
+ ```[!][field [op ]]value```
273
+
274
+ where:
275
+ * `!` negates the condition
276
+ * `field` is the name of a defined field extractor,
277
+ * `op` is one of the following operators:
278
+ * `<`, `<=`, `==`, `!=`, `>=`, `>` the value of field is less than (etc.)
279
+ value
280
+ * `~`, `!~` the value of field matches/does not match the regular expression
281
+ value
282
+ * `in` the value of field is a key (if value is a hash) or a value (if value
283
+ is any other type) in value; equivalent to value.include?(field)
284
+ * `keyin` the value of field is a key of the value hash; equivalent
285
+ to value.key?(field)
286
+ * `valuein` the value of field is a value in the value hash; equivalent to
287
+ value.value?(field)
288
+ * `value` is either a JSON string (which must include double-quotes around string
289
+ literal values and may specify arrays and hashes) or a regular expression
290
+ delimited by `/`.
291
+
292
+ Examples:
293
+
294
+ ```ruby
295
+ # Course code must exactly match CS101
296
+ course_code == "CS101"
297
+
298
+ # Course code must be one of CS101, CS102 or CS103
299
+ course_code in ["CS101", "CS102", "CS103"]
300
+
301
+ # Year must not be 2015 or 2016
302
+ ! year in [2015, 2016]
303
+
304
+ # Course code must begin with CS
305
+ course_code ~ /^CS/
306
+ ```
307
+
308
+ ##### Examples
309
+ ```ruby
310
+ codes = ['COMPSCI101', 'MAGIC101']
311
+ year1_magic = /MAGIC1\d\d/
312
+
313
+ # Extractor
314
+ get_code = proc { |year, course, cohort| course.code }
315
+ extractors = { code: get_code }
316
+
317
+ # Include only the specified codes
318
+ filter = AlmaCourseLoader::Filter.new(codes, :include, get_code)
319
+ # Using a filter specification string
320
+ filter = AlmaCourseLoader::Filter.parse('code in ["COMPSCI101", "MAGIC101"]', extractors)
321
+
322
+ # Include all except the specified codes
323
+ filter = AlmaCourseLoader::Filter.new(codes, :include, get_code, true)
324
+ # Using a filter specification string
325
+ filter = AlmaCourseLoader::Filter.parse('! code in ["COMPSCI101", "MAGIC101"]')
326
+
327
+ # Include all codes matching the regular expression
328
+ filter = AlmaCourseLoader::Filter.new(year1_magic, :match, get_code)
329
+ # Using a filter specification string
330
+ filter = AlmaCourseLoader::Filter.parse('code ~ /MAGIC\d\d/', extractors)
331
+
332
+ # Include exactly the specified code
333
+ filter = AlmaCourseLoader::Filter.new('MAGIC101', :==, get_code)
334
+ # Using a filter specification string
335
+ filter = AlmaCourseLoader::Filter.parse('code == "MAGIC101"', extractors)
336
+
337
+ # Include all except the specified code
338
+ filter = AlmaCourseLoader::Filter.new('MAGIC101', :!=, get_code)
339
+ # or equivalently
340
+ filter = AlmaCourseLoader::Filter.new('MAGIC101', :==, get_code, true)
341
+ # Using a filter specification string
342
+ filter = AlmaCourseLoader::Filter.parse('code != "MAGIC101"', extractors)
343
+ # or equivalently
344
+ filter = AlmaCourseLoader::Filter.parse('! code == "MAGIC101"', extractors)
345
+
346
+ # Include all codes stringwise less than "MAGIC101"
347
+ # - note that comparison operators are called against the filter value,
348
+ # so "code < filter-value" must be formulated as "filter-value > code"
349
+ # and "code > filter-value" as "filter-value < code"
350
+ filter = AlmaCourseLoader::Filter.new('MAGIC101', :>, get_code)
351
+ # Using a filter specification string
352
+ # - no need to invert the test as above, the parser handles this
353
+ filter = AlmaCourseLoader::Filter.parse('code < "MAGIC101"', extractors)
354
+ ```
355
+
356
+ ##### Executing a filter
357
+ Filters provide a `call` method which accepts the year, course and cohort and
358
+ returns `true` if the course passes or `false` if it's rejected.
359
+
360
+ ```ruby
361
+ if filter.call(year, course, cohort)
362
+ # The course passes, continue processing
363
+ else
364
+ # The course is rejected
365
+ end
366
+ ```
367
+
368
+ ##### Using filters with readers
369
+ `Reader` constructor and iterator methods accept a list of filters:
370
+ ```ruby
371
+ filter1 = AlmaCourseLoader::Filter.new(...)
372
+ filter2 = AlmaCourseLoader::Filter.new(...)
373
+ reader = Reader.new(..., filters: [filter1, filter2])
374
+ reader.each(..., filters: [filter1]) { ... }
375
+ ```
376
+
377
+ Course elements must pass all filters. If any filter fails, the course element
378
+ is not passed to the iterator's code block.
379
+
380
+ #### Writing a custom `Reader`
381
+
382
+ A `Reader` subclass may define any implementation of course, cohort, instructor
383
+ and year and must implement the following methods:
384
+
385
+ ##### `courses(year)`
386
+ ```ruby
387
+ # Returns an array of course objects for the year
388
+ def courses
389
+ # A course may be any object defined by the implementation
390
+ end
391
+ ```
392
+
393
+ ##### `course_cohorts(year, course)`
394
+ ```ruby
395
+ # Returns an array of cohorts for the course, or nil if cohorts are not used
396
+ def course_cohorts(year, course)
397
+ # A cohort may be any object defined by the implementation
398
+ end
399
+ ```
400
+
401
+ ##### `current_academic_year`
402
+ ```ruby
403
+ # Returns the current academic year
404
+ def current_academic_year
405
+ # A year may be any object defined by the implementation
406
+ end
407
+ ```
408
+
409
+ ##### `instructors(year, course, cohort)`
410
+ ```ruby
411
+ # Returns an array of instructors for the given year, course and cohort
412
+ def instructors(year, course, cohort)
413
+ # An instructor may be any object defined by the implementation
414
+ end
415
+ ```
416
+
417
+ ##### `row_data(data, year, course, cohort, instructors)`
418
+ ```ruby
419
+ # Populates the data array for a course element row in the Alma course
420
+ # loader CSV file. The data array is pre-allocated by the caller.
421
+ def row_data(data, year, course, cohort, instructors)
422
+ # The implementation must define the current course details
423
+ data[0] = 'Current-year-course-code'
424
+ # :
425
+ data[2] = 'Current-year-section-id'
426
+
427
+ # The implementation must define the previous year's course code/section
428
+ # These will be ignored by the Writer unless required for rollover
429
+ data[29] = 'Previous-year-course-code'
430
+ data[30] = 'Previous-year-section-id'
431
+ end
432
+ ```
433
+
434
+ ### Writer
435
+
436
+ The `Writer` class provides a single class-level method `write` which generates
437
+ an Alma course loader file given an appropriate `Reader`:
438
+
439
+ ```ruby
440
+ Writer.write(output_filename, course_loader_op, reader)
441
+ ```
442
+
443
+ The `course_loader_op` is the Alma course loader operation applied to all course
444
+ elements provided by the `reader`. This may be:
445
+ * `:delete` to delete the courses in the file
446
+ * `:rollover` to implement rollover to the courses defined by the file
447
+ * `:update` to update the courses in the file
448
+
449
+ ### Command Line Scripts
450
+
451
+ The `CLI::CourseLoader` class provides support for writing command-line course
452
+ loader scripts.
453
+
454
+ #### Extending CLI::CourseLoader
455
+
456
+ To implement a course loader command-line script, clients should subclass
457
+ `CLI::CourseLoader` and implement the following methods:
458
+
459
+ ##### `extractors`
460
+
461
+ This method defines the field extractors available to filter specifications.
462
+ It returns a `Hash` mapping symbols (extractor names) to `Proc` instances
463
+ responsible for extracting a single field of the course data. The hash keys are
464
+ the field names used in filter specifications.
465
+
466
+ Each `Proc` instance of the form:
467
+ ```ruby
468
+ proc { |year, course, cohort| # return some field value }
469
+ ```
470
+
471
+ The following example defines the fields `course` and `year` for use in filters:
472
+ ```ruby
473
+ # Field descriptions
474
+ def extractor_details
475
+ {
476
+ course: 'Course code',
477
+ year: 'Course year'
478
+ }.freeze
479
+ end
480
+
481
+ # Field definitions
482
+ def extractors
483
+ {
484
+ course: proc { |year, course, cohort| course.course_code },
485
+ year: proc { |year, course, cohort| year }
486
+ }.freeze
487
+ end
488
+ ```
489
+
490
+ ##### `reader`
491
+
492
+ This should return an instance of a subclass of `AlmaCourseLoader::Reader` which
493
+ returns courses from the course manager data source.
494
+
495
+ ##### `time_period(time_period_s)`
496
+
497
+ This method accepts a client-specific string representation of a time period
498
+ and returns an appropriate internal object representing that time period. For
499
+ example:
500
+
501
+ ```ruby
502
+ def time_period(time_period_s)
503
+ # Accept strings such as "2017-18" but internally work with integer years
504
+ time_period_s[0..3].to_i
505
+ end
506
+ ```
507
+
508
+ #### Command-Line Usage
509
+
510
+ Course loader scripts derived from `CLI::CourseLoader` accept the following
511
+ command-line options:
512
+
513
+ ```bash
514
+ course_loader [-d|--delete]
515
+ [-e|--env=env-file]
516
+ [-f|--filter=filter]...
517
+ [-F|--fields]
518
+ [-l|--log-file=log-file]
519
+ [-L|--log-level=debug|error|fatal|info|warn]
520
+ [-o|--out-file=output-file]
521
+ [-r|--rollover]
522
+ [-t|--time-period=time-period]...
523
+ ```
524
+
525
+ ##### `-d | --delete`
526
+
527
+ Adds the `DELETE` operation to the course loader file, causing all entries in
528
+ the file to be deleted when the file is processed by Alma.
529
+
530
+ ##### `-e env-file | --env=env-file`
531
+
532
+ Specifies a file of environment variable definitions for configuration.
533
+
534
+ ##### `-f filter | --filter=filter`
535
+
536
+ Specifies a filter restricting the courses to be exported. See *Filter
537
+ specification strings* for the filter syntax. This flag may be repeated to
538
+ specify multiple filters; a course must pass every filter to be included in the
539
+ export.
540
+
541
+ ##### `-F | --fields`
542
+
543
+ Lists the fields available to filters.
544
+
545
+ ##### `-h | --help`
546
+
547
+ Displays a help page for the command-line interface.
548
+
549
+ ##### `-l log-file | --log-file=log-file`
550
+
551
+ Specifies a file for logging course loader activity.
552
+
553
+ ##### `-L log-level | --log-level=log-level`
554
+
555
+ Specifies the logging level: `fatal|error|warn|info|debug`.
556
+
557
+ ##### `-o out-file | --out-file=out-file`
558
+
559
+ Specifies the output course loader file.
560
+
561
+ ##### `-r | --rollover`
562
+
563
+ Adds the `ROLLOVER` operation and previous course code/section to the course
564
+ loader file, triggering Alma's course rollover processing for the specified
565
+ courses.
566
+
567
+ ##### `-t time-period | --time-period=time-period`
568
+
569
+ Specifies the course time period covered by the export. This flag may be
570
+ repeated to specify multiple time periods.
571
+ The exact syntax and meaning of `time-period` is left to clients of this gem.
572
+
573
+ ## Development
574
+
575
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
576
+
577
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
578
+
579
+ ## Contributing
580
+
581
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lulibrary/alma_course_loader. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
582
+
583
+
584
+ ## License
585
+
586
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).