I want, from a helper, to render some variables in a .scss.erb template that makes use of the image-url() sass function:
// template.scss.erb
#<%= id %> {
background-image: image-url('<%= image_file %>');
}
So far, the ERB part has been easy:
(leveraging this stack overflow answer)
vars_binding = OpenStruct.new(
id: 'foo',
image_file: 'foo.jpg'
).instance_eval { binding }
template = File.read('path/to/template.scss.erb')
rendered_sass = ERB.new(template).result(vars_binding)
Running that code, sass is now equal to:
#foo {
background-image: image-url('foo.jpg');
}
However, when I next try to run:
css = Sass::Engine.new(
rendered_sass,
syntax: :scss,
cache: false,
load_paths: view_context.assets.paths,
read_cache: false,
style: :compressed
).render
It returns
NoMethodError: undefined method `[]' for nil:NilClass
from …/sprockets-3.2.0/lib/sprockets/sass_processor.rb:267:in `sprockets_context'
because the call to Sass::Engine doesn’t provide a Sprockets context.
If I remove image-url() from the .scss.erb template, and replace it with the native url(), it will then render correctly as CSS, no problem.
So how do I render this template within a sprockets context?
After digging through a similar question, and a lot of trial and error, I found my solution: I have to supply a :sprockets hash when calling Sass::Engine.new.
css = Sass::Engine.new(
rendered_sass,
syntax: :scss,
cache: false,
load_paths: view_context.assets.paths,
read_cache: false,
style: :compressed,
# The key ingredient…
sprockets: {
context: view_context,
environment: view_context.assets
}
).render
It should be noted that view_context was passed from a view file, but it could have also been ActionView::Base.new
After struggling with the same issue I have found a live example of the same functionality on a Github page, surprisingly not gemified.
https://github.com/BLauris/custom-css-for-user
There is an article of the author explaining the method on this link:
http://www.diatomenterprises.com/dynamically-compile-stylesheets-with-rails-and-sass/
Related
I'm using this tutorial to try to facilitate image uploads using the TinyMCE WYSIWYG editor on my rails 5.2 app.
So far I have implemented all the code in the tutorial and everything works perfectly, but when I try to upload an image I get a "Got a bad response from the server" error message.
In my heroku logs I then get this:
FATAL -- : [527c4468-9ebe-475a-97a9-380bfc327aab] ActionController::RoutingError (uninitialized constant #<Class:0x000055b6d991e020>::EditController
2020-02-22T17:34:07.814727+00:00 app[web.1]: Did you mean? DeviseController):
Here is the route I'm using:
post '/tinymce_assets', to: 'article/edit#image_upload'
With these controller methods:
def image_upload
file = params[:file]
url = upload_file(file)
render json: {
image: {
url: url
}
}, content_type: "text/html"
end
private
def upload_file(file)
s3 = Aws::S3::Resource.new(region:ENV['AWS_REGION'])
obj = s3.bucket(ENV['S3_BUCKET_NAME']).object('articles/images/content/' + filename(file))
obj.upload_file(file.tempfile, {acl: 'public-read'})
obj.public_url.to_s
end
def filename(file)
file.original_filename.gsub(/[^a-zA-Z0-9_\.]/, '_')
end
And this to initialize it:
<%= f.text_area :body, class: "tinymce", rows: 20, cols: 120 %>
<%= tinymce :content_css => asset_path('application.css')%>
...
<script>
$(document).ready(function() {
tinymce.init({
selector: "textarea.tinymce", // change this value according to your HTML
});
});
</script>
Can anyone see where I'm going wrong here?
Rails is trying to find an EditController because of your route: post '/tinymce_assets', to: 'article/edit#image_upload'
This route suggests that the controller it should look in is Article::EditController. Since you are actually looking for the ArticlesController you should change your route to be:
post '/tinymce_assets', to: 'articles#image_upload'
The Rails routing documentation can be helpful, specifically section 2.2 and 2.6. https://guides.rubyonrails.org/routing.html
After already checking the Rails Gem repository for similar issues as well as Stack Overflow, I couldn't find an answer to my problem.
I'm trying to render a pdf using wicked_pdf within a Rails controller, but the header is not showing up, no matter what I do or which recommended solutions to similar issues I follow.
First and foremost, here is the development console output:
***************WICKED***************
Rendering biddings/show.pdf.html.haml within layouts/pdf
Rendered biddings/show.pdf.html.haml within layouts/pdf (0.7ms)
Rendering biddings/header_pdf.html.haml within layouts/pdf_header
Rendered biddings/header_pdf.html.haml within layouts/pdf_header (1.9ms)
"***************[\"/home/tommy/.rvm/gems/ruby-
2.5.1#igalbids/bin/wkhtmltopdf\", \"-q\", \"--encoding\", \"UTF-8\",
\"--javascript-delay\", \"500\",
\"--disable-internal-links\", \"--disable-external-links\",
\"--orientation\", \"Portrait\", \"--margin-top\",
\"50\", \"--margin-bottom\", \"25\", \"--header-html\",
\"file:////tmp/wicked_header_pdf20180801-27285-b8y5sg.html\",
\"--footer-right\", \"Página [page] de [topage]\",
\"file:////tmp/wicked_pdf20180801-27285-1jfgdd7.html\",
\"/tmp/wicked_pdf_generated_file20180801-27285-1bkrvhx.pdf\"]***************"
Rendering text template
Rendered text template (0.1ms)
Sent data Licitación_2524.pdf (0.6ms)
Completed 200 OK in 2334ms (Views: 0.5ms | ActiveRecord: 64.4ms)
As you can see, both the header layout and its contents are being rendered and processed, however they don't make the final output PDF, and I don't know why! Look:
So, here's my controller code:
class Api::V1::Biddings::PdfBiddingsController < PdfController
# JWT Authentication enforced
before_action :authenticate_user!
# GET /biddings/:id/pdf
def show
#bidding = scoped_collection.find(params[:id])
authorize [:biddings, :pdf, #bidding]
respond_to do |format|
format.pdf do
render(
pdf: "#{Bidding.model_name.human}_#{#bidding.code}",
disposition: "inline",
orientation: "Portrait",
template: 'biddings/show.pdf.html.haml',
header: {
html: {
template: "biddings/header_pdf.html.haml",
handlers: [:haml],
layout: "pdf_header",
formats: [:haml, :html]
}
},
footer: {
html: {
handlers: [:haml],
layout: "pdf",
formats: [:haml, :html],
encoding: 'UTF-8'
},
right: "#{I18n.t('pdf.page')} [page] #{I18n.t('pdf.of')} [topage]"
},
margin: { :top => 50, :bottom => 25},
handlers: [:haml],
layout: "pdf",
javascript_delay: 500,
encoding: 'UTF-8',
show_as_html: false,
disable_internal_links: true,
disable_external_links: true) and return
end
end
end
protected
def self.model
Bidding
end
private
def scoped_collection
policy_scope([:biddings, :pdf, Bidding]).includes(:bidding_type, :client, :payment_condition, :price_list, :real_payment_condition, :sales_man, :user)
end
def records_per_page
params[:per_page] || 10
end
end
Nothing fancy, there you can see all the config options, pretty standard. Needless to say, the footer with the page numbering IS working fine (screenshot too long to show, but trust me). Can't say the same about the header.
Here's the PDF header layout file:
pdf_header.html.haml:
!!! 5
%html
%head
%meta{:content => "text/html; charset=utf-8", "http-equiv" => "content-type"}/
= wicked_pdf_stylesheet_link_tag "bidding_pdf", media: :all
= csrf_meta_tags
%body.pdf
= yield
and here the contents for the header "contents" per se:
header_pdf.html.haml:
Test text
Just plain text. I have a Linux 16.04 x64 OS, wicked_pdf (1.1.0), wkhtmltopdf-binary (0.12.4). How can I debug this?
For anyone who bumps in this, since OP's answer is not too precise, what did the job for me was including a DOCTYPE HTML tag in your header/footer. Went from invisible header (with text that could be found using the search tool), to fully rendered.
For anyone else reaching here... it was a CSS issue. The header was there but "invisible" and no matter what margin I set on the render options it was a CSS issue. After starting the CSS from scratch, the header appeared! I could not debug it with the flag show_as_html: true because header and footer are not rendered in that mode, only the body.
If anyone reads this and happens to be in the same situation, use the search tool in the PDF document to find a word you know that's in the header. If it finds something but it's invisible, then you know you have a CSS problem.
Also don't forget to check if you included in the html of the header the <!DOCTYPE html>. Thanks #joaolell for this.
Another thing to check, is that you have the version with patched qt of the wkhtmltopdf library (0.12.4 and above) that supports header and footer. Previous versions won't
Upgrade wkhtmltopdf binary to at least version "0.12.4 (with patched qt)". I just spent half a day troubleshooting because my version of 0.12.1 did not support header and footer.
Ref: Wicked pdf not rendering header/footer
According to the GitHub Page for the axlsx gem I should use this syntax to render a xlsx view to a file and attach it:
xlsx = render_to_string handlers: [:axlsx], formats: [:xlsx], template: "users/export", locals: {users: users}
attachments["Users.xlsx"] = {mime_type: Mime::XLSX, content: xlsx}
Here is my mail method:
xlsx = render_to_string(handlers: [:axlsx], formats: [:xlsx], template: 'v1/reports/reportxyz', params: {start_date: '2016-09-12', period: 'weekly'})
attachments["report.xlsx"] = {content: xlsx, mime_type: Mime::XLSX}
mail(to: "my#email.address", subject: "Report", format: "text")
However I get this error when I try and call the mailer method:
ActionView::MissingTemplate: Missing template layouts/mailer with {:locale=>[:en], :formats=>[:xlsx], :variants=>[], :handlers=>[:axlsx]}. Searched in:
* "path/to/project/app/views"
Why is the render_to_string method affecting what the mailer view the mailer is trying to render? locgially I don't have a mailer.xlsx.axlsx file in my app/views/layouts folder but rather the mailer.text.erb I am trying to use as with other emails.
EDIT
I changed the render line to xlsx = render_to_string(template: 'v1/reports/azamara_social', params: {start_date: '2016-09-12', period: 'weekly'})
And now it seems to try and render the xlsx view but of course gets nil:NilClass errors when the xlsx view tries to reference instance variables defined in the reports controller.
Have you tried passing layout: false? What versions of axlsx, axlsx_rails, rails, and rubyzip are you using?
In the end it all came down to moving the controller code into a lib file. This way I call it in the controller to get the data if it needs to be rendered via web-requests as well as via the Mailer method where I recreate the #variables the view template is looking for.
Here is the finished salient parts of the report mailer method:
data = ReportUtils.get_data(args)
xlsx = render_to_string(template: 'path/to/report.xlsx', locals: {:#period => period, :#date_ranges => data[:date_ranges], :#data => data[:data]})
attachments["report.xlsx"] = {content: xlsx, mime_type: Mime::XLSX}
I'm using the following code snippet to manually compile a sass manifest with some variable overrides appended.
template = File.read("#{Rails.root}/app/assets/schemes/#{scheme}/css/styles.css.scss")
scheme_variables.each do |key, value|
template << "$#{key}:#{value};\n"
end
engine = Sass::Engine.new(template, {
:syntax => :scss,
:cache => false,
:read_cache => false,
:style => :compressed,
:filesystem_importer => Sass::Rails::SassImporter,
:load_paths => MyApp::Application.assets.paths,
:sprockets => {
:context => ?,
:environment => MyApp::Application.assets
}
})
output = engine.render
The Sass::Engine constructor wants a sprockets context and environment in the options hash. What do I put in for the context? The first thing I tried was...
:context => MyApp::Application.assets.context_class,
...but that gives me the following error "undefined method `font_path' for #" when it hits one of my sass asset helpers.
The second thing I tried was...
:context => ActionController::Base.helpers,
...That fixed the asset helper issue, but throws the following error "undefined method `depend_on' for #" when it tries to work through my glob imports (e.g. #import "mixins/*").
I'm using Rails 4.2 and sass-rails 5.0.3.
Any advice on this would be much appreciated. Thanks!
With using Sass::Rails::ScssTemplate you can render your sass code with this snippet:
template = '...' # Your sass code
logical_path = pathname = ''
environment = Rails.application.assets
context = environment.context_class.new(environment, logical_path, pathname)
template = Sass::Rails::ScssTemplate.new(pathname) { template }
output = template.render(context, {})
If you want to render from a file then just add its path to pathname and its asset path to logical_path.
For me it works with Rails 4.2.5.1 and sass-rails 5.0.4.
I ended up solving this in a slightly different way - using Sass::Rails::ScssTemplate's render method. Basically, I write my altered css string out to a file and pass it into the Sass::Rails::ScssTemplate constructor. I then compile and remove the temp file when it's done. This doesn't feel great, but it's working well for me.
scheme_css_dir = "#{Rails.root}/app/assets/schemes/#{scheme}/css"
css = File.read("#{scheme_css_dir}/styles.css.scss")
variables_str = ''
scheme_variables.each do |key, value|
variables_str << "$#{key}:#{value};\n"
end
css.gsub!('#import "./variables";', variables_str)
file = Tempfile.new(['styles', '.css.scss'], scheme_css_dir)
file.write(css)
file.close
abs_path = file.path
relative_path = abs_path[Rails.root.to_s.size + 1..-1]
template = Sass::Rails::ScssTemplate.new(abs_path)
environment = Evrconnect::Application.assets
context = environment.context_class.new(
:environment => environment,
:name => relative_path,
:filename => abs_path,
:metadata => {}
)
output = template.render(context)
file.unlink
To answer your original question, you need to supply either the current view_context, or create one with ActionView::Base.new.
http://apidock.com/rails/AbstractController/Rendering/view_context
In Rails 5.x, Sprockets v3.7.x
NOTE: Following method requires: Rails.application.config.assets.compile == true, i.e., set config.assets.compile = true. Check Sprockets README
I manually returned compiled source for sass/js, using the following code.
# For Scss Assets
def pdf_stylesheet_link_tag(name)
if Rails.env.development?
asset = Rails.application.assets.find_asset(name + '.scss')
raw ('<style type="text/css">' + asset.source + '</style>')
else
raw ('<style type="text/css">' + pdf_asset_contents(name, '.css') + '</style>')
end
end
# For Javascript Assets
def pdf_javascript_include_tag(name, *type)
if Rails.env.development?
if debug? # Can be used to check `debug == true` in params for current route
javascript_include_tag(name)
else
asset = Rails.application.assets.find_asset(name + '.js')
raw ('<script>' + asset.source + '</script>')
end
else
javascript_tag pdf_asset_contents(name, '.js')
end
end
Above code actually helps to dynamically pass compiled version of sass/js
to Wicked_PDF, which actually helps to load styles and js on Generated PDFs, for Dev Environments.
pdf_stylesheet_link_tag can be used as a helper for templates, in development/stage (where, config.assets.precompile == false), instead of using wicked_pdf_stylesheet_link_tag(which actually requires a path to precompiled source-file).
After overnight trial and errors, I found and want to share todays's way of doing it ("today" meaning: rails 5.2.1 and sprockets 3.7.2).
Work as expected: no need of a temp file, accept #import, allow asset path helpers.
# Compile SASS and return the resulting string
# Pass the file path and name without 'sass' extension, relative to assets/stylesheets
def compile_sass(stylesheet)
# Load file content
path = Rails.root.join 'app', 'assets', 'stylesheets', "#{stylesheet}.sass"
sass = File.read path
# Configure engine
environment = Sprockets::Railtie.build_environment Rails.application
scope = environment.context_class.new environment: environment, \
filename: "/", metadata: {}
scope.sass_config.merge! cache: false, style: :compressed
# Compile
engine = Sass::Rails::SassTemplate.new {sass}
engine.render scope
end
Other sass_config options can be found here : https://github.com/sass/ruby-sass/blob/stable/doc-src/SASS_REFERENCE.md#options
The following works with rails 5.2.0, sprockets 3.7.2, sassc-rails 1.3.0 and sassc 1.12.1:
template = '...' # Your sass code
environment = Sprockets::Railtie.build_environment(Rails.application)
engine = SassC::Rails::SassTemplate.new
engine.call(environment: environment,
filename: '/',
data: template,
metadata: {})[:data]
In my layout footer I am including page specific JavaScript files like so:
<%= javascript_include_tag "#{params[:controller]}_#{params[:action]}" if javascript_exists?("#{params[:controller]}_#{params[:action]}") %>
The helper I am using is from another question on this site but modified to work with Rails 2:
application_helper.rb
def javascript_exists?(script)
script = "#{RAILS_ROOT}/public/javascripts/#{script}.js"
extensions = %w(.coffee .erb .coffee.erb) + [""]
extensions.inject(false) do |truth, extension|
truth || File.exists?("#{script}#{extension}")
end
end
Then I also init the included file:
<script>
jQuery(document).ready(function() {
<%= "#{params[:controller].titleize}#{params[:action].titleize}.init();" if javascript_exists?("#{params[:controller]}_#{params[:action]}") %>
});
</script>
This works perfectly fine if my file is named controller_action.js but once renamed to controller_action.js.erb Rails generates an error because it can't find it.
GET http://localhost:3000/javascripts/controller_action.js 404 (Not Found)
Uncaught ReferenceError: ControllerAction is not defined
Update
If I use the .rjs extension then there are no errors generated but this seems to be because Rails doesn't think the file exists. (I updated the javascript_exists? with the .rjs extension.)
After some more debugging, the file is being located:
/path/public/javascripts/circuit_list.js true
but this returns false if .rjs is used.
The routing error when navigating to http://localhost:3000/javascripts/controller_action.js in the browser is:
No route matches "/javascripts/controller_action.js" with {:method=>:get}