require 'nitro/helper/form' require 'nitro/helper/pager' module Nitro # Scaffolding is a feature of Nitro that automatically generates # maintenance methods for you. It is typically used in the # early stages of development or by the auto administration # part. # # This module is applied to controllers to provide # automatically generated CRUD methods. #-- # TODO: add search, add base, add to_title, validate/error # handling. #++ module Scaffolding # The directory where the default scaffold templates reside. setting :template_root, :default => File.join(Nitro.proto_path, 'public', 'scaffold'), :doc => 'The directory where the default scaffold templates reside' # The items to show per page in the scaffolder lists. setting :per_page, :default => 20, :doc => 'The items to show per page in the scaffolder lists' def self.included(base) super base.helper(:form) base.helper(:pager) base.extend(ClassMethods) end def self.class_to_name(klass) klass.to_s.demodulize.underscore.downcase end def self.class_to_list(klass) klass.to_s.demodulize.underscore.downcase.plural end #-- # Compiles the scaffolding code. #++ class ScaffoldCompiler def initialize(controller, klass, options) @controller = controller @klass = klass @options = options @oid = options[:oid] || options[:pk] @name = options[:name] @plural = options[:plural_name] @suffix = options[:suffix] @compiler = Compiler.new(@controller) @base = options[:base] || options[:mount] || options[:at] if @base == true @base = @plural.dup elsif @base == :ROOT @base = nil end @base__ = "#{@base}__" if @base end def class_to_name(klass) klass.to_s.demodulize.underscore.downcase end #-- # A helper that defines a class method. #++ def define_class_method(meth, code) unless @klass.instance_methods.include?(meth.to_s) @klass.module_eval %{ def #{meth} #{code} end } end end #-- # A helper that defines a controller action. #++ def define_controller_action(definition, code, template = nil) sym, args = definition.to_s.split(/\(|\)/) # Access Namespace::Klass.action as namespace/klass/action. sym.gsub!(/::/, '__') if sym == 'index' action = "#{@base__}index" else action = "#@base__#{sym}#@suffix" end unless @controller.action?(action) @controller.module_eval %{ def #{action}(#{args}) @klass = #{@klass} #{code} end } end unless @compiler.template?(action) # Use the standard scaffold template. path = File.join(Scaffolding.template_root, "#{template || sym}.xhtml") if File.exist?(path) and source = File.read(path) # Interpolate the source. source.gsub!(/%base%/, @base.to_s) source.gsub!(/%name%/, @name) source.gsub!(/%plural%/, @plural) # Transform the source. source = @compiler.transform_template(source) @controller.module_eval %{ def #{action}_template #{source} end } end end end # Scaffold the class and the controller. def scaffold scaffold_class scaffold_controller end # Scaffold the class. def scaffold_class define_class_method :scaffold_base, %{ "#{@controller.ann.self.mount_point}/#@base" } define_class_method :to_href, %{ "#{@controller.ann.self.mount_point}/#@base/view/\#@#{@oid}" } define_class_method :to_edit_href, %{ "#{@controller.ann.self.mount_point}/#@base/edit/\#@#{@oid}" } end # Scaffold the controller. def scaffold_controller define_controller_action 'index', %{ #{Aspects.gen_advice_code(:scaffold_index, @controller.advices, :pre)} @list, @pager = paginate(@klass, :per_page => Scaffolding.per_page) } define_controller_action 'list', %{ #{Aspects.gen_advice_code(:scaffold_list, @controller.advices, :pre)} @list, @pager = paginate(@klass, :per_page => Scaffolding.per_page) } define_controller_action 'view(oid)', %{ #{Aspects.gen_advice_code(:scaffold_view, @controller.advices, :pre)} @obj = @klass[oid] } define_controller_action 'new(all = false)', %{ #{Aspects.gen_advice_code(:scaffold_new, @controller.advices, :pre)} @obj = @klass.new @all = all } define_controller_action 'edit(oid = nil, all = false)', %{ #{Aspects.gen_advice_code(:scaffold_edit, @controller.advices, :pre)} @obj = @klass[oid] @all = all } # FIXME: investigate save, improve. define_controller_action 'save', %{ if oid = request.fetch('#@oid', nil) oid = oid.to_s # fix StringIO (post). obj = request.fill(@klass[oid], :assign_relations => true, :force_boolean => true, :preprocess => true) else obj = request.fill(@klass.create, :assign_relations => true, :preprocess => true) end #{Aspects.gen_advice_code(:scaffold_save, @controller.advices, :pre)} unless obj.valid? flash[:ERRORS] = obj.errors redirect_to_referer end obj.save #{Aspects.gen_advice_code(:scaffold_save, @controller.advices, :post)} redirect '#{action_path(:list)}' } define_controller_action 'search(query)', %{ #{Aspects.gen_advice_code(:scaffold_search, @controller.advices, :pre)} @query = query if @klass.respond_to? :search @list = #@klass.search(query) end } define_controller_action 'delete(oid)', %{ #{Aspects.gen_advice_code(:scaffold_delete, @controller.advices, :pre)} @klass.delete(oid) #{Aspects.gen_advice_code(:scaffold_delete, @controller.advices, :post)} redirect_to_referer } # Actions for the relations for rel in @klass.relations define_controller_action "remove_#{rel.target_singular_name}(oid, rid)", %{ obj = @klass[oid] #{Aspects.gen_advice_code(:scaffold_remove_relation, @controller.advices, :pre)} rob = #{rel.target_class}[rid] obj.#{rel.name}.remove(rob) #{Aspects.gen_advice_code(:scaffold_remove_relation, @controller.advices, :post)} redirect_to_referer } define_controller_action "delete_#{rel.target_singular_name}(oid, rid)", %{ #{Aspects.gen_advice_code(:scaffold_delete_relation, @controller.advices, :pre)} obj = @klass[oid] rob = #{rel.target_class}[rid] obj.#{rel.name}.delete(rob) #{Aspects.gen_advice_code(:scaffold_delete_relation, @controller.advices, :post)} redirect_to_referer } end end private def action_method(sym) action = "#{@base__}#{sym}" action << @suffix unless @suffix.nil? or sym == :index return action end def action_path(sym) action = "#{@base}/#{sym}" action << @suffix unless @suffix.nil? or sym == :index return action end end module ClassMethods # This method modifies both the controller and the # scaffolded Class. # # # === Options # # * base/mount : mountpoint for the action, defines a # prefix for the scaffolded action. # If mount => true, the prefix is the plural of the # scaffolded class name. # If mount => :ROOT or nil, the actions are mounted at the # root of the controller. # # === Example # # scaffold Article # # ==== Actions/Methods added to the Controller # # * articles # * edit_article # * view_article # * delete_article # * search_artile # * query_article # # ==== Methods added to the Class (ex: Article) # # * to_href # * to_edit_href # #-- # This is the first phase of the scaffolding proccess, # scaffold targets are marked, and default options set. # # FIXME: Rethink the options, make :ROOT the default. #++ def scaffold(klass, options = {}) o = { :pk => 'oid', :name => Scaffolding.class_to_name(klass), :plural_name => Scaffolding.class_to_list(klass), :mount => true } o.merge!(options) if scaffolding_classes.has_key? klass scaffolding_classes[klass].merge!(o) else scaffolding_classes[klass] = o end end #-- # This is the second phase of the scaffolding process #++ def compile_scaffolding_code # load scaffolding for all og classes if specified if scaffold_all? options = scaffold_all_options Og.manager.manageable_classes.each do |klass| unless klass.ancestors.include?(Og::Unmanageable) or options[:except] && options[:except].include?(klass) scaffold(klass, options) end end scaffold_all false end # compile the scaffold methods scaffolding_classes.each do |klass, options| Scaffolding::ScaffoldCompiler.new(self, klass, options).scaffold end end #-- # set the options flag to trigger scaffolding of all classes # in second phase. # use exclude to ignore classes: # scaffold_all :exclude => [Product, Category] # # gmosx: I do NOT lke this, will remove this in a future # version. #++ def scaffold_all(options = {}) if options o = if options.kind_of? Hash then options else Hash.new end else o = nil end @scaffold_all_options = o end #-- # Keep track of scaffolded classes on this controller #++ def scaffolding_classes # TODO: maybe make this with a inhertitor? # although inhertited scaffolding might be pointless @scaffolding_classes ||= {} end #-- # Scaffold all classes flag #++ def scaffold_all_options @scaffold_all_options end alias_method :scaffold_all?, :scaffold_all_options end end end # * George Moschovitis