app/models/service_response.rb in umlaut-3.3.1 vs app/models/service_response.rb in umlaut-4.0.0.beta1
- old
+ new
@@ -1,37 +1,112 @@
=begin rdoc
-A ServiceResponse is a piece of data generated by a Service. It usually will
-be displayed on the resolve menu.
+A ServiceResponse represents a single piece of data or content generated in response to a request.
+For instance, a full text link, a 'see also' link, a cover image, a library holding, or an
+available 'search inside' service.
-ServiceResponses have a service type, represented by a string. When displaying, ServiceResponses are typically grouped into lists by service type. ServiceResponses are tied to the Service that created them, with the #service accessor.
+ServiceResponses were generated *for* to a particular request (`#request`), were
+generated *by* a particular service (`#service_id` or `#service`), and
+belong to a *category or type* such as 'fulltext', 'excerpts', or 'search_inside'
+(#service_type_value)
-ServiceResponses have a few basic attributes stored in columns in the db: 'display_text' is the text to put in the hyperlink. 'notes' is available for longer explanatory text (\n in notes will be converted to <br> by view). 'url' can be used to store the url to link to (but see below on linking mechanism).
+A ServiceResponse additionally has a hash of keys and values representing
+it's actual payload or content or data. Some of these keys are stored
+individually in database columns of their own, others are stored in
+a hash serialized to attribute `service_data` -- ServiceResponse offers
+an API so you ordinarily don't need to care which is which; any arbitrary
+key/value pairs can be included in a ServiceResponse.
-[The legacy columns response_key, value_string, value_alt_string and value_text are deprecated and should not be used, but some legacy Services still use them, so they're still there for now].
+ service_response.view_data
+ #=> A hash of key/value pairs representing the payload
-In addition, there's a Hash (automatically serialized by ActiveRecord) that's stored in service_data, for arbitrary additional data that a Service can store--whatever you want, just put it in. However, there are conventions that Views expect, see below. You can access ALL the arbitrary key/values in a ServiceResponse, built-in in attributes or from the serialized Hash, by the proxy object returned from #data_values.
+You ordinarily create a ServiceResponse by asking a particular
+Umlaut Request to do so:
-You can create a ServiceResponse object with ServiceResponse.create_from_hash,
-where the hash keys may be direct iVar columns, or serialized in the
-service_data hash, you don't have to care.
+ umlaut_request.add_service_response(
+ :service=>self,
+ :service_type_value => :fulltext
+ :display_text => "Some link",
+ :url => "http://example.com"
+ )
-ServiceResponse is connected to a Request via the ServiceType join table. The data architecture allows a ServiceResponse to be tied to multiple requests, perhaps to support some kind of cacheing re-use in the future. But at present, the code doesn't do this, a ServiceResponse will really only be related to one request. However, a ServiceResponse can be related to a single Request more than once--once per each type of service response. ServiceType is really a three way join, representing a ServiceResponse, attached to a particular Request, with a particular ServiceTypeValue.
+There are certain conventional keys (like :display_text and :notes) accross
+most response types, and other keys that might be particular to a certain type
+or even a certain service. Services can store data in their own custom keys
+for their own use. See "Conventional Keys" below for documentation on Umlaut
+standard naming.
+== Transformation of view data on display: transform_view_data and response_url
+When a ServiceResponse is created, it's initialized with some key/value
+service_data that is stored in the database.
+
+However, there are also features in place letting a Service filter or transform
+this key/value data at the point of display or use.
+
+If a Service implements a method `transform_view_data(hash)`, then this method
+will be called at the point of view_data use (possibly multiple times). You can
+modify the hash passsed in, or even replace it entirelyl, just return a new hash.
+
+ def transform_view_data(hash)
+ if hash[:add_some_notes]
+ hash[:display_text] = some_helper_method( params[:something])
+ end
+
+ return hash
+ end
+
+This can be especially useful for internationalization (i18n), please see Umlaut
+wiki at https://github.com/team-umlaut/umlaut/wiki/Localization#services-and-their-generated-serviceresponses
+
+Similarly, instead of calculating the URL at the point of recording a ServiceResponse,
+you can implement a method `response_url` that will be called when a user clicks
+on a ServiceResponse, and can return a URL to redirect the user too.
+
+This is useful if the URL is expensive to calculate; or if you want to log or report
+something on click; or if the URL needs user-input to be calculated (eg search_inside deep link)
+
+ def response_url(service_response, submitted_params)
+ return "http://books.google.com/#{service_response.view_data[:book_id]?query=submitted_params[:q]"
+ end
+
== View Display of ServiceResponse
-The resolve menu View expects a Hash (or Hash-like) object with certain conventional keys, to display a particular ServiceResponse. You can provide code in your Service to translate a ServiceResponse to a Hash. But you often don't need to, you can use the proxy object returned by #data_values instead, which provides hash-like access to all arbitrary key/values stored in ServiceResponse. If the Service stores properties in there using conventional keys (see below), no further translation is needed.
+The resolve menu View expects a Hash (or Hash-like) object with certain conventional keys,
+to display a particular ServiceResponse. You can provide code in your Service to translate a
+ ServiceResponse to a Hash. But you often don't need to, you can use the proxy object returned by
+ #data_values instead, which provides hash-like access to all arbitrary key/values stored in ServiceResponse.
+ If the Service stores properties in there using conventional keys (see below), no further translation is needed.
However, if you need to do further translation you can implement methods on the Service, of the form: "to_[service type string](response)", for instance "to_fulltext". Umlaut will give it a ServiceResponse object, method should return a hash (or hash-like obj). Service can also implement a method response_to_view_data(response), as a 'default' translation. This mechanism of various possible 'translations' is implemented by Service#view_data_from_service_type.
== Url generation
-At the point the user clicks on a ServiceResponse, Umlaut will attempt to find a url for the ServiceResponse, by calling response_url(response) on the relevant Service. The default implementation in service.rb just returns service_response['url'], so the easiest way to do this is just to put the url in service_response['url']. However, your Service can over-ride this method to provide it's own implementation to generate to generate the url on demand in any way it wants. If it does this, technically service_response['url'] doesn't need to include anything. But if you have a URL, you may still want to put it there, for Umlaut to use in guessing something about the destination, for de-duplication and possibly other future purposes.
+At the point the user clicks on a ServiceResponse, Umlaut will attempt to find a url for the ServiceResponse,
+by calling response_url(response) on the relevant Service. The default
+implementation in service.rb just returns service_response['url'], so the easiest way
+to do this is just to put the url in service_response['url']. However, your Service can over-ride
+this method to provide it's own implementation to generate to generate the url on demand in any way it wants.
+ If it does this, technically service_response['url'] doesn't need to include anything. But if you have a URL,
+ you may still want to put it there, for Umlaut to use in guessing something about the destination,
+ for de-duplication and possibly other future purposes.
-= Conventional keys:
+== Note on ServiceType join table.
+ServiceResponse is connected to a Request via the ServiceType join table. This is mostly
+for legacy reasons, currently unused -- normally a ServiceResponse is attached to one
+and only one Request.
+
+The architecture would allows a ServiceResponse to be tied to multiple requests,
+perhaps to support some kind of cacheing re-use in the future. But at present, the code
+doesn't do this, a ServiceResponse will really only be related to one request. However, a
+ServiceResponse can be related to a single Request more than once--once per each type of
+service response. ServiceType is really a three way join, representing a ServiceResponse,
+attached to a particular Request, with a particular ServiceTypeValue.
+
+= Conventional keys
+
Absolute minimum:
[:display_text] Text that will be used
Basic set (used by fulltext and often others)
[:display_text]
@@ -102,12 +177,12 @@
MatchExact = 'exact'
MatchUnsure = 'unsure'
#MatchAltEdition = 'edition'
#MatchAltWork = 'work'
- def initialize(params = nil)
- super(params)
+ def initialize(*args)
+ super
self.service_data = {} unless self.service_data
end
# Create from a hash of key/values, where some keys
# may be direct iVars, some may end up serialized in service_data,
@@ -120,17 +195,11 @@
# Instantiates and returns a new Service associated with this response.
def service
@service ||= ServiceStore.instantiate_service!( self.service_id, nil )
end
-
- # Returns a hash or hash-like object with properties for the service response.
- def view_data
- self.service.view_data_from_service_type(self)
- end
-
-
+
def service_data
# Fix weird-ass char encoding bug with AR serialize and hashes.
# https://github.com/rails/rails/issues/6538
data = super
if data.kind_of? Hash
@@ -150,68 +219,65 @@
end
def take_key_values(hash)
- # copy it, cause we're gonna modify it
- hash = hash.clone
-
hash.each_pair do |key, value|
setter = "#{key.to_s}="
if ( self.respond_to?(setter))
self.send(setter, value)
- hash.delete(key)
+ else
+ self.service_data[key] = value
end
end
- # What's left is arbitrary key/values that go in service_data
- init_service_data(hash)
end
- def init_service_data(hash)
- hash.each {|key, value| data_values[key] = value} if hash
- end
- def data_values
- # Lazy load, and store a reference. Don't worry, ruby
- # GC handles circular references no problem.
- unless (@data_values_proxy)
- @data_values_proxy = ServiceResponseDataValues.new(self)
+ def view_data
+ unless (@data_values)
+ h = HashWithIndifferentAccess.new
+ ServiceResponse.built_in_fields.each do |key|
+ h[key] = self.send(key)
+ end
+ h.merge!(self.service_data.deep_dup)
+
+ # add in service_type_value
+ h[:service_type_value] = self.service_type_value_name
+
+ # Handle requested i18n translations
+ translate_simple_i18n!(h)
+
+ # Optional additional transformation provided by service?
+ if service.respond_to? :transform_view_data
+ h = service.transform_view_data(h)
+ end
+
+ # Doesn't protect modifying nested structures, but
+ # protects from some problems.
+ h.freeze
+ @data_values = h
end
- return @data_values_proxy;
+ return @data_values;
end
+ # old name now duplicate
+ alias_method :data_values, :view_data
def self.built_in_fields
@@built_in_fields
end
-
-
-end
-
-# A proxy-like class, to provide hash-like access to all arbitrary
-# key/value pairs stored in a ServiceResponse, whether they key/value
-# is stored in an ActiveRecord attribute (#built_in_fields) or in
-# the serialized hash in the service_data attribute.
-# Symbols passed in will be 'normalized' to strings before being used as keys.
-# So symbols and strings are interchangeable. Normally, keys should be symbols.
-class ServiceResponseDataValues
- def initialize(arg_service_response)
- @service_response = arg_service_response
- end
-
- def [](key)
- if ServiceResponse.built_in_fields.include?(key)
- return @service_response.send(key)
- else
- return @service_response.service_data[key]
+ protected
+ # replaces :display_text and :notes with simple i18n key lookups
+ # iff :display_text_i18n and :notes_i18n are defined
+ #
+ # i18n lookups use Service.translate, to use standard scopes for
+ # this service.
+ def translate_simple_i18n!(hash)
+ if key = hash[:display_text_i18n]
+ hash[:display_text] = self.service.translate(key, :default => hash[:display_text])
end
- end
-
- def []=(key, value)
- if(ServiceResponse.built_in_fields.include?(key))
- @service_response.send(key.to_s+'=', value)
- else
- @service_response.service_data[key] = value
+ if key = self[:notes_i18n]
+ hash[:notes] = self.service.translate(key, :default => hash[:notes])
end
end
end