skinny models! yay!
6 December 2011 // Filed under Uncategorized
This guy doesn’t deserve credit for this idea, but his explanation is on point.
http://qualityonrails.com/archives/33
:: Share or discuss :: 2011-12-06 :: admin
6 December 2011 // Filed under Uncategorized
This guy doesn’t deserve credit for this idea, but his explanation is on point.
http://qualityonrails.com/archives/33
:: Share or discuss :: 2011-12-06 :: admin
26 January 2011 // Filed under action mailer + delayed job
Sending emails inside your request/response cycle is one of the slower things you can do to users. A while ago, I contrived a way to delay every single email in my application in one fell swoop, without having to change my models and controllers that were actually sending the email. That is, I wanted to keep the existing deliver_* calls in tact, so that my application code continued to look like rails code.
In Rails 2.3.x, using Delayed Job, here is what I did:
For the record, I only have a single ActionMailer subclass, so this simplifies things quite a bit. In the event you have more than one you’ll need to modify the following to also pass the class name to delayed job. (not hard)
class Notifier < ActionMailer::Base
#...a bunch of mailer methods
def self.method_missing(method, *args)
if method.to_s.match(/^deliver_(.*)/)
Delayed::Job.enqueue(DelayedNotifier.new("create_#{$1}", args))
else
super(method, *args)
end
end
In a nutshell, we're hijacking ActionMailer's built-in deliver_* methods, changing them to create_* methods, and sending it to delayed job. Here's what Delayed Notifier is:
class DelayedNotifier < Struct.new(:method, :args)
def perform
mail = Notifier.send(method, *args)
Notifier.deliver(mail)
end
end
The class that you send to delayed job must respond to #perform in order for Delayed Job to work correctly. So, delayed job grabs the method name, the arguments that were passed in, builds the TMail object and sends it. Again, since I only have a single Mailer class, I don't need to worry about passing around the name of that class.
Note that the call from your controller/model to deliver_* doesn't actually build a Tmail object.*** All it does is build the DelayedNotifier object and save it to the DB. So our servers are *not* creating emails and sending them once. That would be lame.
Well, this broke with Rails 3.
Rails 3's ActionMailer API is a bit different than the deliver_* methods that we've been used to. It looks like this:
Notifier.password_reset_instructions(user).deliver
Well, crap, now it looks like I *have* to build the TMail Mail object inside of the request/response cycle. I really don't want to do that, sooooo, let's bring deliver_* back. Yea, seriously.
class Notifier < ActionMailer::Base
def self.method_missing(method, *args)
if method.to_s.match(/^deliver_(.*)/)
Delayed::Job.enqueue(DelayedNotifier.new($1, args))
else
super(method, *args)
end
end
end
And the new, fancy Delayed Notifier struct:
class DelayedNotifier < Struct.new(:method, :args)
def perform
Notifier.send(method, *args).deliver
end
end
You use this the same way you used mailers in rails 2.3.x. That is, with deliver_*. Blasphemy, I know, but look on the bright side: Now you don't have to grep your code and update all calls to deliver_*. Sweet!
P.S.
Delayed Job allows you to handle exceptions as you see fit, and hoptoad handles exception notification like no other. It's a match made in heaven.
require 'hoptoad_notifier'
class DelayedNotifier < Struct.new(:method, :args)
def perform
Notifier.send(method, *args).deliver
end
def failure(job, exception)
HoptoadNotifier.notify(exception)
end
end
***
TMail objects are very large (html, plain text, headers, attachments, etc...). If you designed this such that the controller built the TMail object, and then saved that to the DB, you would need to make your delayed_jobs' handler column's type mediumtext or bigger, since you run the risk of there not being enough room in the column for the entire TMail object. Also, you're building the TMail object inside the request/response cycle which is slow and completely unnecessary. Keep it simple, save the method name and the arguments required to build the TMail object and let Delayed Job do the rest.
1 comment :: Share or discuss :: 2011-01-26 :: admin
26 January 2011 // Filed under Uncategorized
I launched into a rails 3 upgrade for my main side project last night. I’m going to use it as an excuse to blog about a bunch of things that I’ve been meaning to write about, but never got around to.
Here’s a quick look at the scope of this app:
$ rake stats +----------------------+-------+-------+---------+---------+-----+-------+ | Name | Lines | LOC | Classes | Methods | M/C | LOC/M | +----------------------+-------+-------+---------+---------+-----+-------+ | Controllers | 1867 | 1616 | 23 | 154 | 6 | 8 | | Helpers | 403 | 351 | 0 | 55 | 0 | 4 | | Models | 3310 | 2701 | 58 | 331 | 5 | 6 | | Libraries | 441 | 401 | 2 | 38 | 19 | 8 | | Integration tests | 600 | 340 | 4 | 6 | 1 | 54 | | Functional tests | 8763 | 8421 | 22 | 21 | 0 | 399 | | Unit tests | 5230 | 4794 | 85 | 10 | 0 | 477 | +----------------------+-------+-------+---------+---------+-----+-------+ | Total | 20614 | 18624 | 194 | 615 | 3 | 28 | +----------------------+-------+-------+---------+---------+-----+-------+ Code LOC: 5069 Test LOC: 13555 Code to Test Ratio: 1:2.7
:: Share or discuss :: 2011-01-26 :: admin
1 March 2010 // Filed under Uncategorized
<rant>
There has been, and continues to be, a lot of talk around writing semantic code. One thing that gets under my skin, and I wish it would stop, is the continued use of #can?(:do_something) as a pattern for handling permissions. I have no problem with the pattern itself, it’s the verb I take issue with. “Can” asks for the ability to do something, “may” asks for permission” Get it right people.
Doesn’t anyone else remember the following scenario from second grade?
Miniature you: “Teacher, teacher! Can I go do the bathroom, please?”
Mrs. WhatsHerName: “I don’t know, can you?”
Miniature, pissed off, you: “Well, yea, I can…” <blank stare />
I’m looking at you cancan, canable and walruz.
If you still don’t get it read this.
As far as I know, the only plugin out there that handles this (semantic issue) correctly is Makandra’s Aegis. And they are GERMAN. It functionally works about the same as well. So props to them for that.
</rant>
1 comment :: Share or discuss :: 2010-03-01 :: admin
10 February 2010 // Filed under javascript
I did a ground up re-write of my date-time picker. It now only relies on jQuery (1.4.1) and jQuery UI (1.8rc1).
Use case is pretty simple right now:
$(function(){
$('#datetimepicker').dateTimePicker();
$('#datepicker').dateTimePicker({showTime: false});
$('#timepicker').dateTimePicker({showDate: false});
});
See the demo for more details. Enjoy!
:: Share or discuss :: 2010-02-10 :: admin
25 January 2010 // Filed under rails
For one of my apps I’ve developed what I think is a very easy to use and consistent way of handling error and success messages throughout my app. It would be a lie if I called the following list a set of goals since they evolved over time, but there’s definitely a bit of philosophy driving any future changes that I make to this aspect of the system.
1) Error/Success messages should look and feel the same throughout the application. This means that the HTML and CSS that displays them should be located in one place and not scattered across several different views and stylesheets. It also shouldn’t matter if there is one error or a list of errors.
2) I should be able to generate and display these errors whether this is a synchronous or an asynchronous request.
3) Everything should quack.
The relevant code from ApplicationController:
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
class ApplicationController < ActionController::Base
helper_method :errors_for, :success_for
private
def success_for(message)
unless message.blank?
render_to_string :partial => 'shared/success', :locals => {:message => message}
end
end
def errors_for(object)
unless object.respond_to?(:errors) && object.errors.empty?
render_to_string :partial => 'shared/error', :locals => {:object => object}
end
end
end
Making them helper methods is a necessary evil so that they can be called from both the controller and the view. Calling them from the view should be obvious. Say you have an Active Record object with errors on it:
= errors_for @user_with_errors
…and calling it from inside a controller is extremely useful when you need to pass errors back to an AJAX request:
render :json => {:errors => errors_for(user_with_errors)}
This is where people start to get feisty. “You shouldn’t be passing HTML back with JSON. JSON is for passing raw data.” I say rules are meant to be broken. If I passed the raw data (the errors) back in JSON I would have to create the associated markup in javascript. So not only am I duplicating logic, I’m doing it in two different languages (javascript, ERB/HAML). That sucks.
Here’s my error partial, in HAML:
-if object.is_a?(String) || object.is_a?(Array)
.errorWrapper{:style => "width:#{display_width(object)}px;"}
-if object.is_a? String
%p= object
-elsif object.is_a? Array
%ul
-object.each do |message|
%li= message
-elsif object.kind_of?(ActiveRecord::Base) || object.kind_of?(Authlogic::Session::Base)
.errorWrapper{:style => "width:#{display_width(object)}px;"}
=error_messages_for(:object => object, :id => nil, :header_message => nil, :message => nil)
I know that kind of looks a mess, and some slight duplication, but it’s all for the greater good. First off, the partial renders nothing if it gets passed the wrong Thing. Secondly, it’s wrapped up nicely by errorWrapper, which gives us a consistent place to start writing styles from. We do this since you cannot (currently) change the class that error_messages_for gives you. Thirdly, we’re using error_message_for so that we can seamlessly pass ActiveRecord and AuthLogic objects and it won’t blink.
The success partial is a bit simpler since we don’t expect objects to have “success” messages attached to them. Instead it accepts single strings or an array of strings:
.success{:style => "width:#{display_width(object)}px;"}
-if object.is_a? String
%p= object
-elsif object.is_a? Array
%ul
-object.each do |message|
%li= message
And finally, a bit of “making things look nicer”:
module ApplicationHelper
ERROR_COEF = 8.0
def display_width(object)
(if object.is_a?(String)
object.length
elsif object.is_a?(ActiveRecord::Base) || object.is_a?(UserSession)
object.errors.full_messages.map(&:length).max
elsif object.is_a?(Array)
object.map(&:length).max
end * ERROR_COEF).to_i
end
end
A bit of a mess again, but what we’re doing here is defining a width for the wrapping container based on the length of the error messages. This is so that we don’t just arbitrarily set our container to be 600px wide for errors that are only three words. (This looks pretty awful) You might need to play around with that coefficient a bit to make it work with the rest of your styles — and even that may not be very precise. That said, I’ve yet to run into any error or set of errors that this didn’t handle very nicely.
So how do we use all of this? Pretty much any way you want! Throw an errors_for(@object) in your edit template and forget it. If @object has errors on it, they get displayed, if not, then no markup is generated. Not even a wrapping container.
Want to create an array of errors for an AJAX request and display them? No problem.
error_messages = []
error_messages << "Err"
error_messages << "Foo"
render :json => {:errors => errors_for(error_messages)}
Then, in your javascript (jQuery)
$('form#login').prepend(response.errors);
What about success messages? They’re probably passed in some kind of Flash.
Controller: flash[:success] = "The post was successful" View =success_for flash[:success]
This is what I’m doing, how are you handling this problem?
:: Share or discuss :: 2010-01-25 :: admin
6 January 2010 // Filed under rvm
This recently happened to me:
$ rake db:migrate /Users/jsharpe/.rvm/ree-1.8.7-2009.10/lib/ruby/1.8/openssl/ssl.rb:31: [BUG] Bus Error ruby 1.8.7 (2009-06-12 patchlevel 174) [i686-darwin9.8.0], MBARI 0x8770, Ruby Enterprise Edition 2009.10 Abort trap
I’m really not sure who the culprit is here. I don’t think it’s REE since it also happens with 1.8.7. So is it 1.8.7? Is it RVM? Openssl? Or is it me? Whatever the case this took me a LONG time to figure out – I hope this post saves many thousands of people countless wasted hours.
Add this to ~/.bash_profile
export RUBYOPT="-ropenssl"
4 comments :: Share or discuss :: 2010-01-06 :: admin
13 November 2009 // Filed under active record
I personally don’t think this is all that exciting — but I see this question asked a lot and just want something to point at from now on.
Say you have several ‘nested’ has_many relations:
class State < ActiveRecord::Base has_many :cities end class City < ActiveRecord::Base has_many :streets belongs_to :state end class Street < ActiveRecord::Base has_many :houses belongs_to :city end class House < ActiveRecord::Base belongs_to :street end
How do you get all of the Houses in virginia?
Well, you could do this:
virginia.cities.collect{|c| c.streets}.flatten.uniq.collect{|s| s.houses}.flatten.uniq
... but that's epically lame. It looks like shit and produces an metric ton of queries and as a result is highly inefficient.
How about this:
House.find(
:all,
:include => {:street => {:city => :state}},
:conditions => {'states.id' => virginia.id}
)
This is a single query with joins where appropriate, and it's a finder on House which is what you're getting anyways. Makes sense, no?
4 comments :: Share or discuss :: 2009-11-13 :: admin
27 October 2009 // Filed under active record
Every once in a while I see this little code snippet show up somewhere:
script_console_running = ENV.include?('RAILS_ENV') &&
IRB.conf[:LOAD_MODULES] &&
IRB.conf[:LOAD_MODULES].include?('console_with_helpers')
rails_running = ENV.include?('RAILS_ENV') &&
!(IRB.conf[:LOAD_MODULES] &&
IRB.conf[:LOAD_MODULES].include?('console_with_helpers'))
irb_standalone_running = !script_console_running && !rails_running
if script_console_running
require 'logger'
Object.const_set(:RAILS_DEFAULT_LOGGER, Logger.new(STDOUT))
end
You see, if you drop this little gem in ~/.irbrc you’ll start getting the SQL output from your various ruby commands. This is an excellent debugging tool, but it’s an even better learning tool.
To the novice the difference between the following two statements may not be readily apparent…
User.find(:first).posts.each{|p| p.comments.do_something}
User.find(:first, :include => {:posts => :comments}).posts.each{|p| p.comments.do_something}
…but as soon as they sit and watch what scrolls by in the terminal they will *very* quickly realize that one of them is *very* wrong.
I submit that the next time you are teaching anyone the basics of AR that this be the very first thing that you introduce them to.
:: Share or discuss :: 2009-10-27 :: admin
22 October 2009 // Filed under authentication
A basic authentication scheme should go to some length to do a little bit of remembering in the event your user hits a restricted page before they are actually authenticated.
First, some context. I’ve got this in my ApplicationController:
def require_user
unless current_user
store_location
redirect_to login_path
return false
end
end
def store_location
session[:return_to] = request.request_uri
end
def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
session[:return_to] = nil
end
… which allows me to do this in any of my controllers:
before_filter :require_user, :except => [:index, :show]
… and this is cool because now my users get redirected back to where they were going after they log in. I like saving my users a click or two here and there if at all possible.
There’s a problem with this though. What happens if somehow your user POSTs to an action that is behind require user? Well request.request_uri is *not* what you want to redirect to. Why? Because redirect_to is going GET the same url that your user was trying to POST to. And if you’re being a good Rails developer and using resources then you’re just going to smack them in a face with a big fat 404, and that’s not very nice.
I actually managed to do all of this over at github earlier today by trying to comment on a commit without being logged in. Want to see it in action? (until github fixes their ways) First, go to github and logout. Then find any commit and try to make a comment, you can figure the rest out. Go ahead, I’ll wait.
What is that critter anyways? It’s like a half octopus with a lizard tail and whiskers. wtf.
Anyways, here’s the simple fix:
def store_location
session[:return_to] =
if request.get?
request.request_uri
else
request.referer
end
end
If they’re GETing something let em keep on GETing it, otherwise take them back to where they were coming from.
This solution is only half-baked though. Any form data that the user has filled out will be gone and they’ll have to redo it. On the other hand you really shouldn’t be showing a form to an unauthenticated user that requires them to be logged in to hit it. If you really insist on doing that have fun saving all those parameters in the session or somethin in between requests. I’d imagine that’ll suck.
1 comment :: Share or discuss :: 2009-10-22 :: admin