I've read over the documentation on generators, but can't find any information about creating views. Reading through the code in the Rails 3 Generators gem, I found that you can override the default templates by placing new ones in lib/generators/erb/scaffold/templates/. You can also specify which views you want to create in the scaffold_generator.rb file with a snippet like:
def available_views
['index', 'edit', 'show', 'new', '_form']
end
So my question is, what if I wanted to create both an index.html.erb file and an index.js.erb file?
Apparently, the actual creation of the views is done by a function called copy_view_files. You can specify what type of view you want within that function. After doing so, my scaffold_generator.rb looks like this:
require 'rails/generators/erb/scaffold/scaffold_generator'
module Erb
module Generators
class ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
source_root File.expand_path(File.join('..', 'templates'), __FILE__)
def copy_view_files
primary_views.each do |view|
filename = filename_with_extensions view
template "#{view}.html.erb", File.join('app', 'views', controller_file_path, filename)
template "#{view}.js.erb", File.join('app', 'views', controller_file_path, filename)
end
end
hook_for :form_builder, :as => :scaffold
protected
def primary_views
['index', 'edit', 'show', 'new']
end
def handler
:erb
end
end
end
end
Note that within the copy_view_files function, there are two calls to template. The first creates a set of html views, while the second creates the js views I wanted.
Similar answer I posted to the question https://stackoverflow.com/a/62441675/385730.
You can override the scaffold generator lib/rails/generators/erb/scaffold/scaffold_generator.rb file.
Step 1:
Copy latest scaffold_generator.rb file.
mkdir -p lib/rails/generators/erb/scaffold && cp $(bundle show railties)/lib/rails/generators/erb/scaffold/scaffold_generator.rb lib/rails/generators/erb/scaffold/
Step 2:
Add custom code to generate .js.erb files you want.
# frozen_string_literal: true
require "rails/generators/erb"
require "rails/generators/resource_helpers"
module Erb # :nodoc:
module Generators # :nodoc:
class ScaffoldGenerator < Base # :nodoc:
include Rails::Generators::ResourceHelpers
argument :attributes, type: :array, default: [], banner: "field:type field:type"
def create_root_folder
empty_directory File.join("app/views", controller_file_path)
end
def copy_view_files
available_views.each do |view|
formats.each do |format|
filename = filename_with_extensions(view, format)
template filename, File.join("app/views", controller_file_path, filename)
end
end
javascript_views.each do |view|
path = File.join('app', 'views', controller_file_path, "#{view}.js.erb")
File.open(path, "w")
end
end
private
def available_views
%w(index edit show new _form)
end
def javascript_views
%w(index show create update)
end
end
end
end
Now when you run your scaffold generator you'll see the new .js.erb files that are created.
Related
I'm making an export to csv file functionality in a Ruby on Rails repo and I'm almost done. However, when I press the "Export all" button, I get the undefined method `export' for nil:NilClass error. The log shows that format.csv { send_data #foos.export, filename: "foos-#{Date.today}.csv" } went wrong. What am I missing please?
This is model
class Foo < ApplicationRecord
has_many :bars
def export
[id, name, foos.map(&:name).join(' ')]
end
end
This is part of controller
def index
#foos = Foo.all
end
def export
all = Foo.all
attributes = %w{name}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |foo|
csv << attributes.map{ |attr| foo.send(attr) }
end
respond_to do |format|
format.csv { send_data #foos.export, filename: "foos-#{Date.today}.csv" }
end
end
end
def name
"#{foo_id} #{name}"
end
This is View
<button class="btn btn-success">export all</button>
This is Routes
Rails.application.routes.draw do
resources :foos
get :export, controller: :foos
root "foos#index"
end
This is Rake (lib/tasks/export.rb)
namespace :export do
task foo: :environment do
file_name = 'exported_foo.csv'
csv_data = Foo.to_csv
File.write(file_name, csv_data)
end
end
Start by creating a service object that takes a collection of records and returns CSV so that you can test the CSV generation in isolation:
# app/services/foo_export_service.rb
# Just a Plain Old Ruby Object that converts a collection of foos into CSV
class FooExportService
# The initializer gives us a good place to setup our service
# #param [Enumerable] foo - an array or collection of records
def initialize(foos)
#headers = %w{name} # the attributes you want to use
#foos = foos
end
# performs the actual work
# #return [String]
def perform
CSV.generate do |csv|
#foos.each do |foo|
csv << foo.serializable_hash.slice(#headers).values
end
end
end
# A convenient factory method which makes stubbing the
# service easier
# #param [Enumerable] foos - an array or collection of records
# #return [String]
def self.perform(foos)
new(foos).perform
end
end
# example usage
FooExportService.perform(Foo.all)
Not everything in a Rails application needs to be jammed into a model, view or controller. They already have enough responsiblities. This also lets you resuse the code for example in your rake task if you actually need it.
This simply iterates over the collection and uses Rails built in serialization features to turn the model instances into hashes that can be serialized as CSV. It also uses the fact that Hash#slice also reorders the hash keys.
In your controller you then just use the service object:
class FoosController
def export
#foos = Foo.all
respond_to do |format|
format.csv do
send_data FooExportService.perform(#foos),
filename: "foos-#{Date.today}.csv"
end
end
end
end
You don't even really need a separate export action in the first place. Just use MimeResponds to add CSV as an availble response format to the index:
class FoosController
def index
# GET /foos
# GET /foos.csv
#foos = Foo.all
respond_to do |format|
format.html
format.csv do
send_data FooExportService.perform(#foos),
filename: "foos-#{Date.today}.csv"
end
end
end
end
<%= link_to("Export as CSV", foos_path(format: :csv)) %>
I used to generate PDFs for my users by using the wicked_pdf gem and writing
something like the example code below:
class ThingsController < ApplicationController
def show
respond_to do |format|
format.html
format.pdf do
render pdf: "file_name" # Excluding ".pdf" extension.
end
end
end
end
I would then have a link on a page like <%= link_to report_pdf_path(#report), "Download PDF", target: "_blank" %> that would cause the PDF to show up in the user's browser as a new tab. This is the preferred behavior I would like, but it halts all requests until the PDF is completed for the user and some of these take quite some time to generate.
So I've since offloaded the generating of the PDF to ActiveJob which works nice, but I can't figure out how to have ActiveJob open the file in a new window yet. Currently, I have it writing to the server and then updating a partial that shows the files the user has requested. Below is an example of it.
class GeneratePdfJob < ApplicationJob
queue_as :default
def perform(*args)
params = args.first
generate_pdf_document(params)
end
def generate_pdf_document(params)
html = ApplicationController.new.render_to_string(
template: 'players/board_labels.pdf.erb',
locals: { player_ids: params[:player_ids] }
)
save_to_pdf(html, params[:pdf_title], params[:user_code])
end
def save_to_pdf(html, pdf_title, user_code)
pdf = WickedPdf.new.pdf_from_string(
html,
pdf: "#{pdf_title}",
layout: 'print',
encoding: 'utf-8'
)
pdf_name = "#{pdf_title}.pdf"
pdf_dir = Rails.root.join('public','uploads','reports',"#{user_code}")
pdf_path = Rails.root.join(pdf_dir,pdf_name)
# create the folder if it doesn't exist
FileUtils.mkdir_p(pdf_dir) unless File.directory?(pdf_dir)
# create a new file
File.open(pdf_path,'wb') do |file|
file.binmode
file << pdf.force_encoding("UTF-8")
end
end
end
So how can I make a method that would replace the save_to_pdf method and instead open the file in a new tab for the user? If this isn't possible could I open a tab when I initiate the ActiveJob that is a placeholder tab and then incorporate ActionCable to display the file some how after it's been generated for the user?
i'm working on a Rails app.
And i'm trying to write a controller that will have access on the files of the Public folder of rails.
And i'm kind a stuck right now.
What i'm trying to do is a get method on my FileController.
That will obtain a variable "path" who correspond to the path of the file to read who is located on the public folder of rails.
And i want to modify routes.rb for when i make a GET request of "/file/path/to/htmlpage" the request sendback to me the content of the file, like a string for an HTML file.
EDIT: The problem is that is need REST url like this.
show => GET /file/:path
create => POST /file/:path
update => PUT /file/:path
destroy => DELETE /file/:path
So i deleted my old routes and put
resources :files
And my old routes wad like that
#get "file/:path" => 'file#get', as: :get
#get "file/create/*path" => 'file#create', as: :create
#post "file/create/*path/:content" => 'file#create', as: :create_content
#get "file/update/*path/:content" => 'file#update', as: :update
#get "file/destroy/:path" => 'file#destroy', as: :destroy
My controller is maybe a little bit weird, i need some advice.
class FileController < ApplicationController
# get "file/:path"
def show
path = params[:path]
if File.exists?( Rails.public_path.join( "#{path}.html" ) )
#content = File.read( Rails.public_path.join( "#{path}.html" ) )
puts #content
else
puts "The file you want to access doesn't exist"
end
end
# get "file/create/:path"
# post "file/create/*path/:content"
def create
path = params[:path]
content = params[:content]
if File.exists?( Rails.public_path.join( "#{path}.html" ) )
puts "The file you want to create already exist"
else
File.write(Rails.public_path.join( "#{path}.html" ), "#{content}")
end
end
# get "file/update/*path/:content"
def update
path = params[:path]
content = params[:content]
if File.exists?( Rails.public_path.join( "#{path}" ) )
File.write(Rails.public_path.join( "#{path}" ), "#{content}")
else
puts "The file you want to update doesn't exist"
end
end
# get "file/destroy/:path"
def destroy
path = params[:path]
if File.exists?( Rails.public_path.join( "#{path}.html" ) )
File.delete( Rails.public_path.join( "#{path}.html" ) )
else
puts "The file you want to delete doesn't exist"
end
end
end
But now it's not working, i think i have a problem with my routes and my controller that don't communicate in the right way. But i don't see how i have do to it.
I don't have a file model because i don't see the need for what i want to do.
Thanks for your help
Try this:
# FileController
def action_name
#check and filter here your path param, something like:
path = params[:path].gsub /[^a-z0-9\.\/]+/, ''
if File.exists?( Rails.root.join( "public/#{path}" ) )
#content = File.read( Rails.root.join( "public/#{path}" ) )
end
end
There are several answers to your question that yield different results.
If you want to load the file inside ruby, you can reference the public path like: Rails.public_path. In addition, you can get the root path to your application via Rails.root. That should help you get access to the files you need inside your controller.
However I have to ask: why do you want to do this? If you simply want to serve up the file, you can let rails do that by default by simply providing the path in the link itself - it will serve up the public files. In addition, it's quite likely you don't want to push that through your ruby/rails at all, and want to setup your HTTP server to serve the static files for you which will yield a dramatic performance benefit.
I would use a model like this:
class PublicFile
attr_accessor :path, :content
def save(params)
File.write(params[:path], params[:content])
end
def destroy(params)
File.delete(params[:path])
end
def initialize(params)
#content = File.read(params[:path])
#path = params[:path]
end
def find(path_or_paths)
# to implement
end
def all
# to implement
end
def update_attributes(params)
# to implement
end
end
In controller and views you could generate a scaffold rails g scaffold PublicFile, you need to move the model before running the scaffold, and then use the above model that manipulates files.
In conclusion, what I mean is to change the DB that an usual Rails model has, with the access to files, this would be done in model logic.
Is there a (simple) way to configure the default generator to also generate a js.erb file for each action in addition to html.erb files?
You can override the scaffold generator lib/rails/generators/erb/scaffold/scaffold_generator.rb file.
Step 1:
Copy latest scaffold_generator.rb file.
mkdir -p lib/rails/generators/erb/scaffold && cp $(bundle show railties)/lib/rails/generators/erb/scaffold/scaffold_generator.rb lib/rails/generators/erb/scaffold/
Step 2:
Add custom code to generate .js.erb files you want.
# frozen_string_literal: true
require "rails/generators/erb"
require "rails/generators/resource_helpers"
module Erb # :nodoc:
module Generators # :nodoc:
class ScaffoldGenerator < Base # :nodoc:
include Rails::Generators::ResourceHelpers
argument :attributes, type: :array, default: [], banner: "field:type field:type"
def create_root_folder
empty_directory File.join("app/views", controller_file_path)
end
def copy_view_files
available_views.each do |view|
formats.each do |format|
filename = filename_with_extensions(view, format)
template filename, File.join("app/views", controller_file_path, filename)
end
end
javascript_views.each do |view|
path = File.join('app', 'views', controller_file_path, "#{view}.js.erb")
File.open(path, "w")
end
end
private
def available_views
%w(index edit show new _form)
end
def javascript_views
%w(index show create update)
end
end
end
end
Now when you run your scaffold generator you'll see the new .js.erb files that are created.
The answer from #dale-zak it created an empty js file without the contents from my lib/templates/erb/scaffold/index.js.erb.tt
That code with this loop works for me:
javascript_views.each do |view|
filename = filename_with_extensions(view, :js)
template filename, File.join("app/views", controller_file_path, filename)
end
I'm trying to encapsulate the logic for generating my sitemap in a separate class so I can use Delayed::Job to generate it out of band:
class ViewCacher
include ActionController::UrlWriter
def initialize
#av = ActionView::Base.new(Rails::Configuration.new.view_path)
#av.class_eval do
include ApplicationHelper
end
end
def cache_sitemap
songs = Song.all
sitemap = #av.render 'sitemap/sitemap', :songs => songs
Rails.cache.write('sitemap', sitemap)
end
end
But whenever I try ViewCacher.new.cache_sitemap I get this error:
ActionView::TemplateError:
ActionView::TemplateError (You have a nil object when you didn't expect it!
The error occurred while evaluating nil.url_for) on line #5 of app/views/sitemap/_sitemap.builder:
I assume this means that ActionController::UrlWriter is not included in the right place, but I really don't know
Does this do what you're trying to do? This is untested, just an idea.
in lib/view_cacher.rb
module ViewCacher
def self.included(base)
base.class_eval do
#you probably don't even need to include this
include ActionController::UrlWriter
attr_accessor :sitemap
def initialize
#av = ActionView::Base.new(Rails::Configuration.new.view_path)
#av.class_eval do
include ApplicationHelper
end
cache_sitemap
super
end
def cache_sitemap
songs = Song.all
sitemap = #av.render 'sitemap/sitemap', :songs => songs
Rails.cache.write('sitemap', sitemap)
end
end
end
end
then wherever you want to render (I think your probably in your SitemapController):
in app/controllers/sitemap_controller.rb
class SitemapController < ApplicationController
include ViewCacher
# action to render the cached view
def index
#sitemap is a string containing the rendered text from the partial located on
#the disk at Rails::Configuration.new.view_path
# you really wouldn't want to do this, I'm just demonstrating that the cached
# render and the uncached render will be the same format, but the data could be
# different depending on when the last update to the the Songs table happened
if params[:cached]
#songs = Song.all
# cached render
render :text => sitemap
else
# uncached render
render 'sitemap/sitemap', :songs => #songs
end
end
end