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
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -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
|
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|