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.
Backdateable mongoid models – a simple little module I use a lot
I have a lot of models in one of the projects I am currently working on that need to be backdate-able. This is just a simple module I add to any class that needs to support it. It could be easily tweaked for ActiveRecord.
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 | module Backdateable module ClassMethods;end; def self.included(base) base.extend(ClassMethods) #allows this to be an accessor on the front end, although, # if the date is left blank, it gets set properly in before base.send(:field, :backdated, type: Boolean, default: false) base.send(:field, :recorded_at, type: Time) if base.respond_to?(:default_scope) base.send(:default_scope, :asc, :recorded_at) end # Changing this to before_create only allows backdating on creation... base.send(:before_save, :set_recorded_at) end # defaulting recorded at to created_at lets this be sortable by recorded at # we override backdated here in case the date was originally left blank def set_recorded_at self[:recorded_at] ||= self[:created_at] self[:backdated] = (self[:recorded_at] != self[:created_at]) true end end |
Now you can do:
1 2 3 4 5 6 7 8 9 10 11 | class Measurement include Mongoid::Document include Mongoid::Timestamps include Backdateable end Measurement.create # => created_at & recorded_at are set to the same, backdated is set to false Measurement.create(:recorded_at => Time.now.advance(:days => -7)) # => recorded_at is set to the past, backdated is set to true |
Why do I default recorded at to created at? So I can sort by recorded_at.
Done, son.
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.
Matching Unicode characters and strings with Regexp in Ruby 1.9.2
So I've been working on a revamp of a project (still secret, ohhhh ahhh) and I needed Unicode support for multiple languages. I whipped up a pretty simple regexp and found that it didn't work for my test case.
So I've got something equivalent to this running inside some rails rspec test. And my test is failing because I'm expecting some russian words.
1 2 3 | greeting = "Добрый день." greeting.scan( /\w/u ) #=> [] |
WTF.
I double check:
1 2 3 4 5 6 7 8 | greeting = "Добрый день." pattern = /\w/u puts greeting.encoding # => UTF-8 puts pattern.encoding # => UTF-8 |
Hell, I even have "# encoding: UTF-8" at the top of the file.
Fuuuuuuuuuuuuuuuuuuuuuuu.
After some google and some irc'ing I find this answer, which solves the problem and allows for matches of unicode characters in ruby 1.9.2, but I still can't figure out what the 'p' stands for. I'm guessing "please". The problem is apparently there was an issue with \w matching unicode characters, so \w specifically only works with ASCII.
1 2 3 | greeting = "Добрый день." greeting.scan( /\p{Word}/u ) #=> ["Добрый", "день"] |
Wööt.
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
Ruby 1.9 Date#strftime adds a space on %b
So, I had a spec failing in my integration suite and I couldn't figure out what the hell it was - EVERYTHING LOOKED LEGIT. I even logged into the site, and visually verified it. Whats the dilly?
I upgraded to ruby 1.9 from ruby 1.8 and apparently, when doing strftime on date, you get a free whitespace character with "%b"
1 2 3 | # Ruby 1.8 irb(main):006:0> Date.today.strftime("%b %e, %Y") => "May 7, 2010" |
1 2 | irb(main):007:0> Date.today.strftime("%b %e, %Y") => "May 7, 2010" # TWO SPACES |
Super dumb - wasted a good 10 minutes of my day because I could see the diff between:
1 2 | "May 7, 2010" "May 7, 2010" |
In the middle of a web page full of other content.
Why the freebee space character on the %b? Dunno. I just smashed my stftime statement together and my specs are rolling.
1 | Date.today.strftime("%b%e, %Y") |
Uh, yeah - this is my bug report.
Padrino, Compass, and Sass – Working happily via Ian Serlin
My cohort, Ian Serlin, discovered this. In a project we are working on we could get Compass to play well with PadrinoRb. It seems like the #sass method doesn't care about the options being passed to it, and we kept getting stack traces rendered into our CSS files. The stack trace was ruby looking for - and failing to find compass/reset.css.
This is the code that DOESN'T work (padrino 0.9.10, compass 0.8.17, sinatra 1.0).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | configure do register SassInitializer #The Rack Sass reloader... Compass.configuration do |config| config.project_path = File.dirname(__FILE__) config.sass_dir = "stylesheets" config.project_type = :stand_alone config.http_path = "/" config.css_dir = "stylesheets" config.images_dir = "images" config.output_style = :compressed end end get '/stylesheets/:file.css' do content_type 'text/css', :charset => 'utf-8' # This is the doc on how to give Sass some more load paths to find the compass files. # it doesnt work :P and for that matter, neither does: # sass :file, Compass.sass_engine_options # or the set :sass, whatever_hash_here sass :file, :sass => Compass.sass_engine_options end |
Our solution was to force Sass options and Compass options to merge...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | configure do register SassInitializer Compass.configuration do |config| config.project_path = File.dirname(__FILE__) config.sass_dir = "stylesheets" config.project_type = :stand_alone config.http_path = "/" config.css_dir = "stylesheets" config.images_dir = "images" config.output_style = :compressed end Sass::Plugin.options.merge!(Compass.sass_engine_options) end get '/stylesheets/:file.css' do content_type 'text/css', :charset => 'utf-8' sass :file end |
Yay, magic spells - it works. You can check out more ian magic spells at ianserlin.com.
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 |