CarrierWaveDirect Without Processing - ruby-on-rails

I have a document model that consists of PDF files. Because I don't need to manipulate the PDFs in any way, I'm attempting to use CarrierWaveDirect without its processing step, which I believe is downloading and re-uploading the files.
My uploader looks like this:
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::RMagick
include CarrierWave::MimeTypes
include CarrierWaveDirect::Uploader
def will_include_content_type
true
end
default_content_type 'application/pdf'
allowed_content_types = %w(application/pdf)
def store_dir
prefix = Rails.env.production? ? '' : 'tmp/'
"#{prefix}files/documents"
end
def extension_white_list
%w(pdf)
end
end
I define the document (in the controller) as:
#document = Document.new.filename
#document.success_action_redirect = new_document_url(:step => 2)
I'm using the direct upload form to upload the file itself, which is working fine.
<%= direct_upload_form_for #document do |f| %>
<%= f.file_field :filename, :required => true %>
<%= f.submit "Upload Document" %>
<% end %>
When I get the key back, I create an attribute called filename_key, and a callback in my model looks for this attribute to update the column.
Controller:
key = params[:key].split('/').last(2).join('/')
#document = Document.new(:filename_key => key)
Model:
after_save :check_for_file
def check_for_file
unless self.filename_key.blank?
update_columns(:filename => self.filename_key.to_s)
end
end
This actually all works fine. The problem is when I go to save the record again, I get this error:
ActiveRecord::StatementInvalid: Mysql2::Error: Data too long for column 'filename' at row 1: UPDATE `documents` SET `filename` = '--- &1 !ruby/object:DocumentUploader\nmodel: !ruby/object:Document\n attributes:\n id: 92\n ...
It's trying to set the entire contents of the attribute as the attribute itself. My first guess is that I am bypassing vital after_create or after_save callbacks, but I can't get to saving a file without processing unless I avoid these callbacks.
Any suggestions of where to look next are appreciated in advance!

I found I had a asked a similar question almost a year ago. It turns out the solution was the same for both.
CarrierWaveDirect uses filename as a method to (you guessed it!) work with the filename of the uploaded file. For me, changing the name of the column to document solved my issue.
I should also note that I've found ignoring the processing step to work fine. However, you do lose anything that would be called during that step, for example, setting the content type. As I've shown in the question, I'm setting the content type automatically, which enables me to still view the PDF files in the browser, since they are set as application/pdf vs. binary/octet-stream.

Related

Get path to ActiveStorage file on disk

