I have a web project that will render an HTML file, that is based on various .erb files.
I'm not sure about the best way, to do this, since each .erb file need to get specific information, such as cookie content.
Currently I have used this concept:
I have a directory with all of my .erb files, which get rendered using:
ERB.new(template).result
the rendered HTML will get returned to the main .erb template, which will get again rendered by sinatra, using:
erb :main
the result is pretty good, but i don't have the chance to include content from session based cookies, since .erb can not access them
I am pretty sure, the sinatra framework provides a better way to do this. A good way would be...
require 'sinatra'
enable :sessions
get "/" do
content1 = erb :template1, :locals => { :cookie => session[:cookie] }
content2 = erb :template2, :locals => { :cookie => session[:cookie] }
erb :mainTemplate, :locals => { :content => [content1, content2] }
end
... but, unfortunately it doesn't work that easy :(
Does anybody has a better idea?
Here is what I did for mine:
get '/Login' do verbose = params['verbose']
cTest = MyTests.new("envo")
result = cTest.login()
if verbose == 'true'
erb :newResultPage, :locals => {:result => result}
elsif verbose == 'false'
erb :resultPage, :locals => {:result => result}
end
end
Basically, I use a conditional statement to determine which page to use. This one is based on parameters. You could also use return results, or what ever other conditions you like.
Related
my simple Webrick server serves up static html and rhtml embedded ruby files. How would I achieve the same with a multithreaded or multiprocess Ruby server like Thin?
Webrick setup:
#!/usr/bin/ruby
# simple_servlet_server.rb
require 'webrick'
include WEBrick
s = HTTPServer.new(:Port => 8000)
# Add a mime type for *.rhtml files
HTTPUtils::DefaultMimeTypes.store('rhtml', 'text/html')
s.mount('/', HTTPServlet::FileHandler, './public_html')
['TERM', 'INT'].each do |signal|
trap(signal){ s.shutdown }
end
s.start
I have installed Thin and Rack, my config.ru reads HTML but wont render rhtml docs?:
use Rack::Static,
:urls => ["/images", "/js", "/css"],
:root => "public"
run lambda { |env|
[
200,
{
'Content-Type' => 'text/html',
'Cache-Control' => 'public, max-age=86400'
},
File.open('./public_html', File::RDONLY)
]
HTTPUtils::DefaultMimeTypes.store('rhtml', 'text/html')
}
TL;DR; - Dynamic ruby content (i.e., .rhtml files) needs to be rendered by a template engine; The built-in engine used by WebRick is Ruby's ERB engine; Render dynamic files before sending the Rack response.
I would recommend that you separate the static files from the dynamic files.
This would allow you to optimize the static file service using either the reverse proxy (i.e., nginx) or a static file enabled Ruby server (i.e., iodine).
A common approach is to:
Place static files under ./public.
Place dynamic files under ./app/views.
Your Rack application will need to process the dynamic files using the appropriate template engine processor.
For your .rhtml files I would assume that this would be the built-in ERB template engine (there's a nice post about it here).
Assuming you placed your dynamic files and static files as mentioned, you might start up with a config.ru file that looks like this:
require 'erb'
require 'pathname'
ROOT ||= Pathname.new(File.dirname(__FILE__)).expand_path
module APP
VIEWS = ROOT.join('app', 'views')
def self.call(env)
# prevent folder trasversal (security) and extract file name
return [403, {}, ["Not Allowed!"]] if env['PATH_INFO'].index("..") || env['PATH_INFO'].index("//")
filename = VIEWS.join(env['PATH_INFO'][1..-1])
# make sure file exists
return [404, {}, ["Not Found"]] unless File.exists?(filename)
# process dynamic content
template = IO.binread filename
data = ERB.new(template).result(binding)
return [200, {}, [data]]
end
end
run APP
Next you can run the application with iodine, which will handle the static file service part (in this example, a single threaded worker per core):
iodine -w -1 -t 1 -www public
Of course, you can use the Rack::Static middleware, but it should prove to be significantly slower (bench mark it yourself or test it against Thin)...
...I'm iodine's author, so I might be biased.
EDIT
P.S. (side note about security and performance)
I would reconsider the template engine.
ERB is fast and effective, however, it also allows code to be executed within the template.
This could result in difficult project maintenance as code leaks into the template, making the code less readable and harder to maintain.
I would consider switching to Mustache templates, which prevent code from running within the template.
Changing the template engine could also improve performance. Consider the following benchmark, using iodine flavored mustache templates (which offer aggressive HTML escaping):
require 'iodine'
require 'erb'
require 'benchmark'
module Base
ERB_ENG = ERB.new("<%= data %> <%= Time.now %>")
MUS_ENG = Iodine::Mustache.new(nil, "{{ data }} {{ date }}")
def self.render_erb(data)
ERB_ENG.result(binding)
end
def self.render_mus(data)
h = {data: data, date: Time.now.to_s }
MUS_ENG.render(h)
end
end
puts "#{Base.render_mus("hello")} == #{Base.render_erb("hello")} : #{(Base.render_mus("hello") == Base.render_erb("hello"))}"
TIMES = 100_000
Benchmark.bm do |bm|
bm.report("Ruby ERB") { TIMES.times { Base.render_erb("hello") } }
bm.report("Iodine ") { TIMES.times { Base.render_mus("hello") } }
end
The results, on my machine, were (lower is better):
user system total real
Ruby ERB 1.701363 0.006301 1.707664 ( 1.709132)
Iodine 0.256918 0.000750 0.257668 ( 0.258190)
Again, since I'm iodine's author, I'm biased. Play around and find what's best for you.
How would you make an erb template that has human readable json?
The following code works, but it makes a flat json file
default.rb
default['foo']['bar'] = { :herp => 'true', :derp => 42 }
recipe.rb
template "foo.json" do
source 'foo.json.erb'
variables :settings => node['foo']['bar'].to_json
action :create
end
foo.json.erb
<%= #settings %>
Similar SO questions
Chef and ruby templates - how to loop though key value pairs?
How can I "pretty" format my JSON output in Ruby on Rails?
As pointed out by this SO Answer .erb templates are great for HTML, and XML, but is not good for json.
Luckily, CHEF uses its own json library which has support for this using .to_json_pretty
#coderanger in IRC, pointed out that you can use this library right inside the recipe. This article shows more extensively how to use chef helpers in recipes.
default.rb
# if ['foo']['bar'] is null, to_json_pretty() will error
default['foo']['bar'] = {}
recipe/foo.rb
pretty_settings = Chef::JSONCompat.to_json_pretty(node['foo']['bar'])
template "foo.json" do
source 'foo.json.erb'
variables :settings => pretty_settings
action :create
end
Or more concise as pointed out by YMMV
default.rb
# if ['foo']['bar'] is null, to_json_pretty() will error
default['foo']['bar'] = {}
recipe/foo.rb
template "foo.json" do
source 'foo.json.erb'
variables :settings => node['foo']['bar']
action :create
end
templates/foo.json.erb
<%= Chef::JSONCompat.to_json_pretty(#settings) %>
Something like this would also work:
file "/var/my-file.json" do
content Chef::JSONCompat.to_json_pretty(node['foo']['bar'].to_hash)
end
<%= Chef::JSONCompat.to_json_pretty(#settings) %> Works like Charm !!
Hi I'm having trouble downloading multiple files with axlsx. The problem is I'm sending an array of Id's to the controller and asking it to download the report using the render command. It raises an AbstractController::DoubleRenderError. I was thinking of overriding the error but realized it's a bad idea, I don't know what else to do... Any suggestions? Thanks.
My controller code looks like this:
def download_report
params[:user_id].each do |user_id|
#report = Report.find_by(:user_id => user_id)
render :xlsx => "download_report", :filename => "#{#report.user.last_name}.xlsx"
end
end
My axlsx template:
wb = xlsx_package.workbook
wb.add_worksheet(name: "Reports") do |sheet|
wb.styles do |s|
# template code
end
end
It is the built in expectation of Rails that you would call render once per request. And, the browser is going to expect one response per request. So, you are going to have to do something else!
You can use render_to_string, and combine the results into a zip file, serving that. See the bottom of this response.
Or, you could create a single spreadsheet and have each user's report show up on their own worksheet.
Or, on the client side, you could use javascript to request each spreadsheet and download each one separately.
The zip one would be something like this code, which uses render_to_string, rubyzip, and send_data:
def download_report
compressed_filestream = Zip::ZipOutputStream.write_buffer do |zos|
params[:user_id].each do |user_id|
#report = Report.find_by(:user_id => user_id)
content = render_to_string :xlsx => "download_report", :filename => "#{#report.user.last_name}.xlsx"
zos.put_next_entry("user_#{user_id}.xlsx")
zos.print content
end
end
compressed_filestream.rewind
send_data compressed_filestream.read, :filename => 'download_report.zip', :type => "application/zip"
end
Axlsx requires rubyzip, so you should have it already. And you probably want to lookup each user and use their name for the spreadsheet, unless you have it otherwise.
I'd like to to show a message only if on a specific route/page. Essentially, if on /route display a message.
I tried going through the Sinatra Docs, but I can't find a specific way to do it. Is there a Ruby method that will make this work?
EDIT: Here's an example of what I'd like to do.
get '/' do
erb :index
end
get '/page1' do
erb :page1
end
get '/page2' do
erb :page2
end
*******************
<!-- Layout File -->
<html>
<head>
<title></title>
</head>
<body>
<% if this page is 'page1' do something %>
<% else do something else %>
<% end %>
<%= yield %>
</body>
</html>
No idea what how to target the current page using Ruby/Sinatra and structure it into an if statement.
There are several ways to approach this (and BTW, I'm going to use Haml even though you've used ERB because it's less typing for me and plainly an improvement). Most of them rely on the request helper, most often it will be request.path_info.
Conditional within a view.
Within any view, not just a layout:
%p
- if request.path_info == "/page1"
= "You are on page1"
- else
= "You are not on page1, but on #{request.path_info[1..]}"
%p= request.path_info == "/page1" ? "PAGE1!!!" : "NOT PAGE1!!!"
A conditional with a route.
get "/page1" do
# you are on page1
message = "This is page 1"
# you can use an instance variable if you want,
# but reducing scope is a best practice and very easy.
erb :page1, :locals => { message: message }
end
get "/page2" do
message = nil # not needed, but this is a silly example
erb :page2, :locals => { message: message }
end
get %r{/page(\d+)} do |digits|
# you'd never reach this with a 1 as the digit, but again, this is an example
message = "Page 1" if digits == "1"
erb :page_any, :locals => { message: message }
end
# page1.erb
%p= message unless message.nil?
A before block.
before do
#message = "Page1" if request.path_info == "/page1"
end
# page1.erb
%p= #message unless #message.nil?
or even better
before "/page1" do
#message = "Hello, this is page 1"
end
or better again
before do
#message = request.path_info == "/page1" ? "PAGE 1!" : "NOT PAGE 1!!"
end
# page1.erb
%p= #message
I would also suggest you take a look at Sinatra Partial if you're looking to do this, as it's a lot easier to handle splitting up views when you have a helper ready made for the job.
Sinatra has no "controller#action" Rail's like concept, so you wont find a way to instantiate the current route. In any case, you can check request.path.split('/').last to get a relative idea of what is the current route.
However, if you want something so be shown only if request.path == "x", a much better way is to put that content on the template, unless that content has to be rendered in a different place within your layout. In that case you can use something like Rail's content_for. Check sinatra-content-for.
So I have sinatra setup with Rdiscount to render a markdown file with a HAML layout. This all works but I want RDiscount to generate a table of contents based on the headers in my haml file. I've tried setting it in the sinatra configuration.
set :markdown, :generate_toc => true
but that doesn't seem to work.
I've also tried doing it when I render the markdown like so:
markdown :PAGENAMEHERE, :layout => :'layouts/PAGENAMEHERE', :generate_toc => true
which also doesn't work.
Is this even possible? If it is, what am I doing wrong?
While #three's answer helped me a lot, I would like to show a perhaps more general solution:
class MDWithTOC < ::Tilt::RDiscountTemplate
def flags
[:generate_toc]
end
end
Tilt.register MDWithTOC, 'md'
Here we override the flags method of the RDiscount Tilt template handler and regiter it as a handler for the md format.
Now you can use the md helper as always and it will generate the TOC.
This should work:
get '/' do
text = File.read('README.md')
markdown = RDiscount.new(text, :generate_toc)
body = markdown.to_html
haml :home, :layout => true, :locals => {:body => body}
end
You create the body directly via RDiscount and include it as plain HTML in your HAML layout. Whatever prevented toc creation should work this way natively. I found the solution via https://github.com/mjijackson/markdown/blob/master/app.rb