Topic: 5 short Ruby on Rails how-tos
Here are a few useful things I have discovered while learning in Ruby on Rails, I hope they are useful to you and feel free to correct me on anything that is not right or if you have a better solution to any of these problems.
I am very new to programming and I am self-taught, so I do not claim to know what I am talking about. I do, however, try to do things "correctly," so please do not hesitate to enlighten me further if you see something you don't like.
1. Escaping HTML
Purpose: to increase security from cross-site scripting, and to ensure the the proper rendering of your page.
Let's say a hypothetical user wanted to be cool and decided that he'd enter this as his username:
<span style="color:red; font:bold 48px 'Comic Sans MS';border:thick dashed orange; background-color:green;">WomenLoveMe77</span>
If you don't escape the HTML, you'd end up with this when the username is displayed. But thankfully, if you escape the HTML the username will just be the text that was input and the browser will not render it as HTML.
Just use
<%=h @user.userName %><!-- instead of -->
<%= @user.userName %>
<!-- By the way, the "h" is really an alias for this: -->
<%= html_escape(@user.userName) %>
2. Encoding email links:
Purpose: to decrease the harvesting of email addresses by those pesky interweb spyderbots.
You can encode mail_to links using javascript, or if you don't want to require javascript, use hex instead:
<%= mail_to @user.email, "Email Me", :subject => "Hi there", :encode => "javascript" %>
# => <script type="text/javascript">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%65%78%61%6d%70%6c%65%40%65%78%61%6d%70%6c%65%2e%63%6f%6d%3f%73%75%62%6a%65%63%74%3d%48%69%25%32%30%74%68%65%72%65%22%3e%45%6d%61%69%6c%20%4d%65%3c%2f%61%3e%27%29%3b'))</script><%= mail_to @user.email, "Email Me", :subject => "Hi there", :encode => "hex" %>
# => <a href="mailto:%65%78%61%6d%70%6c%65@%65%78%61%6d%70%6c%65.%63%6f%6d?subject=Hi%20there">Email Me</a>
This certainly does not provide foolproof protection from your address getting out there to spammers, but it definitely helps and is an easy way to decrease the chances of a spambot getting addresses off of your site.
If you want something more secure than this, check out the CipherMail plugin, but this is a nice and easy way to make things a little more secure.
3. Using a select menu to redirect to the selection
Purpose: to create a select menu that redirects to the selected item's page on change.
You can read about the different select menu helpers here. But here is an example using the collection_select method.
To make the redirect happen, all you have to do is add something like :onchange => "document.location = '/controller/' + this.value" to your select method:
<%= collection_select(:contact, :id, Contact.find(:all, :order => :firstName), :id, :fullName, { :prompt => "Select a Contact" }, :onchange => "document.location = '/contacts/' + this.value")%>
You can use virtual attributes to display additional
info in the dropdown instead of just one field.
# contact.rb
def fullName
"#{firstName} #{lastName}"
end
4. Creating a two-tiered navigation system with dynamic links
Purpose: to built a dynamic site-wide navigation system.
Here is a solution I came up with for a global navigation link system. The site for which I built this is not restful and is mostly static content, so please excuse the ugly non-named routes I am using.
Goals:
1. Global and completely dynamic system
2. Two-tiered:
a. main-links that are displayed on every page
b. sub-links that are only displayed with their parent
3. Display to the user what the current page is.
4. Easily modifiable and understandable. All links are consolidated.
We start by creating a couple of app-wide helper methods that serve as a sort of site map. This will provide a way for us to easily see what our links will be on each page, which is nice since this scenario includes sub-navigation which is only visible when it's parent is selected.
# application_helper.rbdef get_main_links
@mainLinks = [
"Home",
"Products",
"About Us"
]
enddef get_sub_links(owner)
case owner
when "home"
@subLinks = [
'Welcome',
'Watch Demo Video'
]when "products"
@subLinks = [
'Now Shipping',
'In Development'
]when "about_us"
@subLinks = [
'Our Employees',
'Contact Us'
]
end
end
I also setup a little method to convert our human-readable links to something good for display in the URL:
def linkify(string)
string.gsub(' ', '_').downcase
end# linkify("About Us") => "about_us"
If anyone knows how to turn that into an inflector so we can do "@string.linkify" instead please let me know.
In the view we will use two methods built into rails to make the links dynamic:
link_to_unless_current
This will check both the controller and the action, so we will use this for the sub-links, but it won't work for the main ones, so we'll use:
link_to_unless
With this, we can be more specific. We can pass controller.controller_name and compare that to the main nav links.
Now for the view code:
<!-- layouts/application.html.erb --><div id="navigation">
<div id="mainLinks">
<ul>
<%- get_main_links
for l in @mainLinks -%><!-- get the sublinks for each main link so we know what action to send -->
<% get_sub_links(linkify(l)) %>
<li><%= link_to_unless controller.controller_name == linkify(l), l, { :controller => linkify(l), :action => linkify(@subLinks.first) } %></li>
<%- end -%>
</ul>
</div>
<div id="subLinks">
<ul>
<!-- get the sublinks for the current controller -->
<%- get_sub_links(controller.controller_name)
for l in @subLinks -%>
<li><%= link_to_unless_current l, :action => linkify(l) %></li>
<%- end -%>
</ul>
</div>
</div> <!-- End Navigation -->
5. Tagging a single field with multiple check boxes
Purpose: to provide a simple, dynamic solution for tagging a model with various options that are not mutually exclusive.
Here's what I like about this method:
1. No DB modification or extra relationships needed, just uses a single string field
2. Simple and pretty output of tags. As a string by default, but easily converts into an array.
3. Allows for multiple selections in one field as opposed to a select menu, which is limited to one.
In this example I'll use an address, which can take multiple roles (shipping, billing, etc.)
Let's start with the view so we can see where we are headed:
<!-- inside form for "address" -->
<%= tag_with_check_boxes "address", "addressOptions", @address, ["Shipping", "Billing", "Store", "Office", "PO Box", "Home"] %><!-- or say you want to wrap each box in a div with a class of 'inlineCheckbox' -->
<%= tag_with_check_boxes "address", "addressOptions", @address, ["Shipping", "Billing", "Store", "Office", "PO Box", "Home"], { :startTag => "<div class='inlineBoxes'>", :endTag => "</div>" } %>
This will create a checkbox for each tag you pass, and optionally wrap it in some tags so you have control over the formatting.
Now for the helper method:
# Application Helperdef tag_with_check_boxes(modelName, virtualAttribute, object, tags = [], options = {})
options.reverse_merge! :startTag => "", :endTag => "" # allows for options to be blank@storedTags = object.send(virtualAttribute) # use "send()" to dynamically pass the attribute name
@checkboxTags = ""for tag in tags
@checkboxTags += options[:startTag] + check_box_tag("#{modelName}[#{virtualAttribute}][]", tag, tag_stored?(tag, @storedTags)) + "<label>#{tag}</label>" + options[:endTag]
end@checkboxTags
end# set boxes as checked or unchecked for editing already existing data.
def tag_stored?(tag, storedTags)
if storedTags # check if the record exists so it doesn't break the "edit" action if the field is empty or the "new" action.
storedTags.include? tag
end
end
Now we need to add a virtual attribute to handle the array we will send through the form:
# Address Modeldef addressOptions # we need an alias getter method for 'addressType' in order to keep the function dynamic
self.addressType
enddef addressOptions=(tags)
self.addressType = tags.join(', ')
end
This does not take care of any validation or anything like that, but you can do that if you choose.
If you don't ship to PO Boxes, for example, you could validate that "PO Box" and "Shipping" are not both checked at the same time.
If you want to play with the tags as an array, just do:
@address.addressType.split(', ')
and you can do whatever else you need with it.
One potential problem with this method is that if you want to remove all tags from an entry, the update action doesn't know that you deselected the checkboxes since it only updates values that are sent to it (blank checkboxes == no addressOptions param). If you want to allow the removal of all tags, you can add this to the "update" action of your controller:
if not params[:address][:addressOptions]
@address.addressType = ""
end
Conclusion
I hope that this was beneficial to you, and please let me know how I can improve upon these methods. Thanks for reading, and many thanks to Ryan Bates of Railscasts for helping me learn Ruby on Rails, and for encouraging me and many others to contribute to the community.
Last edited by Zef (2008-05-05 03:42:41)