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 include Netzke::BaseJs # javascript (client-side) module ClassMethods # Class-level Netzke::Base configuration. The defaults also get specified here. def config set_default_config({ # which javascripts and stylesheets must get included at the initial load (see netzke-core.rb) :javascripts => [], :stylesheets => [], :persistent_config_manager => "NetzkePreference", :ext_location => defined?(RAILS_ROOT) && "#{RAILS_ROOT}/public/extjs", :default_config => { :persistent_config => true } }) end def configure(*args) if args.first.is_a?(Symbol) # first arg is a Symbol config[args.first] = args.last else config.deep_merge!(args.first) end enforce_config_consistency end def enforce_config_consistency; end # "Netzke::SomeWidget" => "SomeWidget" def short_widget_class_name self.name.split("::").last end # Multi-user support (deprecated in favor of controller sessions) def user @@user ||= nil end def user=(user) @@user = user end # Access to controller sessions def session @@session ||= {} end def session=(s) @@session = s end # called by controller at the moment of successfull login def login session[:_netzke_next_request_is_first_after_login] = true end # called by controller at the moment of logout def logout session[:_netzke_next_request_is_first_after_logout] = true end # Use this class method to declare connection points between client side of a widget and its server side. # A method in a widget class with the same name will be (magically) called by the client side of the widget. # See netzke-basepack's GridPanel for an example. def 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 def api_points read_inheritable_attribute(:api_points) end # returns an instance of a widget defined in the config def instance_by_config(config) widget_class = "Netzke::#{config[:widget_class_name]}".constantize widget_class.new(config) end # persistent_config and layout manager classes def persistent_config_manager_class Netzke::Base.config[:persistent_config_manager].try(:constantize) rescue NameError nil end # Return persistent config class def persistent_config # if the class is not present, fake it (it will not store anything, and always return nil) if persistent_config_manager_class.nil? {} else persistent_config_manager_class end end private def set_default_config(c) @@config ||= {} @@config[self.name] ||= c end end extend ClassMethods # If the widget has persistent config in its disposal def persistent_config_enabled? !persistent_config_manager_class.nil? && config[:persistent_config] end attr_accessor :parent, :name, :id_name, :permissions, :session api :load_aggregatee_with_cache # every widget gets this api # 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 @id_name = parent.nil? ? @name : "#{parent.id_name}__#{@name}" @flash = [] end # add flatten method to Hash Hash.class_eval do def flatten(preffix = "") res = [] self.each_pair do |k,v| if v.is_a?(Hash) res += v.flatten(k) else res << { :name => ((preffix.to_s.empty? ? "" : preffix.to_s + "__") + k.to_s).to_sym, :value => v, :type => (["TrueClass", "FalseClass"].include?(v.class.name) ? 'Boolean' : v.class.name).to_sym } end end res end end def default_config self.class.config[:default_config].nil? ? {} : {}.merge!(self.class.config[:default_config]) end # Access to the config that takes into account all possible ways to configure a widget. *Read only*. def config # Translates into something like this: # @config ||= default_config. # deep_merge(@passed_config). # deep_merge(persistent_config_hash). # deep_merge(strong_parent_config). # deep_merge(strong_session_config) @config ||= independent_config. deep_merge(strong_parent_config). deep_merge(strong_session_config) end def flat_config(key = nil) fc = config.flatten 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 # Config that is not overwritten by parents and sessions def independent_config @independent_config ||= initial_config.deep_merge(persistent_config_hash) end def flat_independent_config(key = nil) fc = independent_config.flatten 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 key.nil? ? fc : fc.select{ |c| c[:name] == key.to_sym }.first.try(:value) end # Static, hardcoded config. Consists of default values merged with config that was passed during instantiation def initial_config @initial_config ||= default_config.deep_merge(@passed_config) end def flat_initial_config(key = nil) fc = initial_config.flatten key.nil? ? fc : fc.select{ |c| c[:name] == key.to_sym }.first.try(:value) end def build_persistent_config_hash return {} if !initial_config[:persistent_config] prefs = NetzkePreference.find_all_for_widget(id_name) 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 end def persistent_config_hash @persistent_config_hash ||= build_persistent_config_hash end 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[id_name] ||= {} 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 # 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 if config[:persistent_config] config_class = self.class.persistent_config config_class.widget_name = id_name # 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 '#{id_name}'" {} end 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("#{id_name}__#{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" 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.id_name}'" if aggregatee_config.nil? short_class_name = aggregatee_config[:widget_class_name] raise ArgumentError, "No widget_class_name specified for aggregatee #{aggr} of #{aggregator.id_name}" 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) "#{@id_name}__#{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 # API: provides all that is necessary for the browser to render a widget. # params def load_aggregatee_with_cache(params) cache = ActiveSupport::JSON.decode(params.delete(:cache)) relative_widget_id = params.delete(:id).underscore widget = aggregatee_instance(relative_widget_id) # 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 => { :container => params[:container], :config => widget.js_config } }, { :widget_loaded => { :id => relative_widget_id } }] 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