I need to get the path to the file on disk which is using ActiveStorage. The file is stored locally.
When I was using paperclip, I used the path method on the attachment which returned the full path.
Example:
user.avatar.path
While looking at the Active Storage Docs, it looked like rails_blob_path would do the trick. After looking at what it returned though, it does not provide the path to the document. Thus, it returns this error:
No such file or directory # rb_sysopen -
Background
I need the path to the document because I am using the combine_pdf gem in order to combine multiple pdfs into a single pdf.
For the paperclip implementation, I iterated through the full_paths of the selected pdf attachments and load them into the combined pdf:
attachment_paths.each {|att_path| report << CombinePDF.load(att_path)}
Use:
ActiveStorage::Blob.service.path_for(user.avatar.key)
You can do something like this on your model:
class User < ApplicationRecord
has_one_attached :avatar
def avatar_on_disk
ActiveStorage::Blob.service.path_for(avatar.key)
end
end
I'm not sure why all the other answers use send(:url_for, key). I'm using Rails 5.2.2 and path_for is a public method, therefore, it's way better to avoid send, or simply call path_for:
class User < ApplicationRecord
has_one_attached :avatar
def avatar_path
ActiveStorage::Blob.service.path_for(avatar.key)
end
end
Worth noting that in the view you can do things like this:
<p>
<%= image_tag url_for(#user.avatar) %>
<br>
<%= link_to 'View', polymorphic_url(#user.avatar) %>
<br>
Stored at <%= #user.image_path %>
<br>
<%= link_to 'Download', rails_blob_path(#user.avatar, disposition: :attachment) %>
<br>
<%= f.file_field :avatar %>
</p>
Thanks to the help of #muistooshort in the comments, after looking at the Active Storage Code, this works:
active_storage_disk_service = ActiveStorage::Service::DiskService.new(root: Rails.root.to_s + '/storage/')
active_storage_disk_service.send(:path_for, user.avatar.blob.key)
# => returns full path to the document stored locally on disk
This solution feels a bit hacky to me. I'd love to hear of other solutions. This does work for me though.
You can download the attachment to a local dir and then process it.
Supposing you have in your model:
has_one_attached :pdf_attachment
You can define:
def process_attachment
# Download the attached file in temp dir
pdf_attachment_path = "#{Dir.tmpdir}/#{pdf_attachment.filename}"
File.open(pdf_attachment_path, 'wb') do |file|
file.write(pdf_attachment.download)
end
# process the downloaded file
# ...
end

How can I prefill a text area with a long blob in Rails?

In one of my views I have this code:
<%= f.label :default_theme %>
<%= f.text_area(:default_theme, :value => "How do we fill this with a long blob?") %>
I want to pre populate the text area, but with a very long blob of over 160 lines of xml. What is the best way to do this? I understand I could just fill it in as a value, but that seems to be a really horrible way of doing it.
I think you should use a XML-reader library to load the long message into a shared variable #default_theme_content for example:
def index
#default_theme_content = MyXMLReader.read('path/to/xml/file.xml')
# etc.
And then use it in your view:
f.text_area(:default_theme, :value => #default_theme_content)
You also asked if this should be in the model rather than Controller. It depends:
1): If your file is related to a Model, like a Theme model, and should be loaded as the default Theme for a User, then yeah you could have a method in your model to return this file:
class Theme < ActiveRecord::Base
# etc.
def self.default_theme_content
MyXMLReader.read('path/to/xml/file.xml')
end
And use it like this in the controller:
def index
#default_theme_content = Theme.default_theme_content
# etc.
2): If this file is not related to any model, you can delegate this to the controller.

Rails upload CSV file with header

I am using Ruby 1.9.2 with Rails 3.2.1.
I would like to create a view to upload a CSV or tab delimited file, and displays the contents of the file on the same page using a table or pagination display, then process that data in JavaScript.
How can I do this? Please walk me through any code samples you have, I am a total noob in Ruby also.
First, write a view to upload your file. You can use Paperclip for this.
Assuming you have a resource Csv, your upload form could look like this:
<%= form_for #csv, :url => csv_path, :html => { :multipart => true } do |form| %>
<%= form.file_field :attachment %>
<% end %>
Your model:
class Csv < ActiveRecord::Base
attr_accessible :attachment
has_attached_file :attachment
end
Your controller actions:
def create
#csv = Csv.create( params[:csv] )
# your save and redirect code here
end
def show
#csv = Csv.find(params[:id])
end
Having that, you can use something like this in your view:
CSV.foreach(#csv.attachment.path) do |row|
# use the row here to generate html table rows
end
Note: this is just a general idea of how this can be done and you need to have the the resource added to your routes, Paperclip gem installed and configured, etc - read the doc on how to do all that.
Just use a nice Ruby gem for parsing CSV files. This should point you in the right direction. http://fastercsv.rubyforge.org/

Populate a form with the new()-method

Problem solved. HTML5 localStorage messed with me.
I'm trying to populate a form with parameters from the new()-method, and I can't get it to work.
Every user has default values for the form saved in the database(in a table called defaults), and when you create a new record I want it to be populated with the values from that table.
#default = Default.find_by_user_id(current_user.id)
#invoice = Invoice.new(:title => #default.title, :company_information => #default.company_information)
render 'create'
and then in my view:
form_for #invoice, :url => { :action => "create"} do |f| ...
What happens is that the values that are default for invoice are created, but not the ones created in the new()-method.
The weirdest part is that when I check the source code after the page is loaded, the inputs value attributes is filled with the correct information, but not rendered on the page...
What you're doing here:
Invoice.new(:title => #default.title, :company_information => #default.company_information)
Makes sense and should work…unless those fields are protected from mass assignment.
class Invoice << ActiveRecord::Base
attr_accessible :some, :other, :fields
...
end
This would allow you to set :some, :other, (and) :fields when you initialize your Invoice object, but it will prevent you from setting any other "attributes".
Strange, I don't see anything wrong with what you are trying to do... maybe something on the browser side (javascript, css, etc) is fowling things up?
Check to see if there is something selectable inside the form inputs or try creating a vanilla form without any javascript or css. Or, you might even try simply printing the contents of the attribute in the html (without using input/textarea tags) using something like:
<%= #invoice.title %>
This will at least help confirm that the default values where indeed set. Additionally, using:
<%= f.object.title %> # place me inside the form_for block
will help you confirm that the form builder instance also has the correct value.
Good luck.

Creating customized label fields in forms

I'd like to be able to generate the following markup:
<label for="field">Something <span class="hint">Field hint</span></label>
from the following code:
form_for ... do |f|
f.label :field, :hint => "Field hint"
end
So far I've created an initializer to store the custom functionality which re-opens ActionView::Helpers::FormBuilder and changes the label method, however I'm not sure what the best way to actually get the span into the text for the label. If I try to put the text in directly then rails, rightly so, escapes the content.
I'd quite like to use the existing label infrastructure as it has all the validation error support. This rules out using content_tag and generating it all myself (which would work, but doesn't seem... right).
Instead of changing the default builder, you should create a custom builder and pass it to the form with the :builder parameter.
class HintFormBuilder < ActionView::Helpers::FormBuilder
end
form_for #resource, :builder => HintFormBuilder do |f|
# ...
end
The Hint builder inherits all FormBuilder features, including validation, error messages and so on. Now, you should change what you need to change in order to customize the behavior.
This is a really raw draft.
class HintFormBuilder < ActionView::Helpers::FormBuilder
(%w(label)).each do |selector|
src = <<-end_src
def #{selector}(method, options = {})
hint = options.delete(:hint)
returning(super) do |element|
# replace here the value of element with hint
# if hint != nil
# remember to use gsub! and not gsub
end
end
end_src
class_eval src, __FILE__, __LINE__
end
end
EDIT based on the first comment:
It's always a good idea to not hack the Rails internals because you might need to use, now or in the future, plugins or features that rely on the original behavior. If you don't want to manually append the builder in your forms, you can create an helper.
def search_form_for(record_or_name_or_array, *args, &proc)
options = { :builder => HintFormBuilder }
form_for(record_or_name_or_array,
*(args << options),
&proc)
end
If you want to reopen the original class instead, I would suggest to create a new method. This solution also applies to the custom helper and has the benefit you can customize it without the need to gsub! the response. Yes, gsub! is the common way to do so because when extending the original methods you only have access to the method/options and the result, no the value (that is injected by the #object variable).
class ActionView::Helpers::FormBuilder
def label_with_hint(method, text = nil, options = {})
hint = options.delete(:hint)
# do your own customizations...
#template.label(#object_name, method, text, objectify_options(options))
end
end
EDIT: I was mistaken, you can pass a custom text as a parameter so you don't need to gsub! the returned string. I got confused by the text_field tag.
At this point, you can use either the first (subclassing with/without custom method), second (hacking internals) or third option (hacking internals with custom method) and intercept the text value before it is sent to #template.label.
Also note that text can be nil. If nil, the value is automatically generated from method. You should be aware of this.
Here's what I would have done.
# config/initializers/[anything].rb
ActionView::Base.default_form_builder = CustomFormBuilder
# lib/custom_form_builder.rb
class CustomFormBuilder < ActionView::Helpers::FormBuilder
def label(field, text, options = {})
if options[:hint]
hint = #template.content_tag(:span, options[:hint], :class => "hint")
super(field, "#{field.to_s.humanize} #{hint}", options)
else
super
end
end
end

Resources