I'm looking for a best practice/standard pattern for generating feeds in Rails 3. Is http://railscasts.com/episodes/87-generating-rss-feeds still valid?
First of all, nowadays I recommend using an ATOM feed instead of RSS.
The specification of ATOM feed offers more value than the RSS one with internationalization, content types and other things and every modern feed reader supports it.
More info about ATOM vs RSS can be found at:
the Wikipedia ATOM entry
PRO Blogger and Free Marketing Zone blog posts about the subject
On to the coding:
This example assumes:
a model called NewsItem with the following attributes:
title
content
author_name
a controller for that model (news_items_controller.rb), to which you'll add the feed action
We'll use a builder template for this and the Ruby on Rails atom_feed helper which is of great use.
1. Add the action to the controller
Go to app/controllers/news_items_controller.rb and add:
def feed
# this will be the name of the feed displayed on the feed reader
#title = "FEED title"
# the news items
#news_items = NewsItem.order("updated_at desc")
# this will be our Feed's update timestamp
#updated = #news_items.first.updated_at unless #news_items.empty?
respond_to do |format|
format.atom { render :layout => false }
# we want the RSS feed to redirect permanently to the ATOM feed
format.rss { redirect_to feed_path(:format => :atom), :status => :moved_permanently }
end
end
2. Setup your builder template
Now let's add the template to build the feed.
Go to app/views/news_items/feed.atom.builder and add:
atom_feed :language => 'en-US' do |feed|
feed.title #title
feed.updated #updated
#news_items.each do |item|
next if item.updated_at.blank?
feed.entry( item ) do |entry|
entry.url news_item_url(item)
entry.title item.title
entry.content item.content, :type => 'html'
# the strftime is needed to work with Google Reader.
entry.updated(item.updated_at.strftime("%Y-%m-%dT%H:%M:%SZ"))
entry.author do |author|
author.name entry.author_name
end
end
end
end
3. Wire it up with a route
Let's make the feeds available at http://domain.example/feed
This will call the action with the ATOM format by default and redirect /feed.rss to /feed.atom.
Go to config/routes.rb and add:
resources :news_items
match '/feed' => 'news_items#feed',
:as => :feed,
:defaults => { :format => 'atom' }
4. Add the link to ATOM and RSS feeds on the layout
Finally, all that is left is to add the feed to the layout.
Go to app/views/layouts/application.html.erb and add this your <head></head> section:
<%= auto_discovery_link_tag :atom, "/feed" %>
<%= auto_discovery_link_tag :rss, "/feed.rss" %>
I did something similar but without creating a new action.
index.atom.builder
atom_feed :language => 'en-US' do |feed|
feed.title "Articles"
feed.updated Time.now
#articles.each do |item|
next if item.published_at.blank?
feed.entry( item ) do |entry|
entry.url article_url(item)
entry.title item.title
entry.content item.content, :type => 'html'
# the strftime is needed to work with Google Reader.
entry.updated(item.published_at.strftime("%Y-%m-%dT%H:%M:%SZ"))
entry.author item.user.handle
end
end
end
You don't need to do anything special in the controller unless you have some special code like i did. For example I'm using the will_paginate gem and for the atom feed I don't want it to paginate so I did this to avoid that.
controller
def index
if current_user && current_user.admin?
#articles = Article.paginate :page => params[:page], :order => 'created_at DESC'
else
respond_to do |format|
format.html { #articles = Article.published.paginate :page => params[:page], :order => 'published_at DESC' }
format.atom { #articles = Article.published }
end
end
end
Related
I'm getting an error: No route matches {:action=>"sort", :controller=>"links"}
I'm adapting from a non-nested example and am having trouble figuring out the nested routing.
Right now, my route looks like this:
resources :groups do
resources :navbars do
resources :links do
collection { post :sort }
end
end
post :add_user, on: :member
end
I am rendering a collection of links from navbar>show:
= render partial: 'links/link', collection: #navbar.links
and here's the collection, links>_link.html.haml:
%ul#links{"data-update-url" => sort_group_navbar_links_url}
- #links.each do |faq|
= content_tag_for :li, link do
%span.handle [drag]
= link.method_name
= link.code
= link.button
= link.text
= link_to 'Edit Link', edit_group_navbar_link_path(#group, #navbar, link), class: 'btn btn-mini'
= link_to 'Delete', group_navbar_link_path(#group, #navbar, link), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-mini'
My links_controller has the sort action:
def sort
params[:link].each_with_index do |id, index|
Link.update_all({display_order: index+1}, {id: id})
end
render nothing: true
end
Because my link collection is being rendered from the navbar>show page, it's not clear to me where my sort action should be located (in links_controller or in navbars_controller).
And my navbars_controller defines #links:
def show
#links = #navbar.links.order("display_order")
respond_to do |format|
format.html # show.html.erb
format.json { render :json => #navbar }
end
end
Also here's the js for good measure (links.js.coffee):
jQuery ->
$('#links').sortable
axis: 'y'
handle: '.handle'
update: ->
$.post($(this).data('update-url'), $(this).sortable('serialize'))
Maybe this last line also needs work to include the route?
I'm using rails 3.2.
I've implemented the framework outlined in this post: How to use jquery-Tokeninput and Acts-as-taggable-on with some difficulty. This is working insofar as prepopulating with the appropriate theme and ajax search, but when I enter a new tag, it is immediately deleted when the text area loses focus. I'm not sure what I'm doing wrong. Here's some of my relevant code:
User Model (does the tagging):
class User < ActiveRecord::Base
[...]
# tagging
acts_as_tagger
Item Model (accepts a tag):
class Item < ActiveRecord::Base
attr_accessible :title, :tag_list
#tagging functionality
acts_as_taggable_on :tags
Item Controller:
def tags
#tags = ActsAsTaggableOn::Tag.where("tags.name LIKE ?", "%#{params[:q]}%")
respond_to do |format|
format.json { render :json => #tags.collect{|t| {:id => t.name, :name => t.name }}}
end
end
On my form partial:
<%= f.input :tag_list, :label => "Tags", :input_html => { :class => "text_field short", "data-pre" => #item.tags.map(&:attributes).to_json }, :hint => "separate tags by a space" %>
my routes:
get "items/tags" => "items#tags", :as => :tags
resources :items
[almost there!!!]
the js on the form [note: the id of the element is assigned dynamically]:
<script type="text/javascript">
$(function() {
$("#item_tag_list").tokenInput("/art_items/tags", {
prePopulate: $("#item_tag_list").data("pre"),
preventDuplicates: true,
crossDomain: false,
theme: "facebook"
});
});
</script>
If you still want to use Jquery TokenInput and add tags there are different ways to do it.
1.
This is actually from my same question; the newest answer: How to use jquery-Tokeninput and Acts-as-taggable-on
This could go in your controller.
def tags
query = params[:q]
if query[-1,1] == " "
query = query.gsub(" ", "")
Tag.find_or_create_by_name(query)
end
#Do the search in memory for better performance
#tags = ActsAsTaggableOn::Tag.all
#tags = #tags.select { |v| v.name =~ /#{query}/i }
respond_to do |format|
format.json{ render :json => #tags.map(&:attributes) }
end
end
This will create the tag, whenever the space bar is hit.
You could then add this search setting in the jquery script:
noResultsText: 'No result, hit space to create a new tag',
It's a little dirty but it works for me.
2.
Check out this guy's method: https://github.com/vdepizzol/jquery-tokeninput
He made a custom entry ability:
$(function() {
$("#book_author_tokens").tokenInput("/authors.json", {
crossDomain: false,
prePopulate: $("#book_author_tokens").data("pre"),
theme: "facebook",
allowCustomEntry: true
});
});
3.
Not to sure about this one but it may help: Rails : Using jquery tokeninput (railscast #258) to create new entries
4.
This one seems legit as well: https://github.com/loopj/jquery-tokeninput/pull/219
I personally like the first one, seems easiest to get and install.
I am trying to create a custom POST action for my article object.
In my routes.rb, I have set the action in the following way:
resources :articles do
member do
post 'update_assigned_video'
end
end
In my articles_controller.rb I have:
def update_assigned_video
#article = Articles.find(params[:id])
#video = Video.find(:id => params[:chosenVideo])
respond_to do |format|
if !#video.nil?
#article.video = #video
format.html { redirect_to(#article, :notice => t('article.updated')) }
else
format.html { render :action => "assign_video" }
end
end
Then in my view I make a form like this:
<%= form_for #article, :url => update_assigned_video_article_path(#article) do |f|%>
[...]
<%= f.submit t('general.save') %>
The view renders (so I think he knows the route). But clicking on the submit button brings the following error message:
No route matches "/articles/28/update_assigned_video"
rake routes knows it also:
update_assigned_video_article POST /articles/:id/update_assigned_video(.:format) {:action=>"update_assigned_video", :controller=>"articles"}
What am I doing wrong?
Is this the wrong approach to do this?
Your form_for will do a PUT request rather than a POST request, because it's acting on an existing object. I would recommend changing the line in your routes file from this:
post 'update_assigned_video'
To this:
put 'update_assigned_video'
I'm trying to generate emails with rendered PDF attachements using ActionMailer and wicked_pdf.
On my site, I'm using already both wicked_pdf and actionmailer separately. I can use wicked_pdf to serve up a pdf in the web app, and can use ActionMailer to send mail, but I'm having trouble attaching rendered pdf content to an ActionMailer (edited for content):
class UserMailer < ActionMailer::Base
default :from => "webadmin#mydomain.com"
def generate_pdf(invoice)
render :pdf => "test.pdf",
:template => 'invoices/show.pdf.erb',
:layout => 'pdf.html'
end
def email_invoice(invoice)
#invoice = invoice
attachments["invoice.pdf"] = {:mime_type => 'application/pdf',
:encoding => 'Base64',
:content => generate_pdf(#invoice)}
mail :subject => "Your Invoice", :to => invoice.customer.email
end
end
Using Railscasts 206 (Action Mailer in Rails 3) as a guide, I can send email with my desired rich content, only if I don't try to add my rendered attachment.
If I try to add the attachment (as shown above), I get an attachement of what looks to be the right size, only the name of the attachment doesn't come across as expected, nor is it readable as a pdf. In addition to that, the content of my email is missing...
Does anyone have any experience using ActionMailer while rendering the PDF on the fly in Rails 3.0?
Thanks in advance!
--Dan
WickedPDF can render to a file just fine to attach to an email or save to the filesystem.
Your method above won't work for you because generate_pdf is a method on the mailer, that returns a mail object (not the PDF you wanted)
Also, there is a bug in ActionMailer that causes the message to be malformed if you try to call render in the method itself
http://chopmode.wordpress.com/2011/03/25/render_to_string-causes-subsequent-mail-rendering-to-fail/
https://rails.lighthouseapp.com/projects/8994/tickets/6623-render_to_string-in-mailer-causes-subsequent-render-to-fail
There are 2 ways you can make this work,
The first is to use the hack described in the first article above:
def email_invoice(invoice)
#invoice = invoice
attachments["invoice.pdf"] = WickedPdf.new.pdf_from_string(
render_to_string(:pdf => "invoice",:template => 'documents/show.pdf.erb')
)
self.instance_variable_set(:#lookup_context, nil)
mail :subject => "Your Invoice", :to => invoice.customer.email
end
Or, you can set the attachment in a block like so:
def email_invoice(invoice)
#invoice = invoice
mail(:subject => 'Your Invoice', :to => invoice.customer.email) do |format|
format.text
format.pdf do
attachments['invoice.pdf'] = WickedPdf.new.pdf_from_string(
render_to_string(:pdf => "invoice",:template => 'documents/show.pdf.erb')
)
end
end
end
I used of Unixmonkey's solutions above, but then when I upgraded to rails 3.1.rc4 setting the #lookup_context instance variable no longer worked. Perhaps there's another way to achieve the same clearing of the lookup context, but for now, setting the attachment in the mail block works fine like so:
def results_email(participant, program)
mail(:to => participant.email,
:subject => "my subject") do |format|
format.text
format.html
format.pdf do
attachments['trust_quotient_results.pdf'] = WickedPdf.new.pdf_from_string(
render_to_string :pdf => "results",
:template => '/test_sessions/results.pdf.erb',
:layout => 'pdf.html')
end
end
end
Heres' how I fixed this issue:
Removed wicked_pdf
Installed prawn (https://github.com/sandal/prawn/wiki/Using-Prawn-in-Rails)
While Prawn is/was a bit more cumbersome in laying out a document, it can easily sling around mail attachments...
Better to use PDFKit, for example:
class ReportMailer < ApplicationMailer
def report(users:, amounts:)
#users = users
#amounts = amounts
attachments["proveedores.pdf"] = PDFKit.new(
render_to_string(
pdf: 'bcra',
template: 'reports/users.html.haml',
layout: false,
locals: { users: #users }
)
).to_pdf
mail subject: "Report - #{Date.today}"
end
end
Fairly new to Rails 3 and have been Googling every which way to no avail to solve the following problem, with most tutorials stopping short of handling errors.
I have created a Rails 3 project with multiple content types/models, such as Articles, Blogs, etc. Each content type has comments, all stored in a single Comments table as a nested resource and with polymorphic associations. There is only one action for comments, the 'create' action, because there is no need for the show, etc as it belongs to the parent content type and should simply redisplay that page on submit.
Now I have most of this working and comments submit and post just fine, but the last remaining issue is displaying errors when the user doesn't fill out a required field. If the fields aren't filled out, it should return to the parent page and display validation errors like Rails typically does with an MVC.
The create action of my Comments controller looks like this, and this is what I first tried...
def create
#commentable = find_commentable
#comment = #commentable.comments.build(params[:comment])
respond_to do |format|
if #comment.save
format.html { redirect_to(#commentable, :notice => 'Comment was successfully created.') }
else
format.html { redirect_to #commentable }
format.xml { render :xml => #commentable.errors, :status => :unprocessable_entity }
end
end
end
When you fill nothing out and submit the comments form, the page does redirect back to it's appropriate parent, but no flash or nothing is displayed. Now I figured out why, from what I understand, the flash won't persist on a redirect_to, only on a render. Now here's where the trouble lies.
There is only the 'create' action in the comment controller, so I needed to point the render towards 'blogs/show' (NOTE: I know this isn't polymorphic, but once I get this working I'll worry about that then). I tried this in the "else" block of the above code...
else
format.html { render 'blogs/show' }
format.xml { render :xml => #commentable.errors, :status => :unprocessable_entity }
end
Anyway, when I try to submit an invalid comment on a blog, I get an error message saying "Showing [...]/app/views/blogs/show.html.erb where line #1 raised: undefined method `title' for nil:NilClass."
Looking at the URL, I think I know why...instead of directing to /blogs/the-title-of-my-article (I'm using friendly_id), it's going to /blogs/the-title-of-my-article/comments. I figure that extra "comments" is throwing the query off and returning it nil.
So how can I get the page to render without throwing that extra 'comments' on there? Or is there a better way to go about this issue?
Not sure if it matters or helps, but the route.rb for comments / blogs looks like this...
resources :blogs, :only => [:show] do
resources :comments, :only => [:create]
end
I've been plugging away at this over the last few weeks and I think I've finally pulled it off, errors/proper direction on render, filled out fields remain filled in and all. I did consider AJAX, however I would prefer to do it with graceful degradation if at all possible.
In addition, I admit I had to go about this a very hacky-sack way, including pulling in a way to pluralize the parent model to render the appropriate content type's show action, and at this stage I need the code to simply work, not necessarily look pretty doing it.
I KNOW it can be refactored way better, and I hope to do so as I get better with Rails. Or, anyone else who thinks they can improve this is welcomed to have at it. Anyway, here is all my code, just wanted to share back and hope this helps someone in the same scenario.
comments_controller.rb
class CommentsController < ApplicationController
# this include will bring all the Text Helper methods into your Controller
include ActionView::Helpers::TextHelper
def create
#commentable = find_commentable
#comment = #commentable.comments.build(params[:comment])
respond_to do |format|
if #comment.save
format.html { redirect_to(#commentable, :notice => 'Comment was successfully created.') }
else
# Transform class of commentable into pluralized content type
content_type = find_commentable.class.to_s.downcase.pluralize
# Choose appropriate instance variable based on #commentable, rendered page won't work without it
if content_type == 'blogs'
#blog = #commentable
elsif content_type == 'articles'
#article = #commentable
end
format.html { render "#{content_type}/show" }
format.xml { render :xml => #commentable.errors, :status => :unprocessable_entity }
end
end
end
private
# Gets the ID/type of parent model, see Comment#create in controller
def find_commentable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
end
end
articles_controller.rb
class ArticlesController < ApplicationController
def show
#article = Article.where(:status => 1).find_by_cached_slug(params[:id])
#comment = Comment.new
# On another content type like blogs_controller.rb, replace with appropriate instance variable
#content = #article
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #article }
end
end
end
show.html.erb for articles (change appropriate variables for blog or whatever)
<h1><%= #article.title %></h1>
<%= #article.body.html_safe %>
<%= render :partial => 'shared/comments', :locals => { :commentable => #article } %>
shared/_comments.html.erb (I'm leaving out the displaying of posted comments here for simplification, just showing the form to submit them)
<%= form_for([commentable, #comment]) do |f| %>
<h3>Post a new comment</h3>
<%= render :partial => 'shared/errors', :locals => { :content => #comment } %>
<div class="field">
<%= f.label :name, :value => params[:name] %>
<%= f.text_field :name, :class => 'textfield' %>
</div>
<div class="field">
<%= f.label :mail, :value => params[:mail] %>
<%= f.text_field :mail, :class => 'textfield' %>
</div>
<div class="field">
<%= f.text_area :body, :rows => 10, :class => 'textarea full', :value => params[:body] %>
</div>
<%= f.submit :class => 'button blue' %>
<% end %>
shared/_errors.html.erb (I refactored this as a partial to reuse for articles, blogs, comments, etc, but this is just a standard error code)
<% if content.errors.any? %>
<div class="flash error">
<p><strong><%= pluralize(content.errors.count, "error") %> prohibited this page from being saved:</strong></p>
<ul>
<% content.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
I slightly refactored #Shannon answer to make it more dynamic. In my 'find_parent' method I'm grabbing the url path and fetching the controller name. In the 'create' method I'm creating an 'instance_variable_set' which creates a dynamic variable for either Articles (#article) or Blogs (#blog) or what ever it may be.
Hopefully you'll like what I've done? Please let me know if you have any doubts or if something can be improved?
def create
#comment = #commentable.comments.new(params[:comment])
if #comment.save
redirect_to #commentable, notice: "Comment created."
else
content_type = find_parent
instance_variable_set "##{content_type.singularize}".to_sym, #commentable
#comments = #commentable.comments
render "#{content_type}/show"
end
end
def find_parent
resource = request.path.split('/')[1]
return resource.downcase
end
You're getting an error because the blogs/show view likely refers to the #blog object, which isn't present when you render it in the comments controller.
You should go back to using the redirect_to rather than render. It wasn't displaying a flash when you made an invalid comment because you weren't telling it to set a flash if the comment wasn't saved. A flash will persist till the next request.