lusi_alma_course_loader 0.2.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2bb1e3748b483363c5f660babc78160130e15aaa
4
+ data.tar.gz: db8ba8021b5fc706e7917fa021b2996c42b32956
5
+ SHA512:
6
+ metadata.gz: 9fba3d46e754484a80f06ed7c60fa0eec77cedb957a022b5d0c977cb7531f4f656acdea76a60dfe48e6a0e8cfb81c64e39a60270a815bcb63734831cd7e1a32a
7
+ data.tar.gz: a099ae6638521d92e4b580e2de612fe14ea671438d7a2d2edc18019242ca93ccc94afa8f2db7317ac1e39c4e9516a86ef88709a8a35d8507f2127c9097de0012
data/.gitignore ADDED
@@ -0,0 +1,62 @@
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_delete.csv
62
+ /test/fixtures/diff_test_update.csv
data/.travis.yml ADDED
@@ -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,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in lusi_alma_course_loader.gemspec
4
+ gemspec
5
+
6
+ # Lancaster University LUSI API
7
+ gem 'lusi_api', '>=0.1.10', git: 'https://github.com/lulibrary/lusi_api'
8
+
9
+ # Redis-backed Hash
10
+ gem 'redis_hash', '>=0.1.0', git: 'https://github.com/lulibrary/redis_hash'
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,152 @@
1
+ # LUSIAlmaCourseLoader
2
+
3
+ This gem implements an Alma course loader which reads course data from Lancaster
4
+ University's LUSI student records system.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'lusi_alma_course_loader'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install lusi_alma_course_loader
21
+
22
+ ## Usage
23
+
24
+ The course loader is a command-line script:
25
+
26
+ ```bash
27
+ course_loader [-d|--delete]
28
+ [-e|--env=env-file]
29
+ [-f|--filter=filter]...
30
+ [-F]
31
+ [-l|--log-file=log-file]
32
+ [-L|--log-level=debug|error|fatal|info|warn]
33
+ [-o|--out-file=output-file]
34
+ [-p|--password=lusi-password]
35
+ [-r|--rollover]
36
+ [-t|--time-period=time-period]...
37
+ [-u|--user=lusi-username]
38
+ ```
39
+
40
+ ##### `-d | --delete`
41
+
42
+ Adds the `DELETE` operation to the course loader file, causing all entries in
43
+ the file to be deleted when the file is processed by Alma.
44
+
45
+ ##### `-e env-file | --env=env-file`
46
+
47
+ Specifies a file of environment variable definitions for configuration
48
+
49
+ ##### `-f filter | --filter=filter`
50
+
51
+ Specifies a filter restricting the courses to be exported. See *Filter
52
+ specification strings* for the filter syntax. This flag may be repeated to
53
+ specify multiple filters; a course must pass every filter to be included in the
54
+ export.
55
+
56
+ ##### `-F`
57
+
58
+ Lists the fields available to filters and exits.
59
+
60
+ ##### `-h | --help`
61
+
62
+ Displays a help page for the command-line interface.
63
+
64
+ ##### `-l log-file | --log-file=log-file`
65
+
66
+ Specifies a file for logging course loader activity
67
+
68
+ ##### `-L log-level | --log-level=log-level`
69
+
70
+ Specifies the logging level: `fatal|error|warn|info|debug`
71
+
72
+ ##### `-o out-file | --out-file=out-file`
73
+
74
+ Specifies the output course loader file
75
+
76
+ ##### `-p lusi-password | --password=lusi-password`
77
+
78
+ Specifies the password for the LUSI web service. If not specified, the value of
79
+ the `LUSI_API_PASSWORD` environment variable is used.
80
+
81
+ ##### `-r | --rollover`
82
+
83
+ Adds the `ROLLOVER` operation and previous course code/section to the course
84
+ loader file, triggering Alma's course rollover processing for the specified
85
+ courses.
86
+
87
+ ##### `-t time-period | --time-period=time-period`
88
+
89
+ Specifies the course time period covered by the export. This flag may be
90
+ repeated to specify multiple time periods.
91
+ The exact syntax and meaning of `time-period` is left to clients of this gem.
92
+
93
+ ##### `-u lusi-username | --user=lusi-username`
94
+
95
+ Specifies the username for the LUSI web service. If not specified, the value of
96
+ the `LUSI_API_USER` environment variable is used.
97
+
98
+ ### Filter specification strings
99
+
100
+ The general form of a filter specification string is:
101
+
102
+ ```[!][field [op ]]value```
103
+
104
+ where:
105
+ * `!` negates the condition
106
+ * `field` is the name of a defined field extractor,
107
+ * `op` is one of the following operators:
108
+ * `<`, `<=`, `==`, `!=`, `>=`, `>` the value of field is less than (etc.)
109
+ value
110
+ * `~`, `!~` the value of field matches/does not match the regular expression
111
+ value
112
+ * `in` the value of field is a key (if value is a hash) or a value (if value
113
+ is any other type) in value; equivalent to value.include?(field)
114
+ * `keyin` the value of field is a key of the value hash; equivalent
115
+ to value.key?(field)
116
+ * `valuein` the value of field is a value in the value hash; equivalent to
117
+ value.value?(field)
118
+ * `value` is either a JSON string (which must include double-quotes around string
119
+ literal values and may specify arrays and hashes) or a regular expression
120
+ delimited by `/`.
121
+
122
+ Examples:
123
+
124
+ ```ruby
125
+ # Course code must exactly match CS101
126
+ course_code == "CS101"
127
+
128
+ # Course code must be one of CS101, CS102 or CS103
129
+ course_code in ["CS101", "CS102", "CS103"]
130
+
131
+ # Year must not be 2015 or 2016
132
+ ! year in [2015, 2016]
133
+
134
+ # Course code must begin with CS
135
+ course_code ~ /^CS/
136
+ ```
137
+
138
+ ## Development
139
+
140
+ 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.
141
+
142
+ 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).
143
+
144
+ ## Contributing
145
+
146
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/lusi_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.
147
+
148
+
149
+ ## License
150
+
151
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
152
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "lusi_alma_course_loader"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/course_loader ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'lusi_alma_course_loader'
5
+
6
+ dsn = ENV['SENTRY_DSN']
7
+ unless dsn.nil? || dsn.empty?
8
+ require 'raven'
9
+ Raven.configure { |config| config.dsn = dsn }
10
+ end
11
+ LUSIAlmaCourseLoader::CLI::CourseLoader.run
@@ -0,0 +1,11 @@
1
+ require 'lusi_api/calendar'
2
+ require 'lusi_api/core/api'
3
+ require 'lusi_api/core/lookup'
4
+ require 'lusi_api/core/util'
5
+ require 'lusi_api/course'
6
+ require 'lusi_api/organisation'
7
+
8
+ require 'alma_course_loader/writer'
9
+
10
+ require 'lusi_alma_course_loader/reader'
11
+ require 'lusi_alma_course_loader/version'
@@ -0,0 +1,138 @@
1
+ require 'date'
2
+
3
+ require 'lusi_api/core/api'
4
+
5
+ require 'alma_course_loader/cli/course_loader'
6
+
7
+ require 'lusi_alma_course_loader/reader'
8
+
9
+ module LUSIAlmaCourseLoader
10
+ module CLI
11
+ # Field extractors for LUSI course objects
12
+ module Extractors
13
+ # Convert
14
+ def self.date_to_s(date)
15
+ date.strftime('%Y-%m-%dT%H:%M:%S')
16
+ end
17
+
18
+ # Named field value extractors
19
+ EXTRACTORS = {
20
+ accessdate: proc do |_year, course, _cohort|
21
+ date_to_s(course.student_access_date)
22
+ end,
23
+ accessdateutc: proc do |_year, course, _cohort|
24
+ date_to_s(course.student_access_date_utc)
25
+ end,
26
+ conv: proc do |_year, course, _cohort|
27
+ course.is_conversion_space.to_s
28
+ end,
29
+ copy: proc do |_year, course, _cohort|
30
+ course.copy_course_materials_from_prev_session.to_s
31
+ end,
32
+ coursetype: proc { |_year, course, _cohort| course.course_type },
33
+ createspacedate: proc do |_year, course, _cohort|
34
+ date_to_s(course.create_space_date)
35
+ end,
36
+ createspacedateutc: proc do |_year, course, _cohort|
37
+ date_to_s(course.create_space_date_utc)
38
+ end,
39
+ enddate: proc do |_year, _course, cohort|
40
+ date_to_s(cohort.end_date)
41
+ end,
42
+ enddateutc: proc do |_year, _course, cohort|
43
+ date_to_s(cohort.end_date_utc)
44
+ end,
45
+ hide: proc do |_year, course, _cohort|
46
+ course.hide_content_on_rollover.to_s
47
+ end,
48
+ longtitle: proc { |_year, course, _cohort| course.display_long_title },
49
+ shorttitle: proc do |_year, course, _cohort|
50
+ course.display_short_title
51
+ end,
52
+ spacetype: proc { |_year, course, _cohort| course.space_type },
53
+ startdate: proc do |_year, _course, cohort|
54
+ date_to_s(cohort.start_date)
55
+ end,
56
+ startdateutc: proc do |_year, _course, cohort|
57
+ date_to_s(cohort.start_date_utc)
58
+ end,
59
+ status: proc { |_year, course, _cohort| course.status },
60
+ vlespaceid: proc { |_year, course, _cohort| course.lusi_vle_space_id },
61
+ year: proc { |_year, course, _cohort| course.lusi_year_id }
62
+ }.freeze
63
+
64
+ EXTRACTOR_DETAILS = {
65
+ accessdate: 'start date of student access (local time)',
66
+ accessdateutc: 'start date of student access (UTC)',
67
+ conv: '"true" for conversion spaces, else "false"',
68
+ copy: '"true" if copied from previous space, else "false"',
69
+ coursetype: 'CMOD=course module, SOS=scheme of study',
70
+ createdate: 'creation date of VLE space (local time)',
71
+ createdateutc: 'creation date of VLE space (UTC)',
72
+ enddate: 'end of course date (local time)',
73
+ enddateutc: 'end of course date (UTC)',
74
+ hide: '"true" if content hidden on rollover, else "false"',
75
+ longtitle: 'VLE space long title',
76
+ shorttitle: 'VLE space short title',
77
+ spacetype: 'SHARED=shared VLE space, SINGLE=single VLE space',
78
+ startdate: 'start of course date (local time)',
79
+ startdateutc: 'start of course date (UTC)',
80
+ vlespaceid: 'LUSI VLE space identity',
81
+ year: 'LUSI year identity'
82
+ }.freeze
83
+ EXTRACTOR_DETAILS.each_value(&:freeze)
84
+ end
85
+
86
+ # Implements the course loader command-line interface
87
+ class CourseLoader < ::AlmaCourseLoader::CLI::CourseLoader
88
+ include Extractors
89
+
90
+ option %w[-p --password], 'LUSI_PASSWORD', 'the LUSI web service password'
91
+ option %w[-u --user], 'LUSI_USER', 'the LUSI web service username'
92
+
93
+ # Returns a hash of named field value extractor details
94
+ # @return [Hash<String|Symbol, String>] the field extractor details
95
+ def extractor_details
96
+ EXTRACTOR_DETAILS
97
+ end
98
+
99
+ # Returns a hash of named field value extractors
100
+ # @return [Hash<String|Symbol, Method|Proc>] the field extractors
101
+ def extractors
102
+ EXTRACTORS
103
+ end
104
+
105
+ # Creates a Reader instance to retrieve course data
106
+ # @return [LUSIAlmaCourseLoader::Reader] the Reader instance
107
+ def reader
108
+ return @reader unless @reader.nil?
109
+ api = lusi_api
110
+ lookup = lusi_lookup(api)
111
+ @reader = ::LUSIAlmaCourseLoader::Reader.new(
112
+ api, lookup, *time_period_list,
113
+ current_year: current_time_period,
114
+ filters: filter_list
115
+ )
116
+ end
117
+
118
+ # Creates a LUSI API instance
119
+ # @return [LUSI::API::Core::API] the LUSI API instance
120
+ def lusi_api
121
+ api_password = password || ENV['LUSI_API_PASSWORD']
122
+ api_user = user || ENV['LUSI_API_USER']
123
+ ::LUSI::API::Core::API.new(api_user: api_user,
124
+ api_password: api_password,
125
+ logger: logger)
126
+ end
127
+
128
+ # Creates a LUSI LookupService instance
129
+ # @param api [LUSI::API::Core::API] the LUSI API instance
130
+ # @return [LUSI::API::Core::Lookup::LookupService] the LUSI lookup service
131
+ def lusi_lookup(api)
132
+ lookup = LUSI::API::Core::Lookup::LookupService.new(api || lusi_api)
133
+ lookup.load
134
+ lookup
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,314 @@
1
+ require 'alma_course_loader/reader'
2
+ require 'lusi_api/core/util'
3
+ require 'lusi_api/course'
4
+ require 'lusi_api/vle'
5
+
6
+ module LUSIAlmaCourseLoader
7
+ # Methods for handling LUSI data
8
+ module Helpers
9
+ include LUSI::API::Core::Util
10
+
11
+ # Course ID year offsets
12
+ COURSE_ID_OFFSETS = {
13
+ next: 1,
14
+ previous: -1,
15
+ rollover: -1
16
+ }.freeze
17
+
18
+ # LUSI instructor roles
19
+ INSTRUCTOR_ROLES = {
20
+ 'administrative staff'.to_sym => true,
21
+ 'co-course convenor'.to_sym => true,
22
+ 'course convenor'.to_sym => true,
23
+ 'partner administrative staff'.to_sym => true,
24
+ 'partner course convenor'.to_sym => true,
25
+ 'teaching staff (not timetabled)'.to_sym => true,
26
+ 'teaching staff (timetabled)'.to_sym => true,
27
+ }.freeze
28
+
29
+ # Returns the major department for the course
30
+ # @param course [LUSI::API::Course::Module] the LUSI course module
31
+ # @return [LUSI::API::Course::CourseDepartment] the major department
32
+ def course_department(course)
33
+ return nil unless course && course.course_departments
34
+ # There should only be one major department
35
+ course.course_departments.select(&:is_major_department)[0]
36
+ end
37
+
38
+ # Returns the Moodle course ID for a given course/cohort
39
+ # @param course [LUSI::API::VLE::VLESpace] the course
40
+ # @param cohort [LUSI::API::Course::Cohort] the course cohort
41
+ # @param offset [Integer, Symbol] a numerical year offset or symbol
42
+ # specifying the number of years from the current course year for which
43
+ # the ID is required. This allows generation of course IDs for years
44
+ # other than the specified course.
45
+ # The following symbols are shorthand for numeric offsets:
46
+ # :previous, :rollover - equivalent to offset: -1
47
+ # :next - equivalent to offset: 1
48
+ # @return [String] the Moodle course ID
49
+ def course_id(course, cohort, offset = nil)
50
+ offset = COURSE_ID_OFFSETS[offset] || offset.to_i
51
+ year = if offset != 0
52
+ # Get the year identity
53
+ lusi_year_identity(course.lusi_year_id, offset: offset)
54
+ else
55
+ course.lusi_year_id
56
+ end
57
+ if course.space_type == 'SHARED'
58
+ "lusi-#{course.lusi_vle_space_id}-#{year}"
59
+ else
60
+ c = course.vle_space_courses[0]
61
+ "lusi-#{c.identity.course}-#{year}-#{c.identity.cohort}"
62
+ end
63
+ end
64
+
65
+ # Returns the organisation unit for a course department
66
+ # @param course [LUSI::API::Course::Module] the LUSI course module
67
+ # @return [LUSI::API::Organisation::Unit] the organisation unit instance of
68
+ # the major department for the course
69
+ def department(course = nil)
70
+ dept = course_department(course)
71
+ dept ? dept.org_unit : nil
72
+ end
73
+
74
+ # Returns the organisation units from LUSI
75
+ def departments
76
+ return @departments unless @departments.nil?
77
+ organisation = LUSI::API::Organisation::Organisation.new
78
+ organisation.load(@lusi_api, in_use_only: false)
79
+ @departments = {}
80
+ organisation.each(:department) { |o| @departments[o.talis_code] = o }
81
+ @departments
82
+ end
83
+
84
+ # Returns a string row field trimmed to the specified length
85
+ # @param value [String] the field value
86
+ # @param length [Integer] the maximum field length
87
+ # @param empty [Object, nil] the value to return if the field value is nil
88
+ # or empty
89
+ # @return [String] the formatted field value
90
+ def field(value, length = nil, empty = nil)
91
+ value = empty if value.nil? || value.empty?
92
+ length && value ? value[0...length] : value
93
+ end
94
+
95
+ # Returns a list of instructor enrolments for the course/cohort
96
+ # @param year [LUSI::API::Calendar::Year] the course year
97
+ # @param course [LUSI::API::VLE::VLESpace] the course
98
+ # @param cohort [LUSI::API::Course::Cohort] the course cohort
99
+ # @return [Array<LUSI::API::Course::StaffModuleEnrolment>] the course
100
+ # instructors
101
+ def instructor_enrolments(year, course, cohort)
102
+ result = []
103
+ course.vle_space_courses.each do |c|
104
+ enrolments = LUSI::API::Course::StaffModuleEnrolment.get_instance(
105
+ @lusi_api, @lusi_lookup,
106
+ active_vle_space_only: false,
107
+ course_identity: c.identity.course,
108
+ cohort_identity: c.identity.cohort,
109
+ require_username: true,
110
+ year_identity: c.identity.year
111
+ )
112
+ e = enrolments.select do |e|
113
+ @instructor_roles.key?(e.enrolment_role.identity)
114
+ end
115
+ result.concat(e)
116
+ end
117
+ result
118
+ end
119
+
120
+ # Returns course instructor roles from the :staff_course_roles lookup table
121
+ # @return [Hash<String, LUSI::API::Person::StaffCourseRole>] the roles
122
+ def instructor_roles
123
+ roles = @lusi_lookup.service(:staff_course_role)
124
+ return [] if roles.nil?
125
+ roles.select do |_k, v|
126
+ INSTRUCTOR_ROLES.include?(v.description.downcase.to_sym)
127
+ end
128
+ end
129
+
130
+ # Returns a comma-separated list of instructor usernames
131
+ # @param usernames [Array<String>] the instructor usernames
132
+ # @return [String, nil] the comma-separated list of instructor usernames
133
+ def instructor_usernames(usernames)
134
+ result = usernames.join(',')
135
+ result.nil? || result.empty? ? nil : result
136
+ end
137
+
138
+ # Returns the ancestor organisation units of the course's major department
139
+ # @param course [LUSI::API::Course::Module] the LUSI course module
140
+ # @return [Array<LUSI::API::Organisation::Unit>] the organisation units in
141
+ # furthest-to-nearest order [institution, faculty, dept]
142
+ def org_units(course = nil)
143
+ dept = department(course)
144
+ result = []
145
+ until dept.nil?
146
+ result.unshift(dept)
147
+ dept = dept.parent
148
+ end
149
+ result
150
+ end
151
+ end
152
+
153
+ # Reads course information from LUSI
154
+ class Reader < AlmaCourseLoader::Reader
155
+ include Helpers
156
+
157
+ # Alma processing department code
158
+ PROCESSING_DEPARTMENT = 'Library'.freeze
159
+
160
+ # Alma term codes
161
+ TERMS = {
162
+ lusi_lent: 'LENT',
163
+ lusi_mich: 'MICHAELMAS',
164
+ lusi_summ: 'SUMMER'
165
+ }.freeze
166
+
167
+ # Initializes a new Reader instance
168
+ # @param lusi_api [LUSI::API::Core::API] the LUSI API instance
169
+ # @param lusi_lookup [LUSI::API::Core::Lookup::LookupService] the LUSI
170
+ # lookup service
171
+ # @return [void]
172
+ def initialize(lusi_api = nil, lusi_lookup = nil, *years, **kwargs)
173
+ # Set the LUSI API and lookup properties for use with subsequent methods
174
+ @lusi_api = lusi_api
175
+ @lusi_lookup = lusi_lookup
176
+ super(*years, **kwargs)
177
+ @departments = departments
178
+ @instructor_roles = instructor_roles
179
+ end
180
+
181
+ # Returns a list of LUSI year instances
182
+ # @param year [LUSI::API::Calendar::Year] the base year (defaults to the
183
+ # current academic year)
184
+ # @param years_previous [Integer] the number of years preceding the base
185
+ # year
186
+ # @param years_next [Integer] the number of years following the base year
187
+ def years(year = nil, years_previous: nil, years_next: nil)
188
+ years_next = years_next.to_i
189
+ years_previous = years_previous.to_i
190
+ result = []
191
+ result.push(*(year.previous(years_previous))) if years_previous > 0
192
+ result.push(*(year.next(years_next))) if years_next > 0
193
+ result
194
+ end
195
+
196
+ protected
197
+
198
+ # Returns a list of courses for the specified year
199
+ # @param year [Integer, String, LUSI::API::Calendar::Year] the course year
200
+ # @return [Array<LUSI::API::VLE::VLESpace>] the courses for the year
201
+ def courses(year)
202
+ year = year_identity(year)
203
+ LUSI::API::VLE::VLESpace.get_instance(@lusi_api, @lusi_lookup,
204
+ active_vle_space_only: false,
205
+ year_identity: year)
206
+ end
207
+
208
+ # Returns a list of cohorts for the specified course
209
+ # @param year [Integer, String, LUSI::API::Calendar::Year] the course year
210
+ # @param course [LUSI::API::Course::Module] the course
211
+ # @return [Array<LUSI::API::Course::Cohort>] the cohorts for the course
212
+ def course_cohorts(year, course)
213
+ # A single VLE space is always associated with a single course cohort
214
+ nil
215
+ end
216
+
217
+ # Returns the current academic year
218
+ # @return [LUSI::API::Calendar::Year] the current academic year
219
+ def current_academic_year
220
+ LUSI::API::Calendar::Year.get_current_academic_year(@lusi_api,
221
+ @lusi_lookup)
222
+ end
223
+
224
+ # Returns a list of instructor usernames for the course/cohort
225
+ # @param year [LUSI::API::Calendar::Year] the course year
226
+ # @param course [LUSI::API::Course::Module] the course
227
+ # @param cohort [LUSI::API::Course::Cohort] the course cohort
228
+ # @return [Array<String>] the course instructors
229
+ def instructors(year, course, cohort)
230
+ enrolments = instructor_enrolments(year, course, cohort)
231
+ result = enrolments.map(&:username)
232
+ result.uniq!
233
+ result
234
+ end
235
+
236
+ # Populates the CSV row (array) for a specific course/cohort
237
+ # @param data [Array<String>] the CSV row (array)
238
+ # @param year [LUSI::API::Calendar::Year] the course year
239
+ # @param course [LUSI::API::Course::Module] the course
240
+ # @param cohort [LUSI::API::Course::Cohort] the course cohort
241
+ # @param instructors [Array<LUSI::API::Course::StaffModuleEnrolment>] the
242
+ # course instructors
243
+ def row_data(data, year, course, cohort, instructors)
244
+ # The code of the major (administering) department for the course.
245
+ # Department codes are the talis_code of LUSI::API::Organisation::Unit
246
+ institution, faculty, department = org_units(course).map(&:talis_code)
247
+ # Course title
248
+ data[1] = course.display_long_title
249
+ # Term 1-4 (data[5..8]) are left empty
250
+ # Number of participants (data[11]) is left empty
251
+ # Weekly hours (data[12]) is left empty
252
+ # Instructor 1-10 (data[17..26]) are left empty
253
+ # All instructors
254
+ data[27] = instructor_usernames(instructors)
255
+ # Operation (data[28]) is left empty
256
+ row_data_course_id(data, course, cohort)
257
+ row_data_dates(data, course, cohort)
258
+ row_data_departments(data, department)
259
+ row_data_ids(data, course, cohort, institution, faculty)
260
+ row_data_rollover(data, course, cohort)
261
+ end
262
+
263
+ def row_data_course_id(data, course, cohort)
264
+ # Course code
265
+ data[0] = course_id(course, cohort)
266
+ # Section ID - not currently used, set a default value for all courses
267
+ data[2] = '1'
268
+ end
269
+
270
+ def row_data_dates(data, course, cohort)
271
+ # Start date
272
+ data[9] = course.start_date_utc ? course.start_date_utc.strftime('%Y-%m-%d') : nil
273
+ # End date
274
+ data[10] = course.end_date_utc ? course.end_date_utc.strftime('%Y-%m-%d') : nil
275
+ # Year
276
+ data[13] = lusi_year(course.lusi_year_id)
277
+ end
278
+
279
+ def row_data_departments(data, department)
280
+ # Academic departments
281
+ data[3] = department
282
+ # Processing department
283
+ data[4] = PROCESSING_DEPARTMENT
284
+ end
285
+
286
+ def row_data_ids(data, course, cohort, institution, faculty)
287
+ # Searchable ID 1
288
+ # - get the course from the short title {course}-{cohort}-{year}
289
+ title = course.display_short_title
290
+ data[14] = if title && title.count('-') > 1
291
+ title.split('-')[0...-2].join('-')
292
+ else
293
+ title
294
+ end
295
+ # Searchable ID 2
296
+ data[15] = faculty
297
+ # Searchable ID 3
298
+ data[16] = institution
299
+ end
300
+
301
+ def row_data_rollover(data, course, cohort)
302
+ # Rollover course code
303
+ data[29] = course_id(course, cohort, :rollover)
304
+ # Rollover course section - use the current section ID
305
+ data[30] = data[2]
306
+ end
307
+
308
+ # Returns the LUSI year identity code for the specified year. If year is
309
+ # not specified, returns the identity of the current academic year
310
+ def year_identity(year = nil)
311
+ year ? lusi_year_identity(year) : current_academic_year.identity
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,3 @@
1
+ module LUSIAlmaCourseLoader
2
+ VERSION = '0.2.0'.freeze
3
+ end
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'lusi_alma_course_loader/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'lusi_alma_course_loader'
9
+ spec.version = LUSIAlmaCourseLoader::VERSION
10
+ spec.authors = ['Lancaster University Library']
11
+ spec.email = ['library.dit@lancaster.ac.uk']
12
+
13
+ spec.summary = 'Generates Alma course loader files from LUSI'
14
+ spec.description = 'This gem provides a script to generate Alma course ' \
15
+ "loader files from Lancaster University's LUSI student" \
16
+ 'records system.'
17
+ spec.homepage = 'https://github.com/lulibrary/lusi_alma_course_loader'
18
+ spec.license = 'MIT'
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+ spec.bindir = 'exe'
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_dependency 'clamp'
27
+ spec.add_dependency 'alma_course_loader', '~> 0.9.1'
28
+
29
+ spec.add_development_dependency 'bundler', '~> 1.14'
30
+ spec.add_development_dependency 'dotenv'
31
+ spec.add_development_dependency 'rake', '~> 10.0'
32
+ spec.add_development_dependency 'minitest', '~> 5.0'
33
+ spec.add_development_dependency 'minitest-hooks'
34
+ spec.add_development_dependency 'minitest-reporters'
35
+ spec.add_development_dependency 'rubocop'
36
+ spec.add_development_dependency 'sentry-raven'
37
+ end
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lusi_alma_course_loader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Lancaster University Library
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-09-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: clamp
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: alma_course_loader
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.9.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.9.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotenv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '5.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '5.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest-hooks
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest-reporters
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sentry-raven
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: This gem provides a script to generate Alma course loader files from
154
+ Lancaster University's LUSI studentrecords system.
155
+ email:
156
+ - library.dit@lancaster.ac.uk
157
+ executables:
158
+ - course_loader
159
+ extensions: []
160
+ extra_rdoc_files: []
161
+ files:
162
+ - ".gitignore"
163
+ - ".travis.yml"
164
+ - CODE_OF_CONDUCT.md
165
+ - Gemfile
166
+ - LICENSE.txt
167
+ - README.md
168
+ - Rakefile
169
+ - bin/console
170
+ - bin/setup
171
+ - exe/course_loader
172
+ - lib/lusi_alma_course_loader.rb
173
+ - lib/lusi_alma_course_loader/cli/course_loader.rb
174
+ - lib/lusi_alma_course_loader/reader.rb
175
+ - lib/lusi_alma_course_loader/version.rb
176
+ - lusi_alma_course_loader.gemspec
177
+ homepage: https://github.com/lulibrary/lusi_alma_course_loader
178
+ licenses:
179
+ - MIT
180
+ metadata: {}
181
+ post_install_message:
182
+ rdoc_options: []
183
+ require_paths:
184
+ - lib
185
+ required_ruby_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ requirements: []
196
+ rubyforge_project:
197
+ rubygems_version: 2.6.11
198
+ signing_key:
199
+ specification_version: 4
200
+ summary: Generates Alma course loader files from LUSI
201
+ test_files: []