Rails 3 Easy Search forms using SimpleForm and ActiveModel
For the sites I'm currently working on I have search pages all over the place for different resources, especially on the backend. In the past I would forego using form builders and just do some HTML to make a form GET the data. I hate HTML.
So I did some poking around to see what I could do to DRY up my time on creating these search forms. My first thought was to create a Search class and use active model, but it sucked because I everytime I had to create a new form I had to come into the search class and add a bunch of attr_accessors for all of the search fields. That sucks.
So I decided to mix it up with OpenStruct and ActiveModel
require 'ostruct' class Search < OpenStruct include ActiveModel::Validations include ActiveModel::Conversion extend ActiveModel::Naming def initialize(*args) super end def persisted? ; false ; end; end
Now I use this one model for searching of all of my resources.
I create the form super easily with SimpleForm:
= simple_form_for(@search, url: my_cool_path(), html:{method: :get}) do |f| = f.input :text = f.select :category, Product::CATEGORIES = f.input :minimum_price = f.input :maximum_price = f.submit :search
In your controller you are going to get a params[:search] hash with :text, :category, :minimum_price, :maximum_price.
Want to persist that search across page reloads?
class ApplicationController < ActionController::Base before_filter :persist_search protected def persist_search @search = Search.new params[:search] end end
Now you'll have a Search object in all of your requests with your user's search params. Use it as you will to return them the resources they are looking for.
*Side note*
I use the above logic for all of my resources, until one of my resources has a requirement like a validation. Then I break it into something like FlightSearch:
require 'ostruct' # ostruct for that lazy goodness class FlightSearch < OpenStruct # Get activemodel in there for all its cool functionality like model name, pluralization, validation, yada yada yada include ActiveModel::Validations include ActiveModel::Conversion extend ActiveModel::Naming validates_presence_of :origin_airport, :destination_airport # you can do cool things here, just make sure you call super so ostruct # takes all those hash params and makes them methods def initialize(*args) super end # ActiveModel needs to know that this wasn't persisted. def persisted? ; false ; end; end
Using devise’s scoped url helpers in Cucumber; new_registration_path, new_session_path
You can use devise's scoped url helpers from cucumber, but you have to use them by their real Url Helper method names:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | module NavigationHelpers def path_to(page_name) case page_name when /the sign up page/ # dont use the helper's helper # new_registration_path(:user) # use the underlying url helper new_user_registration_path when /the sign in page/ # dont use the helper's helper # new_session_path(:user) # use the underlying url helper new_user_session_path else #...yada, yada |
cucumber.yml was found, but could not be parsed. Please refer to cucumber’s documentation on correct profile usage.
Yeah, I got that message today and wasted about 30 minutes of my life.
I hadn't changed my cuke yaml file since I started work on the app.
I tried:
* upgrading cucumber (FAIL)
* using another yaml file (FAIL)
* upgrading gherkin (FAIL)
* hard reseting the branch I was on (FAIL)
Turned out to just be a bum rerun.txt file. So I deleted it.
I need a refund on that 30 minutes.
Biting myself on the ass with Cucumber
So I spent a few hours today biting my own ass with cucumber.
If you are ever trying to "see" stuff in your page and notice that it has strangely disappeared, like oh say, your flash messages or validation errors. You may want to pay close attention if you are testing the page you are on just before hand.
I found out a sore lesson today, there is a big difference between:
1 2 | Then I am on the sign up page And I should see "Email can't be blank" |
And
1 2 | Then I should be on the sign up page And I should see "Email can't be blank" |
The difference here? "The I am on" reloads the page. I didn't know that. So I was trying to make sure I was on the right page, then checking for errors, etc. The page would reload, all my errors would be gone, and my feature was failing.
"Then I should be on" tests that the url matches the one you are on.
The more you know.
HamlburgerHelper sets the Table – Easily create and display standard tables in Rails
I create tables a lot in my back end for display information.
I posted a table display/create helper method on dzone that easy to use but has a ton of options.
This is the helper I currently use on my admin backend pages to create the simplest table to a pretty complex table.
Here are all of the available options and their defaults:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | :table_class => 'display_grid', # CSS class name of the table :table_id => "display_grid_#{objects.first.class.to_s.underscore.pluralize}", # CSS ID of the table :heading_class => 'display_grid_heading', # CSS class name of the first TR (one containing TH) :heading_id => "display_grid_heading_#{objects.first.class.to_s.underscore.pluralize}", # CSS ID of the first TR :th_class => 'display_grid_th', # CSS class for all TH elements :tr_class => 'display_grid_tr', # CSS class for all TR elements :td_class => 'display_grid_td', # CSS class for all TD elements :even_odd => true, # Should even/odd classes be added :format_date => nil, #nil | lambda{|datetime|} # Time formatter (receives date object, expects string) :numeric_td_class => 'numeric', # CSS class for any TDs containing a number :date_td_class => 'date', # CSS class for any TDs containing something date-like :string_td_class => 'string', # CSS class for any TDs containing a string # Any of this will create an 'Actions' heading, the lambdas receive the object, and expect a string :show_action => nil, # lambda{|object| link_to '', my_path(object)} :edit_action => nil, # lambda{|object|} :destroy_action => nil, # lambda{|object|} |
Using it is pretty easy, just drop it in your app/helpers folder and...
1 2 3 4 | display_grid(User.all, { :show_action => lambda{|user| link_to user.display_name, public_user_path(user) } :table_id => 'cool_users' }) |
Its pretty simple. It'll dump out a table, and then you can go and style it using the CSS classes/ids above.
Woot, back end DRY.
methods_like helper for finding ruby methods
Here is a little gem I have in my ~/.irbrc I use it a lot when debugging code. I tend to use call #methods a lot on objects to figure out how they quack.
Sometimes I can remember kinda what a method's name is I want to use, but doing something like the following can generate a huge amount of output especially when working with ActiveRecord
1 | my_object.methods.sort |
So I threw together this little method that adds to ruby's base Object class
1 2 3 4 5 6 7 8 | class Object def methods_like(pat) pattern = pat.is_a?(String) ? /#{pat}/ : pat self.methods.sort.select{|m| m =~ pattern } end end |
Now from IRB, Rails console, or Padrino console you can do things like:
my_collection.methods_like(/^update/) #=> [ array of method names that start with update ] user.methods_like("name") #=> [ :first_name, :last_name, :etc ] Array.new.methods_like "sort" #=> ["sort", "sort!", "sort_by"]
Its a handy little snippet when you can't remember an exact method name, and generally for me works faster than googling for it. Toss it in your ~/.irbrc and tell me what you think.
UPDATE
I added some more functionality for getting additional methods (private, protected, singleton):
class Object # pattern - string or pattern to match # access - :public, :private, :protected, :singleton, :all def methods_like(pattern, access = :public, details = false) master_method_list = case access when :public then self.methods when :private then self.private_methods when :protected then self.protected_methods when :singleton then self.singleton_methods when :all ( self.methods + self.private_methods + self.protected_methods + self.singleton_methods ).uniq end matched_method_list = master_method_list.sort.select{ |m| m =~ ( pattern.is_a?(String) ? /#{pattern}/ : pattern ) } if !details matched_method_list else details_list = {} matched_method_list.each do |current_method| details_list[current_method] = method_details(current_method) end details_list end end # Returns details about method def method_details(current_method) current_method = current_method.to_s access_level = if self.methods.include?(current_method) :public elsif self.private_methods.include?(current_method) :private elsif self.protected_methods.include?(current_method) :protected elsif self.singleton_methods.include?(current_method) :singleton end { :owner => self.method(current_method).owner, :arity => self.method(current_method).arity, :receiver => self.method(current_method).receiver, :access => access_level } end end
Now you can do:
variable.methods_like pattern, :private #=> Array of private methods with pattern variable.methods_like pattern, :all, true #=> Hash of methods with additional details
If you pass 'true' for details you will get a hash that looks like:
Array.new.methods_like "sort", :all, true #=> { "sort_by"=>{:owner=>Enumerable, :access=>:public, :receiver=>[], :arity=>0}, "sort!" =>{:owner=>Array, :access=>:public, :receiver=>[], :arity=>0}, "sort" =>{:owner=>Array, :access=>:public, :receiver=>[], :arity=>0} }
Access options available are :public, :private, :singleton, :protected, :all
Attaching local or remote files to Paperclip and Milton Models in Rails (Mocking content_type and original_filename in a Tempfile)
I was working on a project today where I needed to import some data from MySpace accounts (yeah, MySpace), which included importing the users profile image. In the controller that did the importing I was using OpenURI to retrieve the image and then turn it into a Tempfile to be attached the the model, like so:
1 2 3 4 5 6 7 | def import #...snip tempfile = Tempfile.new( my_filename) tempfile.write open( image_url ).read @imported_user.images.create(:file => tempfile) #...snip end |
This doesn't work. It blows up missing one of two methods:
- #original_filename
- #content_type
If you inspect a normal file upload in Rails which has these methods, you'll find that is just a regular old Tempfile. But it has the methods! If you create a Tempfile manually, it won't have the methods. That's because Rails magics them on. I am not sure why they don't create a subclass like Rails::Tempfile that contains these methods and just use that. I guess its because OOP is retarded (sarcasm).
So, I wrote a little subclass that will take a file path, and quack like the magic'd rails Tempfiles you get from an upload so you can attach local files to models or even remote files.
This works pretty straightforward and I'm currently using it in production. It won't work on windows systems because the dependency on the 'file' binary. Also, some linux systems are missing this library by default, so make sure you yum|dpkg|apt|port or whatever to get it installed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | require 'open-uri' require 'digest/sha1' class RemoteFile < ::Tempfile def initialize(path, tmpdir = Dir::tmpdir) @original_filename = File.basename(path) @remote_path = path super Digest::SHA1.hexdigest(path), tmpdir fetch end def fetch string_io = OpenURI.send(:open, @remote_path) self.write string_io.read self.rewind self end def original_filename @original_filename end def content_type mime = `file --mime -br #{self.path}`.strip mime = mime.gsub(/^.*: */,"") mime = mime.gsub(/;.*$/,"") mime = mime.gsub(/,.*$/,"") mime end end |
Usage is pretty simple:
1 2 3 | remote_file = RemoteFile.new("http://www.google.com/intl/en_ALL/images/logo.gif") remote_file.original_filename #=> logo.gif remote_file.content_type #= image/gif |
Using it in your controller:
1 2 3 4 5 | def import #...snip @imported_user.images.create(:file => RemoteFile.new( url_to_image )) #...snip end |
Expiring rails fragement caches using an expires time instead of a sweeper (expires_in) – (a ‘duh’ post)
So, I'm setting up fragment caching for some stuff, and I honestly don't care about setting up an observer to sweep some stuff. I just want to cache this one slow fragment for like 5 minutes. I've seen these tutorials all over the web that shows people using the expires_in setting to auto-expire cache like so:
1 | Rails.cache.write('test_key', 'test_value', :expires_in => 5.minutes) |
That's great and all, but how the hell do I use it for a Page, Action or Fragment cache? Well you just pass the same :expires_in param and yay it works as long as its a mem_cache_store (although, I did see a hackaroo for file_store)
Even the Rails Guide on caching mentions 'expires_in' but doesn't show how to use it.
1 2 3 | class StupidController < ApplicationController caches_page :whatever, :expires_in => 5.minutes end |
What really got me was when I was trying to use it with a fragment cache. I tried this and it didn't work:
1 | - cache(:action => 'home', :action_suffix => 'advertisements', :expires_in => 10.minutes) do |
Why? Well, the cache method takes two hashes, and if you leave off the curly braces, that expires_in ends up in the first one, which is used for the name... To roll it right do:
1 | - cache({:action => 'home', :action_suffix => 'advertisements'}, :expires_in => 10.minutes) do |
Duh, the more you know.
A Rails rake file for compressing your Javascript with YUI
Here is a quick rake file I threw together to use with my Ruby YUI 2.0 (I-refuse-to-make-a-gem-
edition).
This of course should be used with the Ruby YUI Compressor I posted about the other day.
This script will tar/gzip all of your javascripts just incase something stupid happens. So you have a little safety net.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | require 'fileutils' require 'pathname' namespace :js do desc <<-INFO Output Compressed JavaScript to STDOUT; COMPRESS=ALL to compress all Javascript files that DONT contain ".min." INFO task :yui => :environment do targz() if ENV['COMPRESS'] == 'ALL' Dir[Rails.root.to_s / :public / :javascripts / '**/*.js'].each do |javascript| next if javascript =~ /\.min\./ compress(javascript) end else if File.exist?(Rails.root + "config/yui.yml") javascripts = YAML.load(File.open("config/yui.yml").read)["javascripts"] if !javascripts.blank? javascripts.each {|script| compress(Rails.root + "public/javascripts" + script)} else raise Exception, "No javascript files in config/yui.yml" end else raise Exception, "config/yui.yml Not Found; Do rake js:generate to create one or COMPRESS=ALL to run without a config file" end end end desc "Generate YUI Compressor Config file" task :generate => :environment do config_path = Rails.root + "config/yui.yml" File.open(config_path, "w+") do |f| f.puts <<-CONFIG --- javascripts: - "application.js" - "jquery.js" CONFIG puts config_path end end def targz() tgz_file = "#{Time.now.to_i}.javascripts.tgz" `cd #{Rails.root + "public"}; tar -zcf #{tgz_file} javascripts/` puts "Backed up assets to: #{tgz_file}" end def compress(path) puts "Compressing #{path}" file_handle = File.open(path) compressed_output = YUI.compress_safe file_handle file_handle.close #overwrite the file File.open(path, "w+") { |file| file.puts compressed_output } end end |
Wanna use it?
1. Drop it in your lib/tasks folder or wherever your Rakefile looks
You can compress 'everything' or specific files.
If you want to only compress specific Javascript files do:
1 2 3 | rake js:generate
# Edit your config/yui.yml file
rake js:yui |
If you want to compress "everything" (It actually won't compress files that contain ".min.". You can remove the regexp's if you don't like it.)
1 | rake js:yui COMPRESS=ALL |
Yay, now your raking it in (Pun harhar) w/ your web2.0 yui compressed rails app. Woot woot.
An Excel CSV exporter for ActiveRecord
This is a mix of two blog posts on exporting ActiveRecord data to CSV. This explicitly was designed to export stuff so excel wouldn't freak out.
This strips new lines from any string field, set the BOM and converts the encoding to utf16.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | require "fastercsv" require "iconv" # Excel info from: http://blog.plataformatec.com.br/2009/09/exporting-data-to-csv-and-excel-in-your-rails-app/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed:+PlataformaBlog+(Plataforma+Blog) # Original post: http://www.brynary.com/2007/4/28/export-activerecords-to-csv # Usage: # Given User is an ActiveRecord model # # Options for Model.to_csv, Array.to_csv, and ActionController::Base#send_csv # # :only - Array, trumps :exclude. Only these attributes will be included instead of all attributes (things listed in :methods will still be sent) # :only => [ :id, :name, :created_at ] # :methods - Array, additional methods to evaluate and add to the CSV response. Note: you can send nested method calls # :methods => [ :to_s, :complex_method, "my.method.on.a.related.object"] # :exclude - Array, attributes to exclude from CSV result # # From ActionController # send_csv User, :only => [:first_name, :email, :created_at] #This will do all records # send_csv User.all(:conditions => ["created_at > ?", some_date]), :only => [:first_name, :email, :created_at] # # From ActiveRecord Model # User.to_csv :only => [ :username, :email, :created_at ], :methods => [ :age, "account.id"] # All users additionally get their related Account#id number # User.all(:limit => 10).to_csv :exclude => [:password, :birthdate] # class ActiveRecord::Base # Shortcut for CSV of whole table def self.to_csv(*args) find(:all).to_csv(*args) end # Get the column headers def self.csv_columns(options={}) tmp_columns = if options[:only] options[:only] #only trumps exclude else self.content_columns.map{|curr_col| curr_col.name } - options[:exclude].map{|curr_col| curr_col.to_s } end tmp_columns + options[:methods].map{|curr_col| curr_col.to_s } end # Record to a row level csv array def to_csv(options={}) self.class.csv_columns(options).map { |curr_col| curr_col = curr_col.to_s #Its a chain of method calls, on intermediary nil, just return nil if !curr_col.index(".") col_val = self.send(curr_col) else col_val = self curr_col.split(".").each {|curr_method| col_val = col_val.send(curr_method) unless col_val.nil?} end col_val.gsub!("\n", " ") if col_val.is_a?(String) # Strip newlines, Appease Excel Gods col_val } end end class Array # Convert an array of objects into a CSV String. def to_csv(options = {}) column_options = { :only => options.delete(:only), :exclude => options.delete(:exclude) || [], :methods => options.delete(:methods) || [] } options = { :col_sep => "\t" # Appease Excel Gods }.merge(options) if all? { |e| e.respond_to?(:to_csv) } header_row = first.class.csv_columns(column_options).to_csv content_rows = map { |e| # Get all the values of non-excluded rows e.to_csv(column_options) }.map{|r| # Call to_csv on the array of row-level values, this will join them into a CSV row r.to_csv(column_options) } ([header_row] + content_rows).join else FasterCSV.generate_line(self, options) end end end # module for extending ActionController module ExcelCSVExporter BOM = "\377\376" #Byte Order Mark, Appease Excel Gods def send_csv(kollection, options={}) filename = options.delete(:filename) || I18n.l(Time.now, :format => :short) + ".csv" content = kollection.to_csv(options) # Appease Excel Gods content = BOM + Iconv.conv("utf-16le", "utf-8", content) send_data content, :filename => filename end end ActionController::Base.send :include, ExcelCSVExporter |
Wanna use it?
1 2 3 4 5 6 7 | class MyCoolController < ActionController::Base def index respond_to {|want| want.csv{ send_csv MyCoolModel.all, :exclude => [:secret_column], :methods => [:complex_method, "related.table.method"]} } end end |