lusi_api 0.1.11

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,247 @@
1
+ require 'lusi_api/core/api'
2
+ require 'lusi_api/core/code'
3
+ require 'lusi_api/core/xml'
4
+
5
+
6
+ module LUSI
7
+ module API
8
+ module Enrolment
9
+
10
+ # Represents an enrolment role in the LUSI API
11
+ class EnrolmentRole < LUSI::API::Core::Code
12
+
13
+ # @!attribute [rw] vle_role_description
14
+ # @return [String, nil] the VLE role description a member has against this enrolment
15
+ attr_accessor :vle_role_description
16
+
17
+ # Initialises a new EnrolmentRole instance
18
+ # @param (see LUSI::API::Core::Code#initialize)
19
+ # @param vle_role_description [String, nil] the default VLE role description
20
+ # @return [void]
21
+ def initialize(xml = nil, lookup = nil, vle_role_description: nil, **kwargs)
22
+ super(xml, lookup, **kwargs)
23
+ @vle_role_description = LUSI::API::Core::XML.xml_content_at(xml, 'xmlns:VLERoleDescription',
24
+ vle_role_description)
25
+ end
26
+
27
+ end
28
+
29
+
30
+ # The abstract base class for enrolment classes
31
+ # @abstract Subclasses must define the lusi_ws_* endpoint methods and additional attributes
32
+ class EnrolmentBase
33
+
34
+ extend LUSI::API::Core::Endpoint
35
+
36
+ # @!attribute [rw] enrolment_role
37
+ # @return [LUSI::API::Enrolment::EnrolmentRole, nil] the role that members have against this enrolment
38
+ attr_accessor :enrolment_role
39
+
40
+ # @!attribute [rw] identity
41
+ # @return [LUSI::API::Enrolment::EnrolmentIdentity] the identity of the subject (person) of the enrolment
42
+ attr_accessor :identity
43
+
44
+ # @!attribute [rw] is_current_enrolment
45
+ # @return [Boolean, nil] true if the enrolment is current, false otherwise
46
+ attr_accessor :is_current_enrolment
47
+
48
+ # @!attribute [rw] is_current_identity
49
+ # @return [Boolean, nil] true if the member's identity is current, false otherwise
50
+ attr_accessor :is_current_identity
51
+
52
+ # @!attribute [rw] username
53
+ # @return [String, nil] the username of the subject (person) of the enrolment
54
+ attr_accessor :username
55
+
56
+ # Initalises a new Enrolment instance
57
+ # @param xml [Nokogiri::XML::Document, Nokogiri::XML::Node] the parsed XML root of the enrolment
58
+ # @param lookup [LUSI::API::Core::Lookup::LookupService, nil] the lookup service for object resolution
59
+ # @param enrolment_role [LUSI::API::Enrolment::EnrolmentRole, nil] the default enrolment role
60
+ # @param identity [String, nil] the default user identity code
61
+ # @param is_current_enrolment [Boolean, nil] the default current enrolment flag
62
+ # @param is_current_identity [Boolean, nil] the default current identity flag
63
+ # @param username [String, nil] the default username
64
+ # @return [void]
65
+ def initialize(xml = nil, lookup = nil, enrolment_role: nil, identity: nil, is_current_enrolment: nil,
66
+ is_current_identity: nil, username: nil)
67
+ is_current_enrolment = is_current_enrolment.nil? || is_current_enrolment ? true : false
68
+ is_current_identity = is_current_identity.nil? || is_current_identity ? true : false
69
+ @enrolment_role = EnrolmentRole.new(LUSI::API::Core::XML.xml_at(xml, 'xmlns:EnrolmentRole', enrolment_role),
70
+ lookup)
71
+ @identity = LUSI::API::Core::XML.xml_content_at(xml, 'xmlns:Identity', identity)
72
+ @is_current_enrolment = LUSI::API::Core::XML.xml_boolean_at(xml, 'xmlns:IsCurrentEnrolment',
73
+ is_current_enrolment)
74
+ @is_current_identity = LUSI::API::Core::XML.xml_boolean_at(xml, 'xmlns:IsCurrentIdentity',
75
+ is_current_identity)
76
+ @username = LUSI::API::Core::XML.xml_content_at(xml, 'xmlns:Username', username)
77
+ end
78
+
79
+ # Returns an array of instances matching the specified search criteria
80
+ # @param api [LUSI::API::Core::API] the LUSI API instance to use for searching
81
+ # @param lookup [LUSI::API::Core::Lookup::LookupService, nil] the lookup service for object resolution
82
+ # @param (see #get_instance_params)
83
+ # @yield [obj] Passes the instance to the block
84
+ def self.get_instance(api, lookup = nil, current_only: nil, **kwargs)
85
+ current_only = current_only.nil? || current_only ? true : false
86
+ if current_only
87
+ # Filter nodes which have current enrolments and user identities
88
+ filter = Proc.new do |node|
89
+ LUSI::API::Core::XML.xml_boolean_at(node, 'xmlns:IsCurrentEnrolment', false) &&
90
+ LUSI::API::Core::XML.xml_boolean_at(node, 'xmlns:IsCurrentIdentity', false)
91
+ end
92
+ else
93
+ filter = nil
94
+ end
95
+ params = self.get_instance_params(**kwargs)
96
+ xml = api.call(self.lusi_ws_path, self.lusi_ws_endpoint, self.lusi_ws_method, **params)
97
+ result = LUSI::API::Core::XML.xml(xml, "xmlns:#{self.lusi_ws_xml_root}", filter: filter) do |m|
98
+ obj = self.new(m, lookup)
99
+ yield(obj) if block_given?
100
+ obj
101
+ end
102
+ # Return the array of instances
103
+ result
104
+ end
105
+
106
+ # Returns the lookup indices supported by this enrolment type
107
+ # @return [Array<Symbol>] the supported lookup indices
108
+ def lookup_indices
109
+ [:identity, :role, :username]
110
+ end
111
+
112
+ # Returns the value to be used as a key for the specified lookup index
113
+ # @return [Object] the key value
114
+ def lookup_key(index = nil)
115
+ case index
116
+ when :identity
117
+ self.identity
118
+ when :role
119
+ self.enrolment_role ? self.enrolment_role.identity : nil
120
+ when :username
121
+ self.username
122
+ else
123
+ nil
124
+ end
125
+ end
126
+
127
+ # @see (LUSI::API::Core::Endpoint#lusi_ws_path)
128
+ def self.lusi_ws_path
129
+ 'UserDetails'
130
+ end
131
+
132
+ protected
133
+
134
+ # Returns a hash of parameters for the LUSI API call. Subclasses may extend or override this method.
135
+ # @param active_vle_space_only [Boolean, nil] if true, restrict results to active VLE spaces only
136
+ # @param asp_identity [String, nil] return instances matching the academically significant period (ASP) identity
137
+ # @param cohort_identity [String, nil] return instances matching the cohort identity
138
+ # @param course_identity [String, nil] return instances matching the course identity
139
+ # @param current_student_only [Boolean, nil] if true, restrict results to current students only
140
+ # @param department_identity [String, nil] return instances matching the department identity
141
+ # @param require_username [Boolean, nil] if true, include the student username, ignore students with no username
142
+ # @param year_identity (String, nil) return instances matching the year identity
143
+ # @return [Hash<String, String>] the parameters for the LUSI API call
144
+ def self.get_instance_params(**kwargs)
145
+ result = {
146
+ ActiveVLESpaceOnly: kwargs.fetch(:active_vle_space_only, true) ? 'true' : 'false',
147
+ ASPIdentity: kwargs.fetch(:asp_identity, ''),
148
+ CohortIdentity: kwargs.fetch(:cohort_identity, ''),
149
+ CourseIdentity: kwargs.fetch(:course_identity, ''),
150
+ DepartmentIdentity: kwargs.fetch(:department_identity, ''),
151
+ RequireUsername: kwargs.fetch(:require_username, true) ? 'true' : 'false',
152
+ YearIdentity: kwargs.fetch(:year_identity, '')
153
+ }
154
+ if self.lusi_ws_endpoint == 'Student.asmx'
155
+ result[:CurrentStudentOnly] = kwargs.fetch(:current_student_only, true) ? 'true' : 'false'
156
+ end
157
+ result
158
+ end
159
+
160
+ end
161
+
162
+
163
+ class EnrolmentLookup
164
+
165
+ # Initialises a new EnrolmentLookup instance
166
+ def initialize(enrolments = nil, *indices, &block)
167
+ @default_proc = block
168
+ @indices = {}
169
+ enrolments.each { |e| add(e, *indices) }
170
+ end
171
+
172
+ # @see (LUSI::API::Enrolment::EnrolmentLookup#fetch)
173
+ def [](key, *indices)
174
+ fetch(key, *indices)
175
+ end
176
+
177
+ # Adds an enrolment to specified indices (default is all indices if unspecified)
178
+ # @param enrolment [LUSI::API::Enrolment::EnrolmentBase] the enrolment to add
179
+ # Reminaing positional parameters specify the indices to update
180
+ # @return [void]
181
+ def add(enrolment = nil, *indices)
182
+ indices = enrolment.lookup_indices if indices.nil? || indices.empty?
183
+ indices.each { |index| add_enrolment(enrolment, index) }
184
+ nil
185
+ end
186
+
187
+ # Searches the specified indices for the lookup key and returns the first match
188
+ # @param key [Object] the lookup key. If the key defines method #enrolment_lookup_keys, this method determines
189
+ # which keys are searched for.
190
+ # Remaining positional parameters specify the indices to search. If no indices are specified, but the key
191
+ # instance defines method #enrolment_lookup_indices, this method determines which indices are searched.
192
+ # @return [Array<LUSI::API::Enrolment::EnrolmentBase>, nil] the enrolments corresponding to key, or nil
193
+ # if match was found
194
+ def fetch(key = nil, *indices)
195
+
196
+ # If no index is specified, infer it from the key type if possible
197
+ if indices.nil? || indices.empty?
198
+ indices = key.respond_to?(:enrolment_lookup_indices) ? key.enrolment_lookup_indices : nil
199
+ end
200
+ return nil if indices.nil? || indices.empty?
201
+
202
+ # Use the lookup keys specified by the key instance if possible, otherwise use the literal key value
203
+ keys = key.respond_to?(:enrolment_lookup_keys) ? key.enrolment_lookup_keys : [key]
204
+
205
+ # Search the specified indices until a match is found, then return matches for all key values from this index
206
+ unless keys.nil? || keys.empty?
207
+ result = []
208
+ indices.each do |index|
209
+ i = @indices[index]
210
+ if i
211
+ keys.each { |key| result += i[key] || [] }
212
+ end
213
+ return result unless result.empty?
214
+ end
215
+ end
216
+
217
+ # If we get here, the search failed in all indices - call the default_proc if available, otherwise return nil
218
+ @default_proc ? @default_proc.call(key) : nil
219
+
220
+ end
221
+
222
+ protected
223
+
224
+ # Adds an enrolment to the specified index
225
+ # @param enrolment [LUSI::API::Enrolment::EnrolmentBase] the enrolment
226
+ # @param index [Symbol] the index to be updated
227
+ def add_enrolment(enrolment, index)
228
+ # Get the specified index hash
229
+ @indices[index] = {} unless @indices.include?(index)
230
+ hash = @indices[index]
231
+ # Add the enrolment to the list
232
+ key = enrolment.lookup_key(index)
233
+ if hash.include?(key)
234
+ hash[key].push(enrolment)
235
+ else
236
+ hash[key] = [enrolment]
237
+ end
238
+ # Return the list containing the enrolment
239
+ hash[key]
240
+ end
241
+
242
+ end
243
+
244
+
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,291 @@
1
+ require 'lusi_api/core/xml'
2
+
3
+
4
+ module LUSI
5
+ module API
6
+
7
+ # Classes representation organisation structure
8
+ module Organisation
9
+
10
+ # Represents an organisation as a hierarchy of Units.
11
+ #
12
+ # The position in the hierarchy is defined by the Unit.type attribute.
13
+ #
14
+ # For each hierarchy level (Unit type) the Organisation maintains three indices, identified as follows:
15
+ # :identity allows retrieval by the Unit.identity attribute
16
+ # :mnemonic allows retrieval by the Unit.mnemonic attribute
17
+ # :title allows retrieval by the Unit.title attribute
18
+ #
19
+ # Once an instance is created, it should be populated by calling the #load method
20
+ # @example Populating an Organisation instance
21
+ # api = LUSI::API::Core::API.new(...)
22
+ # org = LUSI::API::Organisation::Organisation.new
23
+ # begin
24
+ # org.load(api, in_use_only: false)
25
+ # rescue LUSI::API::Core::APIError => e
26
+ # # API error handling
27
+ # end
28
+ class Organisation
29
+
30
+ # Defines the organisation hierarchy as a mapping from unit type to its child unit type and corresponding XPath.
31
+ # A leaf unit with no children maps to nil. The reserved unit type "_root" defines the root of the hierarchy.
32
+ HIERARCHY = {
33
+ _root: { type: :institution, path: '//xmlns:Institution' },
34
+ institution: { type: :faculty, path: 'xmlns:Faculties/xmlns:Faculty' },
35
+ faculty: { type: :department, path: 'xmlns:Departments/xmlns:Department' },
36
+ department: nil
37
+ }
38
+
39
+ # Initialises a new Organisation instance
40
+ # @param api [LUSI::API::Core::API, nil] an optional LUSI API instance used to retrieve data
41
+ # @param lookup [LUSI::API::Core::Lookup::LookupService, nil] the lookup service for object resolution
42
+ # @return [void]
43
+ def initialize(api = nil, lookup = nil)
44
+ @api = api
45
+ clear
46
+ end
47
+
48
+ # Searches the organisation top-down for the specified identity code and returns the first matching Unit
49
+ # @param identity [any] the identity code
50
+ # @return [Unit, nil] the matching Unit instance, or nil if no match is found
51
+ def [](identity = nil)
52
+ key = self.class.key(identity)
53
+ type = HIERARCHY[:_root][:type]
54
+ until type.nil? do
55
+ if @indices[type]
56
+ unit = @indices[type][:identity][key]
57
+ return unit if unit
58
+ end
59
+ type = HIERARCHY[type] ? HIERARCHY[type][:type] : nil
60
+ end
61
+ nil
62
+ end
63
+
64
+ # Adds a new Unit to the Organisation
65
+ # Child units are recursively added
66
+ # @param unit [LUSI::API::Organisation::Unit] the Unit instance
67
+ # @return void
68
+ def add(unit)
69
+
70
+ # Require a Unit instance
71
+ return unless unit.is_a?(Unit)
72
+
73
+ # Initialise the indices for this Unit type if required
74
+ @indices[unit.type] = { identity: {}, mnemonic: {}, title: {} } unless @indices.include?(unit.type)
75
+
76
+ # Index the unit
77
+ hash = @indices[unit.type]
78
+ hash[:identity][self.class.key(unit.identity)] = unit
79
+ hash[:mnemonic][self.class.key(unit.mnemonic)] = unit
80
+ hash[:title][self.class.key(unit.title)] = unit
81
+
82
+ # Index the unit's child units
83
+ unit.children.each { |child| add(child) } if unit.children
84
+
85
+ end
86
+
87
+ # Clears the organisation structure
88
+ # @return [void]
89
+ def clear
90
+ @indices = {}
91
+ end
92
+
93
+ # Iterates over selected parts of the organisation hierarchy
94
+ # @param type [Symbol, nil] the Unit type to iterate over
95
+ # @param index [Symbol, nil] the Unit type's index to iterate over (:identity | :mnemonic | :title)
96
+ # @return [void]
97
+ # @yield Passes a Unit instance to the block
98
+ # @yieldparam unit [LUSI::API::Organisation::Unit] the current unit of the iterator
99
+ def each(type = nil, index = nil, &block)
100
+ if type
101
+ # Iterate over the selected type; ignore invalid types
102
+ hash = @indices[type]
103
+ if hash
104
+ hash_index = hash.include?(index) ? index : :identity
105
+ hash[hash_index].each_value(&block) if hash[hash_index]
106
+ end
107
+ else
108
+ # Iterate over all types
109
+ @indices.each_value do |type|
110
+ hash_index = type.include?(index) ? index : :identity
111
+ type[hash_index].each_value(&block) if type[hash_index]
112
+ end
113
+ end
114
+ end
115
+
116
+ # Iterates over each unit type in the organisation hierarchy starting from the root
117
+ # @return [void]
118
+ # @yield [unit_type] Passes the unit type to the block
119
+ # @yieldparam unit_type [Symbol] the current unit type of the iterator
120
+ def each_unit_type
121
+ unit_type = HIERARCHY[:_root][:type]
122
+ while unit_type
123
+ yield(unit_type)
124
+ unit_type = HIERARCHY[unit_type] ? HIERARCHY[unit_type][:type] : nil
125
+ end
126
+ end
127
+
128
+ # Indicates whether the organisation is empty (contains units) or not
129
+ # @return [Boolean] true if the organisation contains no units, otherwise false
130
+ def empty?
131
+ # If @indices is not empty, use the #length method to determine emptiness
132
+ @indices.nil? || @indices.empty? || length == 0
133
+ end
134
+
135
+ # Returns the Unit instance matching the given parameters
136
+ # Key values are case-insensitive and normalised by removing leading, trailing and redundant whitespace.
137
+ # Title matching is based on the exact title after key normalisation.
138
+ # @param key [any] the Unit attribute value to search for
139
+ # @param type [Symbol, nil] the Unit type to search for (defaults to the root type of the organisation hierarchy)
140
+ # @param index [Symbol, nil] the Unit index to search (:identity, :mnemonic, :title)
141
+ # @return [Unit, nil] the matching Unit instance, or nil if no matches are found
142
+ def get(key, type = nil, index: nil)
143
+ hash = @indices[type] || @indices[HIERARCHY[:_root][:type]]
144
+ if hash
145
+ index = :identity unless hash.include?(index)
146
+ hash[index][self.class.key(key)]
147
+ else
148
+ nil
149
+ end
150
+ end
151
+
152
+ # Returns the number of Units matching the given parameters
153
+ # @param type [Symbol, nil] the Unit type to count (defaults to all units)
154
+ # @param index [Symbol, nil] the Unit index to count (:identity, :mnemonic, :title)
155
+ # @return [Integer, nil] the number of Unit instances for the given parameters, or nil
156
+ def length(type = nil, index: nil)
157
+ count = 0
158
+ types = type.nil? ? @indices.keys : [type]
159
+ types.each do |unit_type|
160
+ hash = @indices[unit_type]
161
+ if hash
162
+ hash_index = hash.include?(index) ? hash[index] : hash[:identity]
163
+ count += hash_index.length if hash_index
164
+ end
165
+ end
166
+ count
167
+ end
168
+
169
+ # Populates the Organisation instance from the LUSI API
170
+ # @param api [LUSI::API::Core::API, nil] the LUSI API instance to use (defaults to the Organisation's @api)
171
+ # @param in_use_only [Boolean] if true, include only current Units; if false, additionally include historic Units
172
+ # @return [Nokogiri::XML::Node] the parsed XML <body/> content from the LUSI API call
173
+ def load(api = nil, in_use_only: true)
174
+
175
+ # Call the LUSI API
176
+ api ||= @api
177
+ xml = api.call('LUSIReference', 'General.asmx', 'GetOrganisationStructure', InUseOnly: in_use_only)
178
+
179
+ # Clear the existing organisation structure
180
+ clear
181
+
182
+ # Add each organisation unit to the structure
183
+ root = HIERARCHY[:_root]
184
+ LUSI::API::Core::XML.xml(xml, root[:path]) do |u|
185
+ add(Unit.new(u, type: root[:type], hierarchy: HIERARCHY))
186
+ end
187
+
188
+ # Return the parsed XML body content from the LUSI API call
189
+ xml
190
+
191
+ end
192
+
193
+ protected
194
+
195
+ # Returns a normalised index key
196
+ # Normalisation removes redundant whitespace and converts to uppercase for case-insensitivity
197
+ # @param key [any] the key value
198
+ # @return [String, nil] the normalised key value
199
+ def self.key(key)
200
+ return nil if key.nil?
201
+ key = key.to_s.strip
202
+ key.squeeze!(' ')
203
+ key.upcase!
204
+ key
205
+ end
206
+
207
+ end
208
+
209
+
210
+ # Represents a single unit (component) of an Organisation
211
+ class Unit
212
+
213
+ # @!attribute [rw] children
214
+ # @return [Array<Unit>, nil] an array of child Unit instances
215
+ attr_accessor :children
216
+ # @!attribute [rw] identity
217
+ # @return [any, nil] the identity code of the unit
218
+ attr_accessor :identity
219
+ # @!attribute [rw] in_use
220
+ # @return [Boolean, nil] true if the unit is currently in use, or false if historic
221
+ attr_accessor :in_use
222
+ # @!attribute [rw] is_academic
223
+ # @return [Boolean, nil] true if the unit is an academic unit, or false if not (e.g. administrative)
224
+ attr_accessor :is_academic
225
+ # @!attribute [rw] mnemonic
226
+ # @return [String, nil] a short mnemonic code for the unit
227
+ attr_accessor :mnemonic
228
+ # @!attribute [rw] parent
229
+ # @return [Unit, nil] the parent of this unit in the organisation hierarchy, or nil if this is a top-level unit
230
+ attr_accessor :parent
231
+ # @!attribute [rw] talis_code
232
+ # @return [String, nil] the code for this unit used by Talis Aspire reading lists
233
+ attr_accessor :talis_code
234
+ # @!attribute [rw] title
235
+ # @return [String, nil] the title (name) of the unit
236
+ attr_accessor :title
237
+ # @!attribute [rw] type
238
+ # @return [Symbol, nil] the type of the unit (e.g. :institution, :faculty, :department)
239
+ attr_accessor :type
240
+
241
+ # Initialises a new Unit instance
242
+ # If the hierarchy definition for the unit type specifies child units, the child unit instances are recursively
243
+ # created and added to the @children attribute.
244
+ # @param xml [Nokogiri::XML::Document, Nokogiri::XML::Node] the XML root of the unit from the LUSI API call
245
+ # @param lookup [LUSI::API::Core::Lookup::LookupService, nil] the lookup service for object resolution
246
+ # @param type [Symbol] the unit type
247
+ # @param hierarchy [Hash] the organisation's hierarchy definition
248
+ # @see LUSI::API::Organisation::Organisation::HIERARCHY
249
+ # @param parent [Unit, nil] the parent Unit of this unit, or nil for a top-level unit
250
+ # @param identity [any, nil] the default identity code
251
+ # @param in_use [Boolean, nil] the default in-use flag value
252
+ # @param is_academic [Boolean, nil] the default is-academic flag value
253
+ # @param mnemonic [any, nil] the default mnemonic code
254
+ # @param talis_code [any, nil] the default Talis code
255
+ # @param title [any, nil] the default unit title (name)
256
+ def initialize(xml = nil, lookup = nil, type: nil, hierarchy: nil, parent: nil, identity: nil, in_use: nil,
257
+ is_academic: nil, mnemonic: nil, talis_code: nil, title: nil)
258
+
259
+ @identity = LUSI::API::Core::XML.xml_content_at(xml, 'xmlns:Identity', identity)
260
+ @in_use = LUSI::API::Core::XML.xml_boolean_at(xml, 'xmlns:InUse', in_use)
261
+ @is_academic = LUSI::API::Core::XML.xml_boolean_at(xml, 'xmlns:IsAcademic', is_academic)
262
+ @mnemonic = LUSI::API::Core::XML.xml_content_at(xml, 'xmlns:Mnemonic', mnemonic)
263
+ @parent = parent
264
+ @talis_code = LUSI::API::Core::XML.xml_content_at(xml, 'xmlns:TalisCode', talis_code)
265
+ @title = LUSI::API::Core::XML.xml_content_at(xml, 'xmlns:Title', title)
266
+ @type = type
267
+
268
+ # Get the child units of this unit
269
+ # If a block is given, yield each child to the block
270
+ child = hierarchy ? hierarchy[type] : nil
271
+ if child
272
+ @children = LUSI::API::Core::XML.xml(xml, child[:path]) do |u|
273
+ Unit.new(u, lookup, type: child[:type], parent: self, hierarchy: hierarchy)
274
+ end
275
+ else
276
+ @children = nil
277
+ end
278
+
279
+ end
280
+
281
+ # Returns a string representation of the unit
282
+ def to_s
283
+ "#{@type}: #{@title}"
284
+ end
285
+
286
+ end
287
+
288
+ end
289
+
290
+ end
291
+ end