require 'netzke/base_js' module Netzke # = Base # Base class for every Netzke widget # # To instantiate a widget in the controller: # # netzke :widget_name, configuration_hash # # == Configuration # * :widget_class_name - name of the widget class in the scope of the Netzke module, e.g. "FormPanel". # When a widget is defined in the controller and this option is omitted, widget class is deferred from the widget's # name. E.g.: # # netzke :grid_panel, :data_class_name => "User" # # In this case :widget_class_name is assumed to be "GridPanel" # # * :ext_config - a config hash that is used to create a javascript instance of the widget. Every # configuration that comes here will be available inside the javascript instance of the widget. # * :persistent_config - if set to true, the widget will use persistent storage to store its state; # for instance, Netzke::GridPanel stores there its columns state (width, visibility, order, headers, etc). # A widget may or may not provide interface to its persistent settings. GridPanel and FormPanel from netzke-basepack # are examples of widgets that by default do. # # Examples of configuration: # # netzke :books, # :widget_class_name => "GridPanel", # :data_class_name => "Book", # GridPanel specific option # :persistent_config => false, # don't use persistent config for this instance # :ext_config => { # :icon_cls => 'icon-grid', # :title => "My books" # } # # netzke :form_panel, # :data_class_name => "User" # FormPanel specific option class Base extend ActiveSupport::Memoizable include Netzke::BaseJs # javascript (client-side) attr_accessor :parent, :name, :global_id, :permissions, :session # Class-level Netzke::Base configuration. The defaults also get specified here. def self.config set_default_config({ # Which javascripts and stylesheets must get included at the initial load (see netzke-core.rb) :javascripts => [], :stylesheets => [], # AR model that provides us with persistent config functionality :persistent_config_manager => "NetzkePreference", # Default location of extjs library :ext_location => defined?(RAILS_ROOT) && "#{RAILS_ROOT}/public/extjs", # Default location of icons (TODO: has no effect for now) :icons_location => defined?(RAILS_ROOT) && "#{RAILS_ROOT}/public/images/icons", # Default instance config :default_config => { :persistent_config => true } }) end def self.set_default_config(c) #:nodoc: @@config ||= {} @@config[self.name] ||= c end # Override class-level defaults specified in Netzke::Base.config. # E.g. in config/initializers/netzke-config.rb: # # Netzke::GridPanel.configure :default_config => {:persistent_config => true} def self.configure(*args) if args.first.is_a?(Symbol) config[args.first] = args.last else # first arg is hash config.deep_merge!(args.first) end # widget may implement some kind of control for configuration consistency enforce_config_consistency if respond_to?(:enforce_config_consistency) end # Short widget class name, e.g.: # Netzke::Module::SomeWidget => Module::SomeWidget def self.short_widget_class_name self.name.sub(/^Netzke::/, "") end # Access to controller sessions def self.session @@session ||= {} end def self.session=(s) @@session = s end # Should be called by session controller at the moment of successfull login def self.login session[:_netzke_next_request_is_first_after_login] = true end # Should be called by session controller at the moment of logout def self.logout session[:_netzke_next_request_is_first_after_logout] = true end # Declare connection points between client side of a widget and its server side. For example: # # api :reset_data # # will provide JavaScript side with a method resetData that will result in a call to Ruby # method reset_data, e.g.: # # this.resetData({hard:true}); # # See netzke-basepack's GridPanel for an example. def self.api(*api_points) apip = read_inheritable_attribute(:api_points) || [] api_points.each{|p| apip << p} write_inheritable_attribute(:api_points, apip) # It may be needed later for security api_points.each do |apip| module_eval <<-END, __FILE__, __LINE__ def api_#{apip}(*args) #{apip}(*args).to_nifty_json end # FIXME: commented out because otherwise ColumnOperations stop working # def #{apip}(*args) # flash :warning => "API point '#{apip}' is not implemented for widget '#{short_widget_class_name}'" # {:flash => @flash} # end END end end api :load_aggregatee_with_cache # every widget gets this api # Array of API-points specified with Netzke::Base.api method def self.api_points read_inheritable_attribute(:api_points) end # Instance of widget by config def self.instance_by_config(config) widget_class = "Netzke::#{config[:widget_class_name]}".constantize widget_class.new(config) end # Persistent config manager class def self.persistent_config_manager_class Netzke::Base.config[:persistent_config_manager].try(:constantize) rescue NameError nil end # Return persistent config class # def self.persistent_config # # if the class is not present, fake it (it will not store anything, and always return nil) # persistent_config_manager_class || {} # end # Widget initialization process # * the config hash is available to the widget after the "super" call in the initializer # * override/add new default configuration options into the "default_config" method # (the config hash is not yet available) def initialize(config = {}, parent = nil) @session = Netzke::Base.session @passed_config = config # configuration passed at the moment of instantiation @parent = parent @name = config[:name].nil? ? short_widget_class_name.underscore : config[:name].to_s @global_id = parent.nil? ? @name : "#{parent.global_id}__#{@name}" @flash = [] end # # Configuration # # Default config - before applying any passed configuration def default_config self.class.config[:default_config].nil? ? {} : {}.merge(self.class.config[:default_config]) end # Static, hardcoded config. Consists of default values merged with config that was passed during instantiation def initial_config default_config.deep_merge(@passed_config) end memoize :initial_config # Config that is not overwritten by parents and sessions def independent_config initial_config.deep_merge(persistent_config_hash) end memoize :independent_config # If the widget has persistent config in its disposal def persistent_config_enabled? !persistent_config_manager_class.nil? && initial_config[:persistent_config] end # Store some setting in the database as if it was a hash, e.g.: # persistent_config["window.size"] = 100 # persistent_config["window.size"] => 100 # This method is user-aware def persistent_config(global = false) if persistent_config_enabled? || global config_class = self.class.persistent_config_manager_class config_class.widget_name = global ? nil : persistent_config_id # pass to the config class our unique name config_class else # if we can't use presistent config, all the calls to it will always return nil, # and the "="-operation will be ignored logger.debug "==> NETZKE: no persistent config is set up for widget '#{global_id}'" {} end end # A string which will identify the persistent config records for this widget def persistent_config_id #:nodoc: initial_config[:persistent_config_id] || global_id end def update_persistent_ext_config(hsh) current_config = persistent_config[:ext_config] || {} current_config.deep_merge!(hsh.deep_convert_keys{ |k| k.to_s }) # first, recursively stringify the keys persistent_config[:ext_config] = current_config end # Resulting config that takes into account all possible ways to configure a widget. *Read only*. # Translates into something like this: # default_config. # deep_merge(@passed_config). # deep_merge(persistent_config_hash). # deep_merge(strong_parent_config). # deep_merge(strong_session_config) def config independent_config.deep_merge(strong_parent_config).deep_merge(strong_session_config) end memoize :config def flat_config(key = nil) fc = config.flatten_with_type key.nil? ? fc : fc.select{ |c| c[:name] == key.to_sym }.first.try(:value) end def strong_parent_config @strong_parent_config ||= parent.nil? ? {} : parent.strong_children_config end def flat_independent_config(key = nil) fc = independent_config.flatten_with_type key.nil? ? fc : fc.select{ |c| c[:name] == key.to_sym }.first.try(:value) end def flat_default_config(key = nil) fc = default_config.flatten_with_type key.nil? ? fc : fc.select{ |c| c[:name] == key.to_sym }.first.try(:value) end def flat_initial_config(key = nil) fc = initial_config.flatten_with_type key.nil? ? fc : fc.select{ |c| c[:name] == key.to_sym }.first.try(:value) end # Returns a hash built from all persistent config values for the current widget, following the double underscore # naming convention. E.g., if we have the following persistent config pairs: # enabled => true # layout__width => 100 # layout__header__height => 20 # # this method will return the following hash: # {:enabled => true, :layout => {:width => 100, :header => {:height => 20}}} def persistent_config_hash return {} if !initial_config[:persistent_config] prefs = NetzkePreference.find_all_for_widget(persistent_config_id) res = {} prefs.each do |p| hsh_levels = p.name.split("__").map(&:to_sym) tmp_res = {} # it decends into itself, building itself anchor = {} # it will keep the tail of tmp_res hsh_levels.each do |level_prefix| tmp_res[level_prefix] ||= level_prefix == hsh_levels.last ? p.normalized_value : {} anchor = tmp_res[level_prefix] if level_prefix == hsh_levels.first tmp_res = tmp_res[level_prefix] end # Now 'anchor' is a hash that represents the path to the single value, # for example: {:ext_config => {:title => 100}} (which corresponds to ext_config__title) # So we need to recursively merge it into the final result res.deep_merge!(hsh_levels.first => anchor) end res.deep_convert_keys{ |k| k.to_sym } # recursively symbolize the keys end memoize :persistent_config_hash def ext_config config[:ext_config] || {} end # Like normal config, but stored in session def weak_session_config widget_session[:weak_session_config] ||= {} end def strong_session_config widget_session[:strong_session_config] ||= {} end # configuration of all children will get deep_merge'd with strong_children_config # def strong_children_config= (c) # @strong_children_config = c # end # This config will be picked up by all the descendants def strong_children_config @strong_children_config ||= parent.nil? ? {} : parent.strong_children_config end # configuration of all children will get reverse_deep_merge'd with weak_children_config # def weak_children_config= (c) # @weak_children_config = c # end def weak_children_config @weak_children_config ||= {} end def widget_session session[global_id] ||= {} end # Rails' logger def logger Rails.logger end def dependency_classes res = [] non_late_aggregatees.keys.each do |aggr| res += aggregatee_instance(aggr).dependency_classes end res << short_widget_class_name res.uniq end # 'Netzke::Grid' => 'Grid' def short_widget_class_name self.class.short_widget_class_name end ## Dependencies def dependencies @dependencies ||= begin non_late_aggregatees_widget_classes = non_late_aggregatees.values.map{|v| v[:widget_class_name]} (initial_dependencies + non_late_aggregatees_widget_classes << self.class.short_widget_class_name).uniq end end # override this method if you need some extra dependencies, which are not the aggregatees def initial_dependencies [] end ### Aggregation def initial_aggregatees {} end def aggregatees @aggregatees ||= initial_aggregatees.merge(initial_late_aggregatees.each_pair{|k,v| v.merge!(:late_aggregation => true)}) end def non_late_aggregatees aggregatees.reject{|k,v| v[:late_aggregation]} end def add_aggregatee(aggr) aggregatees.merge!(aggr) end def remove_aggregatee(aggr) if config[:persistent_config] persistent_config_manager_class.delete_all_for_widget("#{global_id}__#{aggr}") end aggregatees[aggr] = nil end # The difference between aggregatees and late aggregatees is the following: the former gets instantiated together with its aggregator and is normally *instantly* visible as a part of it (for example, the widget in the initially expanded panel in an Accordion). A late aggregatee doesn't get instantiated along with its aggregator. Until it gets requested from the server, it doesn't take any part in its aggregator's life. An example of late aggregatee could be a widget that is loaded dynamically into a previously collapsed panel of an Accordion, or a preferences window (late aggregatee) for a widget (aggregator) that only gets shown when user wants to edit widget's preferences. def initial_late_aggregatees {} end def add_late_aggregatee(aggr) aggregatees.merge!(aggr.merge(:late_aggregation => true)) end # recursively instantiates an aggregatee based on its "path": e.g. if we have an aggregatee :aggr1 which in its turn has an aggregatee :aggr10, the path to the latter would be "aggr1__aggr10" # TODO: introduce memoization def aggregatee_instance(name, strong_config = {}) aggregator = self name.to_s.split('__').each do |aggr| aggr = aggr.to_sym aggregatee_config = aggregator.aggregatees[aggr] raise ArgumentError, "No aggregatee '#{aggr}' defined for widget '#{aggregator.global_id}'" if aggregatee_config.nil? short_class_name = aggregatee_config[:widget_class_name] raise ArgumentError, "No widget_class_name specified for aggregatee #{aggr} of #{aggregator.global_id}" if short_class_name.nil? widget_class = "Netzke::#{short_class_name}".constantize conf = weak_children_config. deep_merge(aggregatee_config). deep_merge(strong_config). # we may want to reconfigure the aggregatee at the moment of instantiation merge(:name => aggr) aggregator = widget_class.new(conf, aggregator) # params: config, parent # aggregator.weak_children_config = weak_children_config # aggregator.strong_children_config = strong_children_config end aggregator end def full_widget_class_name(short_name) "Netzke::#{short_name}" end def flash(flash_hash) level = flash_hash.keys.first raise "Unknown message level for flash" unless %(notice warning error).include?(level.to_s) @flash << {:level => level, :msg => flash_hash[level]} end def widget_action(action_name) "#{@global_id}__#{action_name}" end # called when the method_missing tries to processes a non-existing aggregatee def aggregatee_missing(aggr) flash :error => "Unknown aggregatee #{aggr} for widget #{name}" {:feedback => @flash}.to_nifty_json end def tools persistent_config[:tools] ||= config[:tools] || [] end def menu persistent_config[:menu] ||= config[:menu] == false ? nil : config[:menu] end # some convenience for instances def persistent_config_manager_class self.class.persistent_config_manager_class end # override this method to do stuff at the moment of loading by some parent def before_load widget_session.clear end # Returns global id of a widget in the hierarchy, based on passed reference that follows # the double-underscore notation. Referring to "parent" is allowed. If going to far up the hierarchy will # result in nil, while referring to a non-existent aggregatee will simply provide an erroneous ID. # Example: # parent__parent__child__subchild will traverse the hierarchy 2 levels up, then going down to "child", # and further to "subchild". If such a widget exists in the hierarchy, its global id will be returned, otherwise # nil will be returned. def global_id_by_reference(ref) ref = ref.to_s return parent && parent.global_id if ref == "parent" substr = ref.sub(/^parent__/, "") if substr == ref # there's no "parent__" in the beginning return global_id + "__" + ref else return parent.global_id_by_reference(substr) end end # API: provides what is necessary for the browser to render a widget. # params should contain: # * :cache - an array of widget classes cached at the browser # * :id - reference to the aggregatee # * :container - Ext id of the container where in which the aggregatee will be rendered def load_aggregatee_with_cache(params) cache = params[:cache].gsub(".", "::").split(",") # array of cached class names (in Ruby) relative_widget_id = params.delete(:id).underscore.to_sym widget = aggregatees[relative_widget_id] && aggregatee_instance(relative_widget_id) if widget # inform the widget that it's being loaded widget.before_load [{ :js => widget.js_missing_code(cache), :css => widget.css_missing_code(cache) }, { :render_widget_in_container => { # TODO: rename it :container => params[:container], :config => widget.js_config } }, { :widget_loaded => { :id => relative_widget_id } }] else {:feedback => "Couldn't load aggregatee '#{relative_widget_id}'"} end end # Method dispatcher - instantiates an aggregatee and calls the method on it # E.g.: # users__center__get_data # instantiates aggregatee "users", and calls "center__get_data" on it # books__move_column # instantiates aggregatee "books", and calls "api_move_column" on it def method_missing(method_name, params = {}) widget, *action = method_name.to_s.split('__') widget = widget.to_sym action = !action.empty? && action.join("__").to_sym if action if aggregatees[widget] # only actions starting with "api_" are accessible api_action = action.to_s.index('__') ? action : "api_#{action}" aggregatee_instance(widget).send(api_action, params) else aggregatee_missing(widget) end else super end end end end