Loading a page into memory in Rails - ruby-on-rails

My rails app produces XML when I load /reports/generate_report.
On a separate page, I want to read this XML into a variable and save it to the database.
How can I do this? Can I somehow stream the response from the /reports/generate_report.xml URI into a variable? Or is there a better way to do it since the XML is produced by the same web app?
Here is my generate_report action:
class ReportsController < ApplicationController
def generate_report
respond_to do |format|
#products = Product.all
format.xml { render :layout => false }
end
end
end
Here is the action I am trying to write:
class AnotherController < ApplicationController
def archive_current
#output = # get XML output produced by /reports/generate_report
# save #output to the database
respond_to do |format|
format.html # inform the user of success or failure
end
end
end
Solved: My solution (thanks to Mladen Jablanović):
#output = render_to_string(:file => 'reports/generate_report.xml.builder')
I used the following code in a model class to accomplish the same task since render_to_string is (idiotically) a protected method of ActionController::Base:
av = ActionView::Base.new(Rails::Configuration.new.view_path)
#output = av.render(:file => "reports/generate_report.xml.builder")

Perhaps you could extract your XML rendering logic to a separate method within the same controller (probably a private one), which would render the XML to a string using render_to_string, and call it both from generate_report and archive_current actions.

What I typically do in this type of situation is to create a separate module/class/model to generate the report (it could even potentially be right in the Product model). This separate component could be in app/models or it could be in lib. In any case, once you have it extracted you can use it anywhere you need it. The controller can call it directly. You can generate it from the console. You can have a cron job generate it. This is not only more flexible, but it also can help smooth out your request response times if the report becomes slow to generate.
Since you are using a template it's understandable that the controller route is convenient, but even if you have to include some kind of ruby templating system in your auxiliary lib, it's still probably going to be less hassle and more flexible then trying to go through the controller.

#output = Product.all.to_xml

I'm sorry, is you question about Xml or about sessions? I mean is the fact that your action generates Xml material to the question? Or do you just want to save the output of the action for latter use?
You said on a "separate" page - you mean on another request? (like after user approved it?)
Why do you want to save the output? Because it should be saved exactly as rendered? (for example user can get frustrated if he clicked to save one report and you saved another)
Or is this thing expensive to generate?
Or may be, I got it wrong and it's about refactoring?

Related

Persisting ActiveRecord objects between requests

