lib/survey_gizmo/resource.rb in survey-gizmo-ruby-1.0.5 vs lib/survey_gizmo/resource.rb in survey-gizmo-ruby-2.0.0

- old
+ new

@@ -1,16 +1,15 @@ -require "set" -require "addressable/uri" +require 'set' +require 'addressable/uri' module SurveyGizmo module Resource extend ActiveSupport::Concern included do include Virtus.model instance_variable_set('@paths', {}) - instance_variable_set('@collections', {}) SurveyGizmo::Resource.descendants << self end # @return [Set] Every class that includes SurveyGizmo::Resource def self.descendants @@ -20,28 +19,62 @@ # These are methods that every API resource has to access resources # in Survey Gizmo module ClassMethods # Convert a [Hash] of filters into a query string - # @param [Hash] filters + # @param [Hash] filters - simple pagination or other options at the top level, and surveygizmo "filters" at the :filters key # @return [String] + # + # example input: {page: 2, filters: [{:field=>"istestdata", :operator=>"<>", :value=>1}]} + # The top level keys (e.g. page, resultsperpage) get simply encoded in the url, while the contents of the array of hashes + # passed at filters[:filters] gets turned into the format surveygizmo expects for its internal filtering, for example: + # + # filter[field][0]=istestdata&filter[operator][0]=<>&filter[value][0]=1 def convert_filters_into_query_string(filters = nil) - "" unless filters && filters.size > 0 - uri = Addressable::URI.new - uri.query_values = filters - "?#{uri.query}" + if filters && filters.size > 0 + output_filters = filters[:filters] || [] + filter_hash = {} + output_filters.each_with_index do |filter,i| + filter_hash.merge!({ + "filter[field][#{i}]".to_sym => "#{filter[:field]}", + "filter[operator][#{i}]".to_sym => "#{filter[:operator]}", + "filter[value][#{i}]".to_sym => "#{filter[:value]}", + }) + end + simple_filters = filters.reject {|k,v| k == :filters} + filter_hash.merge!(simple_filters) + + uri = Addressable::URI.new + uri.query_values = filter_hash + "?#{uri.query}" + else + '' + end end # Get a list of resources # @param [Hash] conditions # @param [Hash] filters - # @return [SurveyGizmo::Collection, Array] + # @return [Array] of objects of this class def all(conditions = {}, filters = nil) - response = Response.new SurveyGizmo.get(handle_route(:create, conditions) + convert_filters_into_query_string(filters)) + response = RestResponse.new(SurveyGizmo.get(handle_route(:create, conditions) + convert_filters_into_query_string(filters))) if response.ok? - _collection = SurveyGizmo::Collection.new(self, nil, response.data) - _collection.send(:options=, {:target => self, :parent => self}) + _collection = response.data.map {|datum| datum.is_a?(Hash) ? self.new(datum) : datum} + + # Add in the properties from the conditions hash because many of the important ones (like survey_id) are + # not often part of the SurveyGizmo returned data + conditions.keys.each do |k| + if conditions[k] && instance_methods.include?(k) + _collection.each { |c| c[k] ||= conditions[k] } + end + end + + # Sub questions are not pulled by default so we have to retrieve them + if self == SurveyGizmo::API::Question + _collection += _collection.map {|question| question.sub_questions}.flatten + end + _collection else [] end end @@ -49,21 +82,23 @@ # Get the first resource # @param [Hash] conditions # @param [Hash] filters # @return [Object, nil] def first(conditions = {}, filters = nil) - response = Response.new SurveyGizmo.get(handle_route(:get, conditions) + convert_filters_into_query_string(filters)) - response.ok? ? load(conditions.merge(response.data)) : nil + response = RestResponse.new(SurveyGizmo.get(handle_route(:get, conditions) + convert_filters_into_query_string(filters))) + # Add in the properties from the conditions hash because many of the important ones (like survey_id) are + # not often part of the SurveyGizmo's returned data + response.ok? ? new(conditions.merge(response.data)) : nil end # Create a new resource # @param [Hash] attributes # @return [Resource] # The newly created Resource instance def create(attributes = {}) resource = new(attributes) - resource.__send__(:_create) + resource.create_record_in_surveygizmo resource end # Copy a resource # @param [Integer] id @@ -79,12 +114,11 @@ # Deleted the Resource from Survey Gizmo # @param [Hash] conditions # @return [Boolean] def destroy(conditions) - response = Response.new SurveyGizmo.delete(handle_route(:delete, conditions)) - response.ok? + RestResponse.new(SurveyGizmo.delete(handle_route(:delete, conditions))).ok? end # Define the path where a resource is located # @param [String] path # the path in Survey Gizmo for the resource @@ -93,133 +127,63 @@ # which is `:get`, `:create`, `:update`, `:delete`, or `:any` # @scope class def route(path, options) methods = options[:via] methods = [:get, :create, :update, :delete] if methods == :any - methods.is_a?(Array) ? methods.each{|m| @paths[m] = path } : (@paths[methods] = path) + methods.is_a?(Array) ? methods.each { |m| @paths[m] = path } : (@paths[methods] = path) nil end + # This method replaces the :page_id, :survey_id, etc strings defined in each model's URI routes with the + # values being passed in interpolation hash with the same keys. # @api private - def load(attributes = {}) - resource = new(attributes) - resource.__send__(:clean!) - resource - end - - # Defines a new collection. These are child objects of the resource. - # @macro [new] collection - # @param [Symbol] resource_name the name of the collection, pluralized - # @param [Class] model and optional class name if the class name does not match the resource_name - # @return [Collection] - # the $1 collection - # @scope instance - def collection(resource_name, model = nil) - @collections[resource_name] = {:parent => self, :target => (model ? model : resource_name)} # workaround for weird bug with passing a class to Collection - class_eval(<<-EOS) - def #{resource_name} - @#{resource_name} ||= [] - end - - def #{resource_name}=(array) - @#{resource_name} = SurveyGizmo::Collection.new(#{self}, :#{resource_name}, array) - end - EOS - end - - # @api private - def collections - @collections.dup.freeze - end - - # @api private - def handle_route(key, *interp) + def handle_route(key, interpolation_hash) path = @paths[key] raise "No routes defined for `#{key}` in #{self.name}" unless path - options = interp.last.is_a?(Hash) ? interp.pop : path.scan(/:(\w+)/).inject({}){|hash, k| hash.merge(k.to_sym => interp.shift) } + raise "User/password hash not setup!" if SurveyGizmo.default_params.empty? + path.gsub(/:(\w+)/) do |m| - options[$1.to_sym].tap{ |result| raise(SurveyGizmo::URLError, "Missing parameters in request: `#{m}`") unless result } + raise(SurveyGizmo::URLError, "Missing RESTful parameters in request: `#{m}`") unless interpolation_hash[$1.to_sym] + interpolation_hash[$1.to_sym] end end end - # Updates attributes and saves this Resource instance - # - # @param [Hash] attributes - # attributes to be updated - # - # @return [Boolean] - # true if resource is saved - def update(attributes = {}) - self.attributes = attributes - self.save - end - # Save the instance to Survey Gizmo - # - # @return [Boolean] - # true if Resource instance is saved def save - if new? - _create + if id #Then it's an update + handle_response(SurveyGizmo.post(handle_route(:update), query: self.attributes_without_blanks)) + @latest_response.ok? else - handle_response SurveyGizmo.post(handle_route(:update), :query => self.attributes_without_blanks) do - _response.ok? ? saved! : false - end + create_record_in_surveygizmo end end # fetch resource from SurveyGizmo and reload the attributes # @return [self, false] # Returns the object, if saved. Otherwise returns false. def reload - handle_response SurveyGizmo.get(handle_route(:get)) do - if _response.ok? - self.attributes = _response.data - clean! - else - false - end + handle_response(SurveyGizmo.get(handle_route(:get))) + if @latest_response.ok? + self.attributes = @latest_response['data'] + self + else + false end end # Deleted the Resource from Survey Gizmo # @return [Boolean] def destroy - return false if new? || destroyed? - handle_response SurveyGizmo.delete(handle_route(:delete)) do - _response.ok? ? destroyed! : false + if id + handle_response(SurveyGizmo.delete(handle_route(:delete))) + @latest_response.ok? + else + false end end - # The state of the current Resource - # @api private - def new? - @_state.nil? - end - - # @todo This seemed like a good way to prevent accidently trying to perform an action - # on a record at a point when it would fail. Not sure if it's really necessary though. - [:clean, # stored and not dirty - :saved, # stored and not modified - :destroyed, # duh! - :zombie # needs to be stored - ].each do |state| - # Change the method state to $1 - define_method("#{state}!") do - @_state = state - true - end - - # Inquire about the method state if $1 - define_method("#{state}?") do - @_state == state - end - - private "#{state}!" - end - # Sets the hash that will be used to interpolate values in routes. It needs to be defined per model. # @return [Hash] a hash of the values needed in routing def to_param_options raise "Define #to_param_options in #{self.class.name}" end @@ -228,142 +192,78 @@ # @return [Array] def errors @errors ||= [] end - # @return [Hash] The raw JSON returned by Survey Gizmo - def raw_response - _response.response if _response - end - # @visibility private def inspect - attrs = self.class.attribute_set.map do |attrib| - value = attrib.get!(self).inspect - - "@#{attrib.name}=#{value}" if attrib.respond_to?(:name) + if ENV['GIZMO_DEBUG'] + ap "CLASS: #{self.class}" end - "#<#{self.class.name}:#{self.object_id} #{attrs.join(' ')}>" - end + attribute_strings = self.class.attribute_set.map do |attrib| + if ENV['GIZMO_DEBUG'] + ap attrib + ap attrib.name + ap self.send(attrib.name) + ap self.send(attrib.name).class + end - # This class normalizes the response returned by Survey Gizmo - class Response - def ok? - @response && @response['result_ok'] - end - - # The parsed JSON data of the response - def data - @_data ||= (@response['data'] || {}) - end - - # The error message if there is one - def message - @_message ||= @response['message'] - end - - attr_reader :response - - private - def cleanup_attribute_name(attr) - attr.downcase.gsub(/[^[:alnum:]]+/,'_').gsub(/(url|variable|standard|shown)/,'').gsub(/_+/,'_').gsub(/^_/,'').gsub(/_$/,'') - end - - def find_attribute_parent(attr) - case attr.downcase - when /url/ - "url" - when /variable.*standard/ - "meta" - when /variable.*shown/ - "shown" - when /variable/ - "variable" - when /question/ - "answers" + if self.send(attrib.name).class == Hash + value = self.send(attrib.name).inspect + else + value = self.send(attrib.name).to_s end - end - def initialize(response) - @response = response.parsed_response - return if @response.nil? or not ok? - @_data = @response['data'] + " \"#{attrib.name}\" => \"#{value}\"\n" unless value.strip.blank? + end.compact - # Handle really crappy [] notation in SG API, so far just in SurveyResponse - items = (@_data.is_a?(Array) ? @_data : [@_data]).compact - items.each do |data_item| - data_item.keys.grep(/^\[/).each do |key| - next if data_item[key].nil? || data_item[key].length == 0 + "#<#{self.class.name}:#{self.object_id}>\n#{attribute_strings.join()}" + end - parent = find_attribute_parent(key) - data_item[parent] = {} unless data_item[parent] - - case key.downcase - when /(url|variable.*standard)/ - data_item[parent][cleanup_attribute_name(key).to_sym] = data_item[key] - when /variable.*shown/ - data_item[parent][cleanup_attribute_name(key).to_i] = data_item[key].include?("1") - when /variable/ - data_item[parent][cleanup_attribute_name(key).to_i] = data_item[key].to_i - when /question/ - data_item[parent][key] = data_item[key] - end - - data_item.delete(key) - end - end unless items.blank? + # Returns itself if successfully saved, but with attributes added by SurveyGizmo + def create_record_in_surveygizmo(attributes = {}) + http = RestResponse.new(SurveyGizmo.put(handle_route(:create), query: self.attributes_without_blanks)) + handle_response(http) + if http.ok? + self.attributes = http.data + self + else + false end end protected def attributes_without_blanks - self.attributes.reject{|k,v| v.blank? } + self.attributes.reject { |k,v| v.blank? } end private - # The response object from SurveyGizmo. Useful for viewing the raw data returned - attr_reader :_response - - def set_response(http) - @_response = Response.new(http) - end - def handle_route(key) self.class.handle_route(key, to_param_options) end - def handle_response(resp, &block) - set_response(resp) - (self.errors << _response.message) unless _response.ok? - self.errors.clear if !self.errors.empty? && _response.ok? - instance_eval(&block) - end - - def _create(attributes = {}) - http = SurveyGizmo.put(handle_route(:create), :query => self.attributes_without_blanks) - handle_response http do - if _response.ok? - self.attributes = _response.data - saved! - else - false - end + def handle_response(rest_response, &block) + @latest_response = rest_response + if @latest_response.ok? + self.errors.clear + true + else + errors << @latest_response.message + false end end def _copy(attributes = {}) - http = SurveyGizmo.post(handle_route(:update), :query => self.attributes_without_blanks) - handle_response http do - if _response.ok? - self.attributes = _response.data - saved! + http = RestResponse.new(SurveyGizmo.post(handle_route(:update), query: self.attributes_without_blanks)) + handle_response(http) do + if http.ok? + self.attributes = http.data else false end end end - end end