lib/engine2/action.rb in engine2-1.0.4 vs lib/engine2/action.rb in engine2-1.0.5

- old
+ new

@@ -1,213 +1,1369 @@ # coding: utf-8 +# frozen_string_literal: true module Engine2 + class Action + attr_reader :node, :meta, :assets, :static, :invokable - class Action < BasicObject - ACCESS_FORBIDDEN ||= ->h{false} - attr_reader :parent, :name, :number, :actions, :recheck_access - attr_reader :meta_proc - class << self - attr_accessor :count + def action_type at = nil + at ? @action_type = at : @action_type + end - def default_meta - Class.new(InlineMeta){meta_type :inline} + def http_method hm = nil + hm ? @http_method = hm : @http_method end + + def inherited cls + cls.http_method http_method + end + + def inherit + Class.new self do + action_type superclass.action_type + end + end end - def initialize parent, name, meta_class, assets - Action.count += 1 - @number = Action.count - @parent = parent - @name = name - @meta = meta_class.new(self, assets) - @actions = {} + http_method :get + + def initialize node, assets, static = self + @meta = {} + @node = node + @assets = assets + @static = static end - def * &blk - @meta_proc = @meta_proc ? @meta_proc.chain(&blk) : blk if blk - @meta + def http_method + @http_method # || (raise E2Error.new("No http method for action #{self.class}")) end - alias :meta :* + def action_type + @action_type || (raise E2Error.new("No action_type for action #{self.class}")) + end - def access! &blk - ::Kernel.raise E2Error.new("Access for action #{name} already defined") if @access_block - @access_block = blk + def check_static_action + raise E2Error.new("Static action required") if dynamic? end - def access_forbidden! - access! &ACCESS_FORBIDDEN + def define_invoke &blk + check_static_action + self.class.class_eval{define_method :invoke, &blk} end - def check_access! handler - !@access_block || @access_block.(handler) + def invoke! handler + if rmp = @request_action_proc + action = self.class.new(node, assets, self) + action_result = action.instance_exec(handler, *action.request_action_proc_params(handler), &rmp) + action.post_process + response = @requestable ? (action_result || {}) : action.invoke(handler) + response[:meta] = action.meta + response + else + invoke(handler) + end end - def run_scheme name, *args, &blk - result = instance_exec(*args, &SCHEMES[name]) - result.instance_eval(&blk) if blk - result + def repeat time + @meta[:repeat] = time end - def define_action name, meta_class = Action.default_meta, assets = {}, &blk - ::Kernel.raise E2Error.new("Action #{name} already defined") if @actions[name] - action = @actions[name] = Action.new(self, name, meta_class, assets) - action.*.pre_run - define_singleton_method! name do |&ablk| # forbidden list - action.instance_eval(&ablk) if ablk - action - end - action.instance_eval(&blk) if blk - action.*.action_defined - action + def arguments args + @meta[:arguments] = args end - def define_action_meta name, meta_class = Action.default_meta, assets = {}, &blk - define_action name, meta_class, assets do - self.* &blk + def execute time + @meta[:execute] = time + end + + def dynamic? + self != @static + end + + # def [] *keys + # @meta.path(*keys) + # end + + # def []= *keys, value + # @meta.path!(*keys, value) + # end + + def lookup *keys + if dynamic? # we are the request action + value = @meta.path(*keys) + value.nil? ? @static.meta.path(*keys) : value + # value || @static.value.path(keys) + else + @meta.path(*keys) end end - def define_action_invoke name, meta_class = Action.default_meta, assets = {}, &blk - define_action name, meta_class, assets do - self.*.define_invoke &blk + def merge *keys + if keys.length == 1 + key = keys.first + dynamic? ? @static.meta[key].merge(@meta[key] || {}) : @meta[key] + else + dynamic? ? @static.meta.path(*keys).merge(@meta.path(*keys)) : @meta.path(*keys) end end - def define_action_bundle name, *actions - define_singleton_method!(name) do |&blk| - if blk - actions.each{|a|__send__(a, &blk)} # if @actions[action] ? + def freeze_action + hash = @meta + hash.freeze + # hash.each_pair{|k, v| freeze(v) if v.is_a? Hash} + freeze + end + + def request_action_proc_params handler + [] + end + + def request &blk + raise E2Error.new("No block given for request action") unless blk + raise E2Error.new("No request block in request action allowed") if dynamic? + @request_action_proc = @request_action_proc ? @request_action_proc.chain_args(&blk) : blk + nil + end + + def pre_run + @action_type = self.class.action_type + @http_method = self.class.http_method + end + + def node_defined + end + + def post_run + if respond_to? :invoke + @invokable = true + else + if @request_action_proc + @invokable = true + @requestable = true else - ActionBundle.new(self, actions) + @meta[:invokable] = false end end + @meta[:dynamic_meta] = true if @request_action_proc + post_process end - def define_singleton_method! name, &blk - class << self;self;end.instance_eval do # __realclass__ - define_method name, &blk + def post_process + end + + def split_keys id + Sequel::split_keys(id) + end + end + + module ActionWebSocketSupport + WS_METHODS ||= Faye::WebSocket::API::TYPES.keys.map(&:to_sym) + WS_METHODS.each do |method| + define_method :"ws_#{method}" do |&blk| + @ws_methods[method] = blk end end - def [] name - @actions[name] + def pre_run + super + @ws_methods = {} + @meta[:websocket] = {options: {}} end - def actions_info handler - info = actions.inject({}) do |h, (name, a)| - meta = a.* - h[name] = { - meta_type: meta.meta_type, - method: meta.http_method, - number: a.number, - access: recheck_access ? nil : a.check_access!(handler), - recheck_access: a.recheck_access, - terminal: a.actions.empty?, - meta: !meta.get.empty? - } - h + def ws_options opts + @meta[:websocket][:options].merge! opts + end + + def ws_execute execute + (@meta[:websocket][:execute] ||= {}).merge! execute + end + + def post_run + super + @invokable = true + end + + def invoke! handler + if Faye::WebSocket.websocket?(handler.env) + ws = Faye::WebSocket.new(handler.env) + @ws_methods.each do |method, blk| + ws.on(method) do |evt| + begin + if method == :message + blk.(JSON.parse(evt.data, symbolize_names: true), ws, evt) + else + blk.(evt, ws, evt) + end + rescue Exception => e + ws.send! error: {exception: e, method: method} + end + end + end + ws.rack_response + else + super end + end + end - info.first[1][:default] = true unless actions.empty? - info + class WebSocketAction < Action + include ActionWebSocketSupport + end + + class InlineAction < Action + action_type :inline + end + + class RootAction < Action + def initialize *args + super + @meta.merge! environment: Handler::environment, application: Engine2::SETTINGS[:name], key_separator: Engine2::SETTINGS[:key_separator], ws_methods: ActionWebSocketSupport::WS_METHODS end + end - def access_info handler - @actions.inject({}) do |h, (name, a)| - h[name] = a.check_access!(handler) - h + module ActionAPISupport + def info field + (@meta[:info] ||= {})[field.to_sym] ||= {} + end + + def config + @meta[:config] ||= {} + end + + def info! *fields, options + raise E2Error.new("No fields given to info") if fields.empty? + fields.each do |field| + info(field).merge! options # rmerge ? end end - def recheck_access! - @recheck_access = true + def loc! hash + (@meta[:loc] ||= {}).merge! hash end - def each_action &blk - # no self - @actions.each_pair do |n, a| - a.each_action(&blk) if yield a + def decorate list + list.each do |f| + info(f)[:loc] ||= LOCS[f.to_sym] end end - def to_a_rec root = true, result = [], &blk # optimize - if root && (yield self) - result << self - @actions.each_pair do |n, a| - if yield a - result << a - a.to_a_rec(false, result, &blk) + def render field, options + info! field, render: options + end + + def hide_fields *flds + info! *flds, hidden: true + end + + def show_fields *flds + info! *flds, hidden: false + end + + def field_filter *flds, filter + info! *flds, filter: filter + end + end + + module ActionMenuSupport + def menu menu_name, &blk + @menus ||= {} + @menus[menu_name] ||= ActionMenuBuilder.new(:root) + @menus[menu_name].instance_eval(&blk) if blk + @menus[menu_name] + end + + def menu? menu_name + @menus && @menus[menu_name] + end + + def post_process + super + if @menus && !@menus.empty? + @meta[:menus] = {} + @menus.each_pair do |name, menu| + @meta[:menus][name] = {entries: menu.to_a, properties: menu.properties} + end + end + end + end + + module ActionModelSupport + def pre_run + if !(mdl = @assets[:model]) + act = node + begin + act = act.parent + raise E2Error.new("Model not found in tree for node: #{node.name}") unless act + mdl = act.*.assets[:model] + end until mdl + + if asc = @assets[:assoc] + @assets[:model] = asc.associated_class + # raise E2Error.new("Association '#{asc}' for model '#{asc[:class_name]}' not found") unless @assets[:model] + else + @assets[:model] = mdl + asc = act.*.assets[:assoc] + @assets[:assoc] = asc if asc + end + end + + # @meta[:model!] = assets[:model] + # @meta[:assoc!] = assets[:assoc] ? assets[:assoc][:name] : nil + # @meta[:action_class!] = self.class + super + end + + def hide_pk + hide_fields *assets[:model].primary_keys + end + + def show_pk + show_fields *assets[:model].primary_keys + end + + def get_type_info name + model = assets[:model] + info = case name + when Symbol + model.type_info[name] + when Sequel::SQL::QualifiedIdentifier + assoc = model.many_to_one_associations[name.table] || model.many_to_many_associations[name.table] + raise E2Error.new("Association #{name.table} not found for model #{model}") unless assoc + assoc.associated_class.type_info[name.column] + else + raise E2Error.new("Unknown type info key: #{name} in model #{model}") + end + + raise E2Error.new("Type info not found for '#{name}' in model '#{model}'") unless info + info + end + + # def parent_model_name + # model = @assets[:model] + # prnt = node.parent + + # while prnt && prnt.*.assets[:model] == model + # prnt = prnt.parent + # end + # m = prnt.*.assets[:model] + # m ? m.name : nil + # end + + def node_defined + super + # p_model_name = parent_model_name + model = @assets[:model] + + at = action_type + case at + when :list, :star_to_many_list, :star_to_many_link_list, :star_to_many_field, :star_to_many_field_link_list # :many_to_one_list + model.many_to_one_associations.each do |assoc_name, assoc| + unless assoc[:propagate] == false # || p_model_name == assoc[:class_name] + dc = model.type_info[assoc[:keys].first][:decode] + node.run_scheme :decode, model, assoc_name, dc[:search] end end end - result + + case at + when :modify, :create + model.many_to_one_associations.each do |assoc_name, assoc| + unless assoc[:propagate] == false # || p_model_name == assoc[:class_name] + dc = model.type_info[assoc[:keys].first][:decode] + node.run_scheme :decode, model, assoc_name, dc[:form] + end + end + end + + case at + when :list #, :star_to_many_list, :many_to_one_list # list dropdowns + divider = false + model.one_to_many_associations.merge(model.many_to_many_associations).each do |assoc_name, assoc| + unless assoc[:propagate] == false + menu(:item_menu).divider unless divider + divider ||= true + menu(:item_menu).option :"#{assoc_name}!", icon: "list" # , click: "action.show_assoc($index, \"#{assoc_name}!\")" + node.run_scheme :star_to_many, :"#{assoc_name}!", assoc + end + end + end + + case at + when :modify, :create + model.type_info.each do |field, info| + case info[:type] + when :blob_store + node.run_scheme :blob_store, model, field + when :foreign_blob_store + node.run_scheme :foreign_blob_store, model, field + when :file_store + node.run_scheme :file_store, model, field + when :star_to_many_field + assoc = model.association_reflections[info[:assoc_name]] # info[:name] ? + raise E2Error.new("Association '#{info[:assoc_name]}' not found for model '#{model}'") unless assoc + node.run_scheme :star_to_many_field, assoc, field + end + end + end end - def inspect - "Action: #{@name}, meta: #{@meta.class}, meta_type: #{@meta.meta_type}" + def unsupported_association assoc + raise E2Error.new("Unsupported association: #{assoc}") end + end - def setup_action_tree - time = ::Time.now + module ActionQuerySupport + def query q, &blk + @query = blk ? q.naked.with_row_proc(blk) : q.naked + end - model_actions = {} - each_action do |action| - if model = action.*.assets[:model] - model_name = model.name.to_sym - model.synchronize_type_info - model_actions[model_name] = action.to_a_rec{|a| !a.*.assets[:assoc]} - action.run_scheme(model_name) if SCHEMES[model_name, false] - false + def post_run + query select(*assets[:model].columns) unless @query + super + end + + def get_query # move to query ? + if dynamic? + @query || @static.get_query + else + @query + end + end + + def find_record handler, id + get_query[assets[:model].primary_keys_hash_qualified(split_keys(id))] + end + + def select *args, use_pk: true, &blk + ds = assets[:model].select(*args, &blk) + ds = ds.ensure_primary_key if use_pk + ds.setup!(@meta[:fields] = []) + end + end + + module ActionTabSupport + def select_tabs tabs, *args, &blk + field_tabs tabs + select *tabs.map{|name, fields|fields}.flatten, *args, &blk + end + + def field_tabs hash + @meta[:tabs] = hash.map{|k, v| {name: k, loc: LOCS[k], fields: v} } + end + + def lazy_tab tab_name + tabs = @meta[:tabs] + raise E2Error.new("No tabs defined") unless tabs + tab = tabs.find{|t| t[:name] == tab_name} + raise E2Error.new("No tab #{tab_name} defined") unless tab + tab[:lazy] = true + end + end + + module ActionAngularSupport + def ng_execute expr + (@meta[:execute] ||= "") << expr + ";" + end + + def ng_record! name, value + value = case value + when String + "'#{value}'" + when nil + 'null' + else + value + end + + "action.record['#{name}'] = #{value}" + end + + def ng_record name + "action.record['#{name}']" + end + + def ng_info! name, *selector, expression + # expression = "'#{expression}'" if expression.is_a? String + "action.meta.info['#{name}'].#{selector.join('.')} = #{expression}" + end + + def ng_call name, *args + # TODO + end + end + + module ActionPanelSupport + def pre_run + modal_action true + super + end + + def post_run + super + if @meta[:panel] + panel_panel_template 'menu_m' if panel[:panel_template].nil? + # modal_action false if panel[:panel_template] == false + panel_class '' unless panel[:class] + panel_footer true if panel[:footer] != false && menu?(:panel_menu) + panel_header true if panel[:header] != false + end + end + + def panel + @meta[:panel] ||= {} + end + + def modal_action modal = true + panel[:modal_action] = modal + end + + def panel_template tmpl + panel[:template] = tmpl + end + + def panel_panel_template tmpl + panel[:panel_template] = tmpl + end + + def panel_class cls + panel[:class] = cls + end + + def panel_title tle + panel[:title] = tle + end + + def panel_header hdr + panel[:header] = hdr + end + + def panel_footer ftr + panel[:footer] = ftr + end + end + + class MenuAction < Action + include ActionMenuSupport + action_type :menu + + def invoke handler + {} + end + end + + class ConfirmAction < Action + include ActionPanelSupport, ActionMenuSupport + action_type :confirm + + def message msg + @meta[:message] = msg + end + + def pre_run + super + panel_template 'scaffold/message' + panel_title LOCS[:confirmation] + panel_class 'modal-default' + + menu :panel_menu do + option :approve, icon: "ok", loc: LOCS[:ok], disabled: "action.action_pending()" + option :cancel, icon: "remove" + end + end + + def invoke handler + params = handler.request.params + # params.merge({arguments: params.keys}) + end + end + + module ActionOnChangeSupport + def on_change field, &blk + node_name = :"#{field}_on_change" + nd = node.define_node node_name, (blk.arity > 2 ? OnChangeGetAction : OnChangePostAction) + nd.*{request &blk} + + info! field, remote_onchange: node_name + info! field, remote_onchange_record: :true if blk.arity > 2 + end + + class OnChangeAction < Action + include ActionAPISupport, ActionAngularSupport + + def request_action_proc_params handler + if handler.request.post? + json = handler.post_to_json + [json[:value], json[:record]] else - true + params = handler.request.params + [params["value"], params["record"]] end end - each_action do |action| - meta = action.* - model = meta.assets[:model] - assoc = meta.assets[:assoc] - if model && assoc - if source_actions = model_actions[model.name.to_sym] - source_action = source_actions.select{|sa| sa.meta_proc && sa.*.class >= meta.class} - # source_action = source_actions.select{|sa| sa.meta_proc && meta.class <= sa.*.class} - unless source_action.empty? - # raise E2Error.new("Multiple meta candidates for #{action.inspect} found in '#{source_action.inspect}'") if source_action.size > 1 - # puts "#{action.inspect} => #{source_action.inspect}\n" - meta.instance_eval(&source_action.first.meta_proc) - end + def invoke handler + {} + end + end + + class OnChangeGetAction < OnChangeAction + action_type :on_change + + def request_action_proc_params handler + params = handler.request.params + [params["value"], params["record"]] + end + end + + class OnChangePostAction < OnChangeAction + http_method :post + action_type :on_change + + def request_action_proc_params handler + json = handler.post_to_json + [json[:value], json[:record]] + end + end + end + + module ActionListSupport + include ActionModelSupport, ActionAPISupport, ActionTabSupport, ActionPanelSupport, ActionMenuSupport, ActionOnChangeSupport + attr_reader :filters, :orders + + def pre_run + super + config.merge!(per_page: 10, use_count: false, selectable: true) # search_active: false, + + panel_template 'scaffold/list' + panel_title "#{:list.icon} #{LOCS[assets[:model].name.to_sym]}" + loc! LOCS[:list_locs] + menu :menu do + properties break: 2, group_class: "btn-group-xs" + option :search_toggle, icon: "search", show: "action.meta.search_fields", active: "action.ui_state.search_active", button_loc: false + # divider + option :refresh, icon: "refresh", button_loc: false + option :default_order, icon: "signal", button_loc: false + divider + option :debug_info, icon: "list-alt" do + option :show_meta, icon: "eye-open" + end if Handler::development? + end + + menu :item_menu do + properties break: 1, group_class: "btn-group-xs" + end + + @meta[:state] = [:query, :ui_state] + end + + def field_tabs hash + super + search_template 'scaffold/search_tabs' + end + + def select_toggle_menu + m = menu :menu + unless m.option_index(:select_toggle, false) + m.option_after :default_order, :select_toggle, icon: "check", enabled: "action.meta.config.selectable", active: "action.selection", button_loc: false + end + end + + def post_run + unless panel[:class] + panel_class case @meta[:fields].size + when 1..3; '' + when 4..6; 'modal-large' + else; 'modal-huge' + end + end + + super + @meta[:primary_fields] = assets[:model].primary_keys + end + + # def find_renderer type_info + # renderer = DefaultSearchRenderers[type_info[:type]] || DefaultSearchRenderers[type_info[:otype]] + # raise E2Error.new("No search renderer found for field '#{type_info[:name]}'") unless renderer + # renderer.(self, type_info) + # end + + def post_process + if fields = @meta[:search_fields] + fields = fields - static.meta[:search_fields] if dynamic? + + decorate(fields) + fields.each do |name| + type_info = get_type_info(name) + + # render = info[name][:render] + # if not render + # info[name][:render] = find_renderer(type_info) + # else + # info[name][:render].merge!(find_renderer(type_info)){|key, v1, v2|v1} + # end + + info(name)[:render] ||= begin # set before :fields + renderer = DefaultSearchRenderers[type_info[:type]] || DefaultSearchRenderers[type_info[:otype]] + raise E2Error.new("No search renderer found for field '#{type_info[:name]}'") unless renderer + renderer.(self, type_info) end + + proc = SearchRendererPostProcessors[type_info[:type]] || ListRendererPostProcessors[type_info[:type]] # ? + proc.(self, name, type_info) if proc end + end - meta.instance_eval(&action.meta_proc) if action.meta_proc - true + if fields = @meta[:fields] + fields = fields - static.meta[:fields] if dynamic? + + decorate(fields) + fields.each do |name| + type_info = get_type_info(name) + proc = ListRendererPostProcessors[type_info[:type]] + proc.(self, name, type_info) if proc + end end - each_action do |action| - action.*.post_run - action.*.freeze_meta + super + end + + def search_template template + panel[:search_template] = template + end + + def sortable *flds + flds = @meta[:fields] if flds.empty? + info! *flds, sort: true + end + + def search_live *flds + flds = @meta[:search_fields] if flds.empty? + info! *flds, search_live: true + end + + def searchable *flds + @meta.delete(:tabs) + search_template 'scaffold/search' + @meta[:search_fields] = *flds + end + + def searchable_tabs tabs + searchable *tabs.map{|name, fields|fields}.flatten + field_tabs tabs + end + + def template + SearchTemplates + end + + def filter name, &blk + (@filters ||= {})[name] = blk + end + + def filter_case_insensitive name + raise E2Error.new("Field '#{name}' needs to be a string one") unless get_type_info(name)[:otype] == :string + filter(name){|query, hash, handler| query.where(name.ilike("%#{hash[name]}%")) } + end + + def order name, &blk + (@orders ||= {})[name] = blk + end + end + + module ActionApproveSupport + include ActionModelSupport + attr_reader :validations + + def self.included action + action.http_method :post if action.is_a? Class + end + + def validate_fields *fields + if fields.empty? + @validate_fields + else + @validate_fields = assets[:model].type_info.keys & (fields + assets[:model].primary_keys).uniq + end + end + + def before_approve handler, record + end + + def after_approve handler, record + end + + def validate_and_approve handler, record, parent_id + static.before_approve(handler, record) + record.valid? + validate_record(handler, record) + if record.errors.empty? + static.after_approve(handler, record) true + else + false end + end - ::Kernel::puts "ACTIONS: #{Action.count}, Time: #{::Time.now - time}" + def allocate_record handler, json_rec + model = assets[:model] + handler.permit json_rec.is_a?(Hash) + val_fields = (dynamic? ? static.validate_fields : @validate_fields) || model.type_info.keys + handler.permit (json_rec.keys - val_fields).empty? + + record = model.call(json_rec) + record.validate_fields = val_fields + record end - def p *args - ::Kernel::p *args + def record handler, record + {errors: nil} end + + def invoke handler + json = handler.post_to_json + record = allocate_record(handler, json[:record]) + validate_and_approve(handler, record, json[:parent_id]) ? static.record(handler, record) : {record!: record.to_hash, errors!: record.errors} + end + + def validate name, &blk + (@validations ||= {})[name] = blk + end + + def validate_record handler, record + @validations.each do |name, val| + unless record.errors[name] + result = val.(record, handler) + record.errors.add(name, result) if result + end + end if @validations + end + + def pre_run + super + execute "action.errors || [action.parent().invoke(), action.panel_close()]" + end + + def post_run + super + validate_fields *node.parent.*.meta[:fields] unless validate_fields + end end + module ActionSaveSupport + include ActionApproveSupport - class ActionBundle - def initialize action, action_names - @action = action - @action_names = action_names + def self.included action + action.http_method :post + class << action + attr_accessor :validate_only + end end - def method_missing name, *args, &blk - @action_names.each{|an| @action[an].__send__(name, *args, &blk)} + def validate_and_approve handler, record, parent_id, validate_only = self.class.validate_only + if validate_only then + super(handler, record, parent_id) + else + record.skip_save_refresh = true + record.raise_on_save_failure = false + model = assets[:model] + assoc = assets[:assoc] + new_assoc = record.new? && assoc && assoc[:type] + + save = lambda do|c| + if super(handler, record, parent_id) + if new_assoc == :one_to_many + handler.permit parent_id + assoc[:keys].zip(split_keys(parent_id)).each{|k, v|record[k] = v} + end + + result = record.save(transaction: false, validate: false) + if result && new_assoc == :many_to_many + handler.permit parent_id + model.db[assoc[:join_table]].insert(assoc[:left_keys] + assoc[:right_keys], split_keys(parent_id) + record.primary_key_values) + end + + model.association_reflections.each do |name, assoc| + hash = record[name] + if hash.is_a?(Hash) + validate_and_approve_association(handler, record, name, :create, hash) + validate_and_approve_association(handler, record, name, :modify, hash) + nd = node.parent[:"#{name}!"] + raise Sequel::Rollback unless record.errors.empty? + nd.confirm_delete.delete.*.invoke_delete_db(handler, hash[:delete].to_a, model.table_name) unless hash[:delete].to_a.empty? + nd.link.*.invoke_link_db(handler, record.primary_key_values, hash[:link].to_a) unless hash[:link].to_a.empty? + nd.confirm_unlink.unlink.*.invoke_unlink_db(handler, record.primary_key_values, hash[:unlink].to_a) unless hash[:unlink].to_a.empty? + end + end + result + end + end + (model.validation_in_transaction || new_assoc == :many_to_many) ? model.db.transaction(&save) : save.(nil) + end end + + def validate_and_approve_association handler, record, assoc_name, node_name, hash + records = hash[node_name].to_a + unless records.empty? + action = node.parent[:"#{assoc_name}!"][node_name].approve.* + parent_id = Sequel::join_keys(record.primary_key_values) + records.each do |arec| + rec = action.allocate_record(handler, arec) + action.validate_and_approve(handler, rec, parent_id, false) + rec.errors.each do |k, v| + (record.errors[assoc_name] ||= []).concat(v) + end unless rec.errors.empty? + end + end + end end -end \ No newline at end of file + + module ActionInsertSupport + def allocate_record handler, json_rec + record = super(handler, json_rec) + record.instance_variable_set(:"@new", true) + model = assets[:model] + model.primary_keys.each{|k|record.values.delete k} unless model.natural_key + handler.permit !record.has_primary_key? unless model.natural_key + record + end + end + + module ActionUpdateSupport + def allocate_record handler, json_rec + record = super(handler, json_rec) + model = assets[:model] + handler.permit record.has_primary_key? unless model.natural_key or self.class.validate_only + record + end + end + + module ActionFormSupport + include ActionModelSupport, ActionAPISupport, ActionTabSupport, ActionPanelSupport, ActionMenuSupport, ActionAngularSupport, ActionOnChangeSupport + + def field_template template + panel[:field_template] = template + end + + def pre_run + super + panel_template 'scaffold/form' + field_template 'scaffold/fields' + panel_class 'modal-large' + top = node.parent.parent == nil + menu :panel_menu do + option :approve, icon: "ok", disabled: "action.action_pending()" # text: true, + option :cancel, icon: "remove" unless top # text: true, + end + # modal_action false + end + + def field_tabs hash + super + panel_template 'scaffold/form_tabs' + end + + def record handler, record + end + + def post_process + if fields = @meta[:fields] + fields = fields - static.meta[:fields] if dynamic? + + decorate(fields) + + fields.each do |name| + # type_info = model.type_info.fetch(name) + type_info = get_type_info(name) + + info(name)[:render] ||= begin + renderer = DefaultFormRenderers[type_info[:type]] # .merge(default: true) + raise E2Error.new("No form renderer found for field '#{type_info[:name]}' of type '#{type_info[:type]}'") unless renderer + renderer.(self, type_info) + end + + proc = FormRendererPostProcessors[type_info[:type]] + proc.(self, name, type_info) if proc + end + + assoc = assets[:assoc] + if assoc && assoc[:type] == :one_to_many + # fields.select{|f| assoc[:keys].include? f}.each do |key| + # # hide_fields(key) if self[:info, key, :hidden] == nil + # info! key, disabled: true + # end + assoc[:keys].each do |key| + info! key, disabled: true if fields.include? key + end + end + end + + super + end + + def post_run + super + @meta[:primary_fields] = assets[:model].primary_keys + end + + def template + Templates + end + + def hr_after field, message = '-' + info! field, hr: message + end + end + + module ActionCreateSupport + include ActionFormSupport + + def self.included action + action.action_type :create + end + + def pre_run + super + panel_title LOCS[:create_title] + node.parent.*.menu(:menu).option_at 0, node.name, icon: "plus-sign", button_loc: false + + hide_pk unless assets[:model].natural_key + end + + def record handler, record + create_record(handler, record) + end + + def create_record handler, record + end + + def invoke handler + record = {} + # if assoc = assets[:assoc] + # case assoc[:type] + # when :one_to_many + # parent = handler.params[:parent_id] + # assoc[:keys].zip(split_keys(parent)).each{|key, val| record[key] = val} if parent + # end + # end + static.record(handler, record) + {record: record, new: true} + end + end + + module ActionModifySupport + include ActionFormSupport + + def self.included action + action.action_type :modify + end + + def pre_run + super + panel_title LOCS[:modify_title] + node.parent.*.menu(:item_menu).option node.name, icon: "pencil", button_loc: false + end + + def record handler, record + modify_record(handler, record) + end + + def modify_record handler, record + end + + def invoke handler + handler.permit id = handler.params[:id] + record = find_record(handler, id) + + if record + static.record(handler, record) + {record: record} + else + handler.halt_not_found LOCS[:no_entry] + end + end + + def post_run + super + assets[:model].primary_keys.each do |key| # pre_run ? + info! key, disabled: true + end + end + end + + module ActionViewSupport + include ActionModelSupport, ActionAPISupport, ActionTabSupport, ActionPanelSupport, ActionMenuSupport + + def self.included action + action.action_type :view + end + + def pre_run + super + panel_template 'scaffold/view' + panel_title LOCS[:view_title] + panel[:backdrop] = true + + menu(:panel_menu).option :cancel, icon: "remove" + node.parent.*.menu(:item_menu).option node.name, icon: "file", button_loc: false + end + + def field_tabs hash + super + panel_template 'scaffold/view_tabs' + end + + def record handler, record + end + + def invoke handler + handler.permit id = handler.params[:id] + record = find_record(handler, id) + if record + static.record(handler, record) + {record: record} + else + handler.halt_not_found LOCS[:no_entry] + end + end + + def post_process + if fields = @meta[:fields] + fields = fields - static.meta[:fields] if dynamic? + + decorate(fields) + fields.each do |name| + type_info = get_type_info(name) + proc = ListRendererPostProcessors[type_info[:type]] + proc.(self, name, type_info) if proc + end + end + + super + end + end + + module ActionDeleteSupport + include ActionModelSupport + + def self.included action + action.http_method :delete + action.action_type :delete + end + + def pre_run + super + execute "action.errors || [action.parent().invoke(), action.panel_close()]" + node.parent.parent.*.menu(:item_menu).option :confirm_delete, icon: "trash", show: "action.selected_size() == 0", button_loc: false + end + end + + module ActionBulkDeleteSupport + include ActionModelSupport + + def self.included action + action.http_method :delete + action.action_type :bulk_delete + end + + def pre_run + super + execute "action.errors || [action.parent().invoke(), action.panel_close()]" + node.parent.parent.*.select_toggle_menu + node.parent.parent.*.menu(:menu).option_after :default_order, :confirm_bulk_delete, icon: "trash", show: "action.selected_size() > 0" + end + end + + (FormRendererPostProcessors ||= {}).merge!( + boolean: lambda{|action, field, info| + action.info(field)[:render].merge! true_value: info[:true_value], false_value: info[:false_value] + action.info(field)[:dont_strip] = info[:dont_strip] if info[:dont_strip] + }, + date: lambda{|action, field, info| + action.info(field)[:render].merge! format: info[:format], model_format: info[:model_format] + if date_to = info[:other_date] + action.info(field)[:render].merge! other_date: date_to #, format: info[:format], model_format: info[:model_format] + action.hide_fields date_to + elsif time = info[:other_time] + action.info(field)[:render].merge! other_time: time + action.hide_fields time + end + }, + time: lambda{|action, field, info| + action.info(field)[:render].merge! format: info[:format], model_format: info[:model_format] + }, + decimal_date: lambda{|action, field, info| + FormRendererPostProcessors[:date].(action, field, info) + action.info! field, type: :decimal_date + }, + decimal_time: lambda{|action, field, info| + FormRendererPostProcessors[:time].(action, field, info) + action.info! field, type: :decimal_time + }, + datetime: lambda{|action, field, info| + action.info(field)[:render].merge! date_format: info[:date_format], time_format: info[:time_format], date_model_format: info[:date_model_format], time_model_format: info[:time_model_format] + }, + currency: lambda{|action, field, info| + action.info(field)[:render].merge! symbol: info[:symbol] + }, + # date_range: lambda{|action, field, info| + # action.info[field][:render].merge! other_date: info[:other_date], format: info[:format], model_format: info[:model_format] + # action.hide_fields info[:other_date] + # action.info[field][:decimal_date] = true if info[:validations][:decimal_date] + # }, + list_select: lambda{|action, field, info| + action.info(field)[:render].merge! list: info[:list] + }, + many_to_one: lambda{|action, field, info| + field_info = action.info(field) + field_info[:assoc] = :"#{info[:assoc_name]}!" + field_info[:fields] = info[:keys] + field_info[:type] = info[:otype] + + (info[:keys] - [field]).each do |of| + f_info = action.info(of) + f_info[:hidden] = true + f_info[:type] = action.assets[:model].type_info[of].fetch(:otype) + end + }, + file_store: lambda{|action, field, info| + action.info(field)[:render].merge! multiple: info[:multiple] + }, + star_to_many_field: lambda{|action, field, info| + field_info = action.info(field) + field_info[:assoc] = :"#{info[:assoc_name]}!" + } + ) + + (ListRendererPostProcessors ||= {}).merge!( + boolean: lambda{|action, field, info| + action.info! field, type: :boolean # move to action ? + action.info(field)[:render] ||= {} + action.info(field)[:render].merge! true_value: info[:true_value], false_value: info[:false_value] + }, + list_select: lambda{|action, field, info| + action.info! field, type: :list_select + action.info(field)[:render] ||= {} + action.info(field)[:render].merge! list: info[:list] + }, + datetime: lambda{|action, field, info| + action.info! field, type: :datetime + }, + decimal_date: lambda{|action, field, info| + action.info! field, type: :decimal_date + }, + decimal_time: lambda{|action, field, info| + action.info! field, type: :decimal_time + }, + # date_range: lambda{|action, field, info| + # action.info[field][:type] = :decimal_date if info[:validations][:decimal_date] # ? :decimal_date : :date + # } + ) + + (SearchRendererPostProcessors ||= {}).merge!( + many_to_one: lambda{|action, field, info| + model = action.assets[:model] + if model.type_info[field] + keys = info[:keys] + else + action.check_static_action + model = model.many_to_one_associations[field.table].associated_class + keys = info[:keys].map{|k| model.table_name.q(k)} + end + + field_info = action.info(field) + field_info[:assoc] = :"#{info[:assoc_name]}!" + field_info[:fields] = keys + field_info[:type] = info[:otype] + + (keys - [field]).each do |of| + f_info = action.info(of) + raise E2Error.new("Missing searchable field: '#{of}' in model '#{action.assets[:model]}'") unless f_info + f_info[:hidden_search] = true + f_info[:type] = model.type_info[of].fetch(:otype) + end + }, + date: lambda{|action, field, info| + action.info(field)[:render] ||= {} + action.info(field)[:render].merge! format: info[:format], model_format: info[:model_format] # Model::DEFAULT_DATE_FORMAT + }, + decimal_date: lambda{|action, field, info| + SearchRendererPostProcessors[:date].(action, field, info) + } + ) + + (DefaultFormRenderers ||= {}).merge!( + date: lambda{|action, info| + info[:other_date] ? Templates.date_range : (info[:other_time] ? Templates.date_time : Templates.date_picker) + + }, + time: lambda{|action, info| Templates.time_picker}, + datetime: lambda{|action, info| Templates.datetime_picker}, + file_store: lambda{|action, info| Templates.file_store}, + blob: lambda{|action, info| Templates.blob}, # !!! + blob_store: lambda{|action, info| Templates.blob}, + foreign_blob_store: lambda{|action, info| Templates.blob}, + string: lambda{|action, info| Templates.input_text(info[:length])}, + text: lambda{|action, info| Templates.text}, + integer: lambda{|action, info| Templates.integer}, + decimal: lambda{|action, info| Templates.decimal}, + decimal_date: lambda{|action, info| DefaultFormRenderers[:date].(action, info)}, + decimal_time: lambda{|action, info| Templates.time_picker}, + email: lambda{|action, info| Templates.email(info[:length])}, + password: lambda{|action, info| Templates.password(info[:length])}, + # date_range: lambda{|action, info| Templates.date_range}, + boolean: lambda{|action, info| Templates.checkbox_buttons(optional: !info[:required])}, + currency: lambda{|action, info| Templates.currency}, + list_select: lambda{|action, info| + length = info[:list].length + if length <= 3 + Templates.list_buttons(optional: !info[:required]) + elsif length <= 15 + max_length = info[:list].max_by{|a|a.last.length}.last.length + Templates.list_bsselect(max_length, optional: !info[:required]) + else + max_length = info[:list].max_by{|a|a.last.length}.last.length + Templates.list_select(max_length, optional: !info[:required]) + end + }, + star_to_many_field: lambda{|action, info| Templates.scaffold}, + many_to_one: lambda{|action, info| + tmpl_type = info[:decode][:form] + case + when tmpl_type[:scaffold]; Templates.scaffold_picker + when tmpl_type[:list]; Templates.bsselect_picker + when tmpl_type[:typeahead];Templates.typeahead_picker + else + raise E2Error.new("Unknown decode type #{tmpl_type}") + end + }, # required/opt + ) + + (DefaultSearchRenderers ||= {}).merge!( + date: lambda{|action, info| SearchTemplates.date_range}, + decimal_date: lambda{|action, info| SearchTemplates.date_range}, + integer: lambda{|action, info| SearchTemplates.integer_range}, + string: lambda{|action, info| SearchTemplates.input_text}, + boolean: lambda{|action, info| SearchTemplates.checkbox_buttons}, + list_select: lambda{|action, info| + length = info[:list].length + if length <= 3 + SearchTemplates.list_buttons + elsif length <= 15 + # max_length = info[:list].max_by{|a|a.last.length}.last.length + SearchTemplates.list_bsselect(multiple: info[:multiple]) + else + # max_length = info[:list].max_by{|a|a.last.length}.last.length + SearchTemplates.list_select + end + }, + many_to_one: lambda{|action, info| + tmpl_type = info[:decode][:search] + case + when tmpl_type[:scaffold]; SearchTemplates.scaffold_picker(multiple: tmpl_type[:multiple]) + when tmpl_type[:list]; SearchTemplates.bsselect_picker(multiple: tmpl_type[:multiple]) + when tmpl_type[:typeahead];SearchTemplates.typeahead_picker + else + raise E2Error.new("Unknown decode type #{tmpl_type}") + end + } + ) +end