require 'singleton' module Edgarj module Drawer # Column-info classes to provide the following common methods: # # * label - for column header label in the list # * sort_key - to sort list # * column_value - cell text for the column in the list # * field - for the form # # and the following optional methods: # * tag_options # * column_header_label # # as wells as the following for backward compatibility: # * name # * type # # NOTE: ColumnInfo::* classes instances are cached during server process # lifetime so that dynamic object (like drawer) cannot be stored. module ColumnInfo # Abstract class for all of ColumnInfo class Base # for column header label in the list def label(vc) raise 'derived should implement' end def sort_key raise 'derived should implement' end # cell text for the column in the list # # @param rec [AR] # @param drawer [Edgarj::Drawer::Base] def column_value(rec, drawer) rec.inspect end # input field for form # # @param rec [AR] # @param form_drawer [Edgarj::FormDrawer::Base] def field(rec, form_drawer) form_drawer.draw_field(rec, self) end # HTML tag options (e.g. css-class) in Hash def tag_options {} end # draw column header (with sort link) # # @param vc [ViewContext] Rails view_context # @param path_info [Edgarj::PageInfo] # @param options [Hash] options to url_for def column_header_label(vc, page_info, options) _label = label(vc) dir = 'asc' if page_info.order_by == sort_key # toggle direction if page_info.dir == 'asc' || page_info.dir.blank? _label += '▲' dir = 'desc' else _label += '▼' end end vc.link_to(_label, { :action => 'page_info_save', :id => page_info.id, 'edgarj_page_info[order_by]' => sort_key, 'edgarj_page_info[dir]' => dir }.merge(options), :remote => true, :method => :put) end def name raise 'derived should implement' end # just for backward compatibility def type raise 'derived should implement' end end # ActiveRecord::ConnectionAdapters::[DRIVER]::Column wrapper class Normal < Base # @param model [AR] # @param name [String] def initialize(model, name) @model = model @name = name @ar_column_info = model.columns_hash[name] end def label(vc) vc.column_label(@name) end # return table_name + col.name for sort def sort_key @model.table_name + '.' + @name end # draw rec.col other than 'belongs_to' # # === INPUTS # rec:: AR instance def column_value(rec, drawer) if (enum = drawer.vc.get_enum(rec.class, @ar_column_info)) drawer.vc.draw_column_enum(rec, @ar_column_info, enum) else case @ar_column_info.type when :datetime drawer.vc.datetime_fmt(rec.send(name)) when :date drawer.vc.date_fmt(rec.send(name)) when :integer rec.send(name).to_s when :boolean rec.send(name) ? '√' : '' else # NOTE: rec.send(col.name) is not used since sssn.send(:data) # becomes hash rather than actual string-data so that following # split() fails for Hash. if str = rec.attributes[name] draw_trimmed_str(str) else '' end end end end def tag_options case @ar_column_info.type when :integer {class: 'align_right'} when :boolean {class: 'align_center'} else {} end end # just for backward compatibility def name @name end # just for backward compatibility def type @ar_column_info.type end private # trim string when too long def draw_trimmed_str(str) s = str.split(//) if s.size > Edgarj::LIST_TEXT_MAX_LEN s = s[0..Edgarj::LIST_TEXT_MAX_LEN] << '...' end s.join('') end end # auto-generated column-info for 'belongs_to' column # # parent model is assumed to have 'name' method class BelongsTo < Normal def initialize(model, name, parent_model, belongs_to_link) super(model, name) @parent_model = parent_model @belongs_to_link = belongs_to_link end # column header for 'belongs_to' column prints label without # any sort action unlike Normal-class behavior. def column_header_label(vc, page_info, options) vc.draw_belongs_to_label_sub(@model, name, @parent_model) end def column_value(rec, drawer) @parent_rec = rec.belongs_to_AR(@ar_column_info) if @belongs_to_link drawer.vc.link_to(@parent_rec.name, drawer.popup_path(self), remote: true) else @parent_rec ? @parent_rec.name : '' end end end end # 'Mediator' to draw list and form of the model on the view. # # This collaborates with the following sub classes: # Edgarj::ListDrawer::Normal:: for list # Edgarj::FormDrawer::Base:: for data entry form # Edgarj::FormDrawer::Search:: for search form class Base attr_accessor :vc, :params, :model, :page_info # * options # * list_drawer_options - options for Edgarj::ListDrawer::Normal # * draw_form_options - options for draw_form_options def initialize(view_context, params, page_info, model, options={}) @vc = view_context @params = params @page_info = page_info @model = model @options = options.dup end # level-1 methods which may be modified most frequently: # define model-wide default columns for view. # # If you need to customize, overwrite it at derived model class. # Example: # def columns # %w(id name email updated_at) # end # # === SEE ALSO # list_columns:: define list columns # form_columns:: define form columns # search_form_columns:: define search form columns def columns @model.columns.map{|c| c.name} end # This defines list columns. # You can overwrite this method at each model if it is different from # columns. Default is calling columns(). # # === SEE ALSO # columns:: define default columns # form_columns:: define form columns # search_form_columns:: define search form columns # def list_columns columns end # This defines form columns. # You can overwrite this method at each model if it is different from # columns. Default is calling columns(). # # === SEE ALSO # columns:: define default columns # list_columns:: define list columns # search_form_columns:: define search form columns # def form_columns columns end # This defines search-form columns. # You can overwrite this method at each model if it is different from # columns. Default is calling columns(). # # === SEE ALSO # columns:: define default columns # list_columns:: define list columns # form_columns:: define form columns # def search_form_columns columns end # level-2 methods which may be modified occasionally: # This defines popup path for the column on the model. # # Default returns parent model's popup-controller. # For example, book.author_id -> 'authors_popup' path # # You can overwrite this method at each model if it is different from # columns. # # @see popup_path_on_search popup path for the column on the model's search form def popup_path(col) parent_model = @model.belongs_to_AR(col) raise 'Parent is nil' if !parent_model popup_field = PopupHelper::PopupField.new_builder(@model.model_name.param_key, col.name) @vc.main_app.url_for( controller: parent_model.model_name.collection + '_popup', id_target: popup_field.id_target) end # This defines popup path for the search column on the model. # # Default returns parent model's popup-controller with id_target # on the search column. # # @see popup_path popup path for the column on the model itself def popup_path_on_search(col) parent_model = @model.belongs_to_AR(col) raise 'Parent is nil' if !parent_model popup_field = PopupHelper::PopupField.new_builder(Edgarj::SearchForm.model_name.param_key, col.name) @vc.main_app.url_for( controller: parent_model.model_name.collection + '_popup', id_target: popup_field.id_target) end def list_drawer_class Edgarj::ListDrawer::Normal end def url_for_show(record) @vc.url_for(action: 'show', id: record.id, format: :js) end def draw_row(record, &block) @vc.content_tag(:tr, class: "list_line#{@line_color} edgarj_row", data: {url: url_for_show(record)}) do yield end end def draw_list(list) @line_color = 1 d = list_drawer_class.new( self, @options[:list_drawer_options] || {}) @vc.content_tag(:table, width: '100%', class: 'list') do @vc.content_tag(:tr) do for col in columns_for(list_columns, :list) do @vc.concat d.draw_column_header(col) end end + @vc.capture do for rec in list do @line_color = 1 - @line_color @vc.concat(draw_row(rec) do @vc.capture do for col in columns_for(list_columns, :list) do @vc.concat d.draw_column(rec, col) end end end) end end end end # overwrite to replace form drawer for the model def form_drawer_class Edgarj::FormDrawer::Base end def draw_form(record) url_hash = { controller: @params[:controller], action: record.new_record? ? 'create' : 'update', } url_hash[:id] = record.id if record.persisted? @vc.draw_form_buttons(@options[:draw_form_options] || {}) + @vc.form_for(record, remote: true, url: url_hash, html: { id: '_edgarj_form', method: record.new_record? ? 'post' : 'put', multipart: true, #target: 'edgarj_form_frame' }) do |f| form_drawer_class.new(self, record, f).draw() + # to avoid submit on 1-textfield form when hit [ENTER] key ''.html_safe end end # cache ColumnInfo array per 'controller x kind' class ColumnInfoCache include Singleton def initialize #Rails.logger.debug('ColumnInfoCache initialized') @cache = {} end # return if @cache[controller][kind] exists def presence(controller, kind) if @cache[controller].nil? @cache[controller] = {} end @cache[controller][kind] end def set(controller, kind, val) if @cache[controller].nil? @cache[controller] = {} end @cache[controller][kind] = val end def cache @cache end end # return array of model columns (ActiveRecord::ConnectionAdapters::X_Column type). # # === INPUTS # column_name_list:: column name list # kind:: :list, :form, or :search_form def columns_for(column_name_list, kind = :default) if (val = ColumnInfoCache.instance.presence(@vc.controller.class, kind)) val else #Rails.logger.debug("ColumnInfoCache non-cached access for (#{@vc.controller.class.name} x #{kind})") ColumnInfoCache.instance.set(@vc.controller.class, kind, [].tap do |result| for col_name in column_name_list do result << if col_name.is_a?(ColumnInfo::Base) col_name elsif (col = @model.columns_hash[col_name]) if (parent = @model.belongs_to_AR(col)) ColumnInfo::BelongsTo.new(@model, col_name, parent, false) else ColumnInfo::Normal.new(@model, col_name) end end end end) end end # overwrite to replace form drawer for search def search_form_drawer_class Edgarj::FormDrawer::Search end def draw_search_form(record) @vc.form_for(record, url: {action: 'search'}, html: { id: '_edgarj_search_form', remote: true, method: :get}) do |f| f.fields_for(record._operator) do |o| search_form_drawer_class.new(self, record, f, o).draw() end + # to avoid submit on 1-textfield form when hit [ENTER] key ''.html_safe end end end end end