require 'net/https' require 'cgi/util' require 'multi_json' # helpers require 'domoscio_rails/version' require 'domoscio_rails/json' require 'domoscio_rails/errors' require 'domoscio_rails/authorization_token' # resources require 'domoscio_rails/http_calls' require 'domoscio_rails/resource' require 'domoscio_rails/data/content' require 'domoscio_rails/data/device' require 'domoscio_rails/data/event' require 'domoscio_rails/data/instance_parameter' require 'domoscio_rails/data/recommendation' require 'domoscio_rails/data/learning_session_recommendation' require 'domoscio_rails/data/learning_session_knowledge_node' require 'domoscio_rails/data/learning_session' require 'domoscio_rails/data/scorm' require 'domoscio_rails/data/student' require 'domoscio_rails/data/student_group' require 'domoscio_rails/data/student_group_user' require 'domoscio_rails/data/question' require 'domoscio_rails/data/user' require 'domoscio_rails/data/account' require 'domoscio_rails/data/role' require 'domoscio_rails/data/supervisor' require 'domoscio_rails/knowledge/knowledge_edge' require 'domoscio_rails/knowledge/knowledge_graph' require 'domoscio_rails/knowledge/scale' require 'domoscio_rails/knowledge/knowledge_graph_scale' require 'domoscio_rails/knowledge/knowledge_graph_node' require 'domoscio_rails/knowledge/knowledge_node_content' require 'domoscio_rails/knowledge/knowledge_node_student' require 'domoscio_rails/knowledge/knowledge_node' require 'domoscio_rails/objective/learning_program_student' require 'domoscio_rails/objective/learning_program' require 'domoscio_rails/objective/objective_knowledge_node_student' require 'domoscio_rails/objective/objective_knowledge_node' require 'domoscio_rails/objective/objective_student' require 'domoscio_rails/objective/objective' require 'domoscio_rails/objective/subscription' require 'domoscio_rails/tag/tag_edge' require 'domoscio_rails/tag/tag_set' require 'domoscio_rails/tag/tag' require 'domoscio_rails/tag/tagging' require 'domoscio_rails/utils/analytic' require 'domoscio_rails/utils/gameplay_util' require 'domoscio_rails/utils/recommendation_util' require 'domoscio_rails/utils/review_util' require 'domoscio_rails/utils/stats_util' require 'domoscio_rails/utils/lxp' require 'domoscio_rails/utils/export' require 'domoscio_rails/utils/export_request' require 'domoscio_rails/utils/kpi_log' module DomoscioRails # Configurable attributes and default values class Configuration attr_accessor :client_id, :client_passphrase, :temp_dir, :version, :root_url, :file_storage, :access_token, :refresh_token def initialize @version = 2 @root_url = '' @file_storage ||= true end end class << self attr_accessor :configuration end def self.configure self.configuration ||= Configuration.new yield configuration end def self.api_uri(url = '') URI(configuration.root_url + url) end # # - +method+: HTTP method; lowercase symbol, e.g. :get, :post etc. # - +url+: the part after Configuration#root_url # - +params+: hash; entity data for creation, update etc.; will dump it by JSON and assign to Net::HTTPRequest#body # # Performs HTTP requests to Adaptive Engine # On token issues, will try once to get a new token then will output a DomoscioRails::ReponseError with details # # Raises DomoscioRails::ResponseError on Adaptive Error Status # Raises DomoscioRails::ProcessingError on Internal Error # def self.request(method, url, params = {}) params ||= {} store_tokens, headers = request_headers params.merge!({ 'per_page': 1000 }) unless params[:per_page] uri = api_uri(url) response = DomoscioRails.send_request(uri, method, params, headers) return response if response.is_a? DomoscioRails::ProcessingError begin raise_http_failure(uri, response, params) if response['Content-Type'] == 'application/zip' || response['Content-Type'] == 'application/xlsx' data = response elsif response.is_a?(Net::HTTPNoContent) data = [] else data = DomoscioRails::JSON.load(response.body.nil? ? '' : response.body) end if store_tokens DomoscioRails::AuthorizationToken::Manager.storage.store({ access_token: response['Accesstoken'], refresh_token: response['Refreshtoken'] }) end rescue MultiJson::LoadError => e data = ProcessingError.new(uri, 500, e, response.body, params) rescue ResponseError => e data = e end if response['Total'] || response['Total-Pages'] != '1' pagetotal = if response['Total'] (response['Total'].to_i / response['Per-Page'].to_f).ceil else response['Total-Pages'].to_i end (2..pagetotal).each do |j| response = DomoscioRails.send_request(uri, method, params.merge({ page: j }), headers) return response if response.is_a? DomoscioRails::ProcessingError begin raise_http_failure(uri, response, params) body = DomoscioRails::JSON.load(response.body.nil? ? '' : response.body) data += body data.flatten! rescue MultiJson::LoadError => e return ProcessingError.new(uri, 500, e, response.body, params) rescue ResponseError => e return e end end end data end # This function catches usual Http errors during callsheaders # def self.send_request(uri, method, params, headers) response = perform_call(uri, method, params, headers) response = retry_call_and_store_tokens(uri, method, params) if %w[401 403].include? response.code response rescue Timeout::Error, Errno::EINVAL, Errno::ECONNREFUSED, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e ProcessingError.new(uri, 500, e, response) end # This helper will check the response status and build the correcponding DomoscioRails::ResponseError # def self.raise_http_failure(uri, response, params) return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPNoContent) raise ResponseError.new( uri, response.code.to_i, DomoscioRails::JSON.load((response.body.nil? ? '' : response.body), symbolize_keys: true), response.body, params ) end # Actual HTTP call is performed here # def self.perform_call(uri, method, params, headers) Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| req = Net::HTTP.const_get(method.capitalize).new(uri.request_uri, headers) req.body = DomoscioRails::JSON.dump(params) http.request req end end # This method is called when AdaptiveEngine returns tokens errors # Action on those errors is to retry and request new tokens, those new token are then stored def self.retry_call_and_store_tokens(uri, method, params) headers = request_new_tokens response = perform_call(uri, method, params, headers) DomoscioRails::AuthorizationToken::Manager.storage.store({ access_token: response['Accesstoken'], refresh_token: response['Refreshtoken'] }) response end def self.user_agent @uname ||= uname { bindings_version: DomoscioRails::VERSION, lang: 'ruby', lang_version: "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})", platform: RUBY_PLATFORM, uname: @uname }.to_s end def self.uname `uname -a 2>/dev/null` if RUBY_PLATFORM =~ /linux|darwin/i rescue Errno::ENOMEM 'uname lookup failed' end # Process the token loading and analyze # will return the processed headers and a token store flag # def self.request_headers auth_token = DomoscioRails::AuthorizationToken::Manager.token if auth_token && auth_token[:access_token] && auth_token[:refresh_token] [false, send_current_tokens(auth_token)] else [true, request_new_tokens] end rescue SyntaxError, StandardError [true, request_new_tokens] end # If stored token successfully loaded we build the header with them # def self.send_current_tokens(auth_token) { 'user_agent' => DomoscioRails.user_agent.to_s, 'AccessToken' => auth_token[:access_token].to_s, 'RefreshToken' => auth_token[:refresh_token].to_s, 'Content-Type' => 'application/json' } end # If we cant find tokens of they are corrupted / expired, then we set headers to request new ones def self.request_new_tokens { 'user_agent' => DomoscioRails.user_agent.to_s, 'Authorization' => "Token token=#{DomoscioRails.configuration.client_passphrase}", 'Content-Type' => 'application/json' } end end