lusi_api 0.1.11

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