I have an ActiveRecord model named Document and have implemented CRUD operations around it. I just have a problem with persisting a Document instance between requests when validation fails (be cause I wanna redirect to another page when this happens).
First, I tried storing the instance in the flash session:
# documents_controller.rb
def new
#document = flash[:document] || Document.new
end
def create
document = Document.new(document_params)
if document.save
return redirect_to documents_path
end
flash[:document] = document
redirect_to new_document_path
end
With the code above, I was expecting that the actual Document instance was stored in the flash session, but instead it became a string which looks somewhat like #<Document:0xad32368>. After searching online for a while, I found out that for some reasons you cannot store ActiveRecord objects in sessions.
There are a lot of suggestions about just storing the object's id in the flash session, but I can't do that because as you can see, the object is not yet stored in the database.
Next, I tried reconstructing the Document instance after the redirect, taking advantage of the instance's attributes method (which returns a serializeable hash that can be stored in the session):
# documents_controller.rb
def new
#document = Document.new(flash[:document_hash] || {})
end
def create
...
flash[:document_attributes] = document.attributes
redirect_to new_document_path
end
This almost solved the problem, except for the part in which the validation errors (document.errors) are not preserved. Also, if this is used to persist an instance already stored in the database (in the case of failed validations when updating a Document instance), I'm not sure which between the original attributes and the new attributes will get persisted.
Right now I've already run out ideas to try. Anyone who has a decent solution for this?
EDIT:
You might be wondering why I still have to redirect to another page instead of just rendering the new document view template or the new action in the create method. I did so because there are some things in my views that are dependent on the current controller method. For example, I have a tab which needs to be highlighted when you are on the document creation page (done by checking if action_name == "new" and controller_name == "documents"). If I do:
def create
...
render action: "new"
end
the tab will not get highlighted because action_name will now be create. I also can't just add additional condition to highlight the tab if action_name == "create" because documents can also be created from the the index page (documents_path). Documents can also be updated from the index page (documents_path) or from the detail page (document_path(document)), and if validation fails in the update method, I'd like to redirect to the previous page.
If I really need to fake persisting something between requests (all of the variables that you set are lost between requests), I will ususally put the relevant attributes into hidden fields in the new form.
In your case, this is overkill. In your code, you are redirecting, which causes a new request:
def create
document = Document.new(document_params)
if document.save
return redirect_to documents_path
end
flash[:document] = document
redirect_to new_document_path
end
You can easily render the output of another action, instead of redirecting, by using render action: 'action_to_render'. So in your example, this would probably be:
def create
#document = Document.new(document_params)
if #document.save
render action: 'index'
else
render action: 'new'
end
end
Which can be simplified to:
def create
#document = Document.new(document_params)
action_to_render = #document.save ? 'index' : 'new'
render action_to_render
end
If you need extra logic from the action, you can refactor the logic to a method called from both actions, or simply call the other action from the current one.
It is fine once in a while, but I would caution that having to jerk around with the rendering too much is usually indicative of poor architecture.
Edit:
An additional option, given the newly highlighted constraints, could be to make the new and create methods the same. Remove the new action and routes, and make create answer for GET and PATCH requests. The action might look something like:
def create
#document = Document.new(document_params)
request.patch? && #document.save && redirect_to( documents_path )
end
I actually use something very similar to this for almost all of my controllers, as it tends to DRY things significantly (as you can remove the extra probably identical view, as well)
Another option would be to just use an instance variable to keep track of the active tab in this instance, and make the rest of the code a lot cleaner.
SOLVED
I was able to make a workaround for it using ActiveSupport::Cache::Store (as suggested by #AntiFun). First I created a fake_flash method which acts closely like the flash sessions except that it uses the cache to store the data, and it looks like this:
def fake_flash(key, value)
if value
Rails.cache.write key, value
else
object = Rails.cache.read key
Rails.cache.delete key
object
end
end
And then I just used it like the flash session.
# documents_controller.rb
def new
...
#document = fake_flash[:document] || Document.new
...
end
def create
document = Document.new document_params
...
# if validation fails
fake_flash :document, document
redirect_to new_document_page
end

How does format.html know what to render?

I have the following piece of code in the file app/mailers/user_mailer.rb .
Now how does format.html know what to render .
It is not in the action of any controller.
I understand the basic fact that it renders show view by default but here is no controller no action.
How does it still work ?
class UserMailer < ActionMailer::Base
def SIGNUP_NOTIFICATION(user)
#recipients = "#{user.email}"
#from = "#{sender.email}"
mailer_name = "SIGNUP_NOTIFICATION"
mail(:to => #recipients, :from => #from) do |format|
format.html
end
end
You're right that ActionMailer is not a controller (that's ActionController). The respond_to method of ActionController and the mail method of ActionMailer are entirely different things, however they both have the concept of "what kind of response should we be sending" and so the rails devs chose to offer a similar interface for format picking in both.
In ActionController the respond_to method takes a block and uses the HTTP request environment to pick which format to render.
In ActionMailer, mail takes a block and uses different logic to decide which format to render. When you give it format.html it simply looks in your views folder for templates with <method_name>.<format>.<acceptable_template_type>, e.g., SIGNUP_NOTIFICATION.html.erb. As an aside, you should name all your methods in snake_case (signup_notification). Uppercase method names are non-idiomatic and might cause problems.
Technical explanation
While you don't need to know this to use ActionMailer, I think it's interesting to walk through the flow of how template generation works here. It includes some interesting meta-programming but the logic is simpler than in regular controllers (no HTTP environment).
You can see in the source for mail that it calls a function to render the messages. If you gave mail a block, mail creates an instance of ActionMailer::Collector (which includes AbstractController::Collector) and yields to this collector. So, in your example the collector gets the function html called on it (since your do |format| ... end has an argument, format will be the collector instance when yield(collector) is called here.
So an instance of ActionMailer::Collector is passed into your do |format| ... block, where it has the function html called on it, The collector uses method missing tricks
from AbstractController::Collector so that it can respond to any mime type, passing the requested type over to ActionMailler::Collector#custom which actually builds the email by looking in the views folder for templates matching that type.
Mailer is like a controller but without inheriting ActionController, for your mailer you would put your view in app/views/user_mailer/SIGNUP_NOTIFICATION.html.erb
And you can send this email calling the method in your mailer:
UserMailer.SIGNUP_NOTIFICATION(some_user_instance).deliver
I am not sure if this is what you are asking for.

Controller Action to Delayed Job

I am uploading a tab-delimited document and processing in the controller. Everything works fine, but can take some time on a large file. I want to move this to a delay_job, which I have working elsewhere in my app, but as this is in the controller, cannot be called in the same way.
The form calls on the process_file action, which in turn calls on the salesupload action. How should I turn this into a background job?
class SalesController < ApplicationController
def salesupload(file)
uploaded_io = file.read
numrows = "uploaded_io.size"
FasterCSV.parse(uploaded_io, {:headers => true, :col_sep =>"\t"}).each do |row_data|
full, upc, _discard, isrc = row_data[26].match(/^([^_]+)(_(.+))?/).to_a
new_record = AppleSale.new(
'provider' => row_data[0],
'provider_country' => row_data[1],
'vendor_identifier' => row_data[2]
)
new_record.save
end
end
def process_file
file = params[:apple_sale][:tsv_file]
salesupload(file)
end
end
I found when I had to do this that the method defined in the controller has to be a class method. I can't remember why this was, I think it had to do with having a more explicit receiver. So what I would do is make the salesupload method a class method, and then just call .delay on it.
def self.salesupload(files)
# code
end
def process_file
file = params[:apple_sale][:tsv_file]
SalesController.delay.salesupload(file)
head :no_content
end
And you should be good to go! I also made my original method (process_file in this case) called via AJAX, and then I appended the head :no_content so that it returned something without needing a redirect or anything.
I wrote a gem called delayed_action to do this.
In this case you'd write
delayed_action [:sales_upload]
and that's it.
I guess you should move this code to a model or a separated class (for instance inside the lib folder). But that is just a matter of organization and best practices.
About the running this code in background, you have many options.
If you want to use delayed_job I guess you should watch this screencast:
http://railscasts.com/episodes/171-delayed-job
but basically, after setting up delayed job, you just use something like send_later(:process_file) to tell to run that project in background.
It is pretty straightforward, your command and data structure will be saved in a database table and later a separated process can execute it. I imagine that after watching the railscast you will have your answers ;)

Fetch Controller Action Response From Tasks and Models

I would love to call a controller action from within a model. Yes, MVC. Thanks.
So, why would i like to call a controller action and fetch the response?
Because my controller knows how to render the file I would like to cache. Why should I duplicate code to collect all data needed by my view?
Setup:
InvoicesController responds_to :html, :pdf
Invoice uses state_machine (:new -> :open -> paid)
What I "need" within the state transition from :new to :open
generate /invoices/:id.pdf
cache the PDF as Invoice#file for later use in delayed_job or simliar
What interface i would love to use elsewhere?
#invoice.build_pdf
Any suggestions?
Update:
I would like to cache the PDF as model attachement for later use (delayed_job mailing, etc)
It may be better to take the code you're using in your controller to render the PDF data, and create a module or class in your lib directory let's say lib/pdf.rb then use that code in both your controller and your model.
Update: You can cache the PDF itself on the filesystem as a file with a timestamp as it's name or something like that that is uniquely identifiable. Then add a file or invoice attribute to the model to store the location in the database for later use.
Update: You can use the block form of respond_to like so:
respond_to do |format|
format.html
format.pdf { InvoiceBuilder.create(#invoice) }
end
Update: It seems that ActionDispatch::Integration::Session is a class for creating and interface for integration testing. But I think you could exploit it for your purposes.
You should be able to use it like this:
# app/models/invoice.rb
class Invoice < ActiveRecord::Base
def get_body
app = ActionDispatch::Integration::Session.new(Rails.application)
app.get("invoices/#{self.id}.pdf")
app.response.body # this returns the rendered content that the browser will see.
end
end
Another way would be to grab the content by using curl i.e. curl http://localhost:3000/invoices/1.pdf and store the output.

How do I expose data in a JSON format through a web service using Rails?

Is there an easy way to return data to web service clients in JSON using Rails?
Rails resource gives a RESTful interface for your model. Let's see.
Model
class Contact < ActiveRecord::Base
...
end
Routes
map.resources :contacts
Controller
class ContactsController < ApplicationController
...
def show
#contact = Contact.find(params[:id]
respond_to do |format|
format.html
format.xml {render :xml => #contact}
format.js {render :json => #contact.json}
end
end
...
end
So this gives you an API interfaces without the need to define special methods to get the type of respond required
Eg.
/contacts/1 # Responds with regular html page
/contacts/1.xml # Responds with xml output of Contact.find(1) and its attributes
/contacts/1.js # Responds with json output of Contact.find(1) and its attributes
http://wiki.rubyonrails.org/rails/pages/HowtoGenerateJSON
Rails monkeypatches most things you'd care about to have a #to_json method.
Off the top of my head, you can do it for hashes, arrays, and ActiveRecord objects, which should cover about 95% of the use cases you might want. If you have your own custom objects, it's trivial to write your own to_json method for them, which can just jam data into a hash and then return the jsonized hash.
There is a plugin that does just this,
http://blog.labnotes.org/2007/12/11/json_request-handling-json-request-in-rails-20/
And from what I understand this functionality is already in Rails. But go see that blog post, there are code examples and explanations.
ActiveRecord also provides methods to interact with JSON. To create JSON out of an AR object, just call object.to_json. TO create an AR object out of JSON you should be able to create a new AR object and then call object.from_json.. as far as I understood, but this did not work for me.

Resources