Thursday, February 17, 2011

Authlogic activation tutorial


Step 1

Let’s begin by adding an ‘active’ field with a default of false to the user model.
script/generate migration AddActiveToUsers active:boolean
# new migration XXX_add_active_to_users.rb
class AddActiveToUsers < ActiveRecord::Migration
def self.up
add_column :users, :active, :boolean, :default => false, :null => false
end

def self.down
remove_column :users, :active
end
end

Step 2

Don’t forget to run the migration.
rake db:migrate
Authlogic automatically executes the following methods, if present, upon user action: active?, approved?, and confirmed?. Since we added an active column, the “active?” method will be dynamically generated by ActiveRecord so we can hook into this magical goodness. And we should make sure that we protect the active attribute from mass-assignments by calling attr_accessible.
# added to user.rb
attr_accessible :login, :email, :password, :password_confirmation, :openid_identifier

Step 3

Now try to log in. You should receive the error, “Your account is not active.” So far so good. Let’s make a controller to handle our activations:
script/generate controller activations create
# new file app/controllers/activations_controller.rb
class ActivationsController < ApplicationController
before_filter :require_no_user

def create
@user = User.find_using_perishable_token(params[:activation_code], 1.week) || (raise Exception)
raise Exception if @user.active?

if @user.activate!
flash[:notice] = "Your account has been activated!"
UserSession.create(@user, false) # Log user in manually
@user.deliver_welcome!
redirect_to account_url
else
render :action => :new
end
end

end

Step 4

I raise exceptions in these actions to make sure that someone who is already active cannot re-activate their account and to deal with an invalid perishable token. I’ll leave it up to you how you want to handle these errors — you should probably provide some sort of “My Token is Expired!” action that will reset the token and resend the activation email if the user does not get around to activating right away.
Going down the list, let’s define the missing actions. First:
# added to user.rb
def activate!
self.active = true
save
end

Step 5

Next, let’s make sure our user gets an e-mail with his activation code when he signs up. How are we getting our activation code? The same way we get our password reset code — through our perishable token:
# added to app/models/user.rb
def deliver_activation_instructions!
reset_perishable_token!
Notifier.deliver_activation_instructions(self)
end

def deliver_welcome!
reset_perishable_token!
Notifier.deliver_welcome(self)
end

# added to app/models/notifier.rb
def activation_instructions(user)
subject "Activation Instructions"
from "noreply@binarylogic.com" # Removed name/brackets around 'from' to resolve "555 5.5.2 Syntax error." as of Rails 2.3.3
recipients user.email
sent_on Time.now
body :account_activation_url => activate_url(user.perishable_token)
end

def welcome(user)
subject "Welcome to the site!"
from "noreply@binarylogic.com"
recipients user.email
sent_on Time.now
body :root_url => root_url
end

# added to config/routes.rb
map.activate '/activate/:activation_code', :controller => 'activations', :action => 'create'


Thank you for creating an account! Click the url below to activate your account!

<%= @account_activation_url %>

If the above URL does not work try copying and pasting it into your browser. If you continue to have problem, please feel free to contact us.


Welcome to the site! Your account has been activated.

<%= @root_url %>

If the above URL does not work try copying and pasting it into your browser. If you continue to have problem, please feel free to contact us.

Note: I got a “Missing host to link to! Please provide :host parameter or set default_url_options[:host]” error when I tried this, so if you do to you will have to set the default_url_options in your environment.rb or environment config.

config.action_mailer.default_url_options = {:host => "binarylogic.com"}

Step 6

Now let’s modify the user create action:
# modified app/controllers/users_controller.rb
def create
@user = User.new(params[:user])

# Saving without session maintenance to skip
# auto-login which can't happen here because
# the User has not yet been activated
if @user.save_without_session_maintenance
@user.deliver_activation_instructions!
flash[:notice] = "Your account has been created. Please check your e-mail for your account activation instructions!"
redirect_to root_url
else
render :action => :new
end
end

Ruby Script: Save String To File


Ruby script to save a string to a file.
# Save a string to a file.
myStr = "This is a test"
aFile = File.new("myString.txt", "w")
aFile.write(myStr)
aFile.close

Tuesday, February 15, 2011

Rails.root


Guys, have you ever tried Rails.root. It gives same value as RAILS_ROOT. But now in edge rails it is modified and you can do much more with it.
You can use Rails.root at places like:
1
File.join(RAILS_ROOT, 'public', 'images')
as
1
Rails.root.join('public', 'images')
Both gives the same result.

Wednesday, February 9, 2011

Editing file uploads with a Paperclip processor


NOTE: Apparently there are issues with this code and the latest Paperclip gem (currently 2.3.4) – its down to the use of reprocess and this known issue 
i’m currently looking at a work around
 here’s a patch fix
I use Paperclip for pretty much all upload processing. Its flexible, fast and easily extendable. One particular feature that has cropped up (a couple of times now) – has been the ability to edit and update the contents of uploaded files. For example; editing css, html or javascript in a CMS. Something I’ve needed for Bugle
In the past I struggled getting this to work with Paperclip. You’ll find me rambling to myself in the mailing list almost a year ago. I figured the Shopify guys we’re doing this in their app, so it had to be possible.
One solution, was to simply read the file contents (on create) from the uploaded file into a database column. Then on future requests for the file, serve it virtually from the database; through a Rails controller/action responding with the appropriate content type and content data.
But, this meant the Rails app would be handling all the css/js requests in the CMS. I really wanted to serve these uploaded files from S3/Cloudfront making full use of Amazon’s CDN. So I set about building a Paperclip::Processorto store the file contents (in the database) on create then on update, update contents and re-upload the file again. To work with cache expiry in the CDN I could use the updated_on timestamp in the URL to the file.
Here’s most of the code below, i’ve also created a git repository with a working simple app. I’m using a RESTful UploadsController with an Upload model. The model has an Paperclip attachment (asset) and the file contents (for editable files) are stored in a TEXT column (‘asset_contents’ in the database).

Controller

Nothing crazy going on here, just straight forward RESTful controller logic (without a show action)
class UploadsController < ApplicationController                       

def index
@uploads = Upload.scoped
end

def new
@upload = Upload.new
end

def edit
@upload = Upload.find(params[:id])
end

def create
@upload = Upload.new(params[:upload])
if @upload.save
flash[:notice] = 'Upload was successfully created'
redirect_to uploads_url
else
render 'new'
end
end

def update
@upload = Upload.find(params[:id])
if @upload.editable? && @upload.update_attributes(params[:upload])
flash[:notice] = 'Upload was successfully updated'
redirect_to uploads_url
else
render 'edit'
end
end

def destroy
@upload = Upload.find(params[:id])
if @upload.destroy
flash[:notice] = 'Upload was successfully deleted'
end
redirect_to uploads_url
end
end

Model

Two things to notice here. I’m using lambda’s on the style and processor attributes. In both cases they check the content-type to see if the file is either editable or thumbnailable. If it is a thumbnailable image, I give it a thumbnail style and the thumbnail (default) Paperclip::Processor. For editable files, I give it a style for the original file only, and use the new FileContents Processor (see below). The style hash sets which database column will be used for storing the file contents, in this case it’s the ‘asset_contents’ attribute.
Second is the after_update hook. When thew Upload model gets saved, I want Paperclip to reprocess the asset again. This ensures that when the asset is saved on update the FileContents processor executes. The thumbnailable? and editable? methods let you decide what file types should be considered for processing.
class Upload < ActiveRecord::Base

after_update :reprocess

has_attached_file :asset, :styles => lambda { |a|
if a.instance.thumbnailable?
{:thumb => ["64x64#", :jpg]}
elsif a.instance.editable?
{:original => {:contents => 'asset_contents'}}
end
},
:path => "/:id/:style/:basename.:extension",
:storage => :s3,
:s3_credentials => "#{Rails.root}/config/s3.yml",
:bucket => "paperclip-example-bucket-#{Rails.env}",
:processors => lambda { |a|
if a.editable?
[:file_contents]
elsif a.thumbnailable?
[:thumbnail]
end
}

attr_protected :asset_file_name, :asset_content_type, :asset_size

validates_attachment_size :asset, :less_than => 6.megabytes
validates_attachment_presence :asset

def editable?
return false unless asset.content_type
['text/css', 'application/js', 'text/plain', 'text/x-json', 'application/json', 'application/javascript',
'application/x-javascript', 'text/javascript', 'text/x-javascript', 'text/x-json',
'text/html', 'application/xhtml', 'application/xml', 'text/xml', 'text/js'].join('').include?(asset.content_type)
end

def thumbnailable?
return false unless asset.content_type
['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png', 'image/jpg'].join('').include?(asset.content_type)
end

private
def reprocess
asset.reprocess! if editable?
end
end

FileContents Paperclip::Processor

This processor basically reads the uploaded file contents on create and sets the asset_contents attribute. On update, it creates a new Tempfile with its content from the asset_contents attribute and then returns this Tempfile for Paperclip uploading. Comments in the code below explain further, (place this file in lib/paperclip/file_contents.rb).
module Paperclip
class FileContents < Processor

def initialize file, options = {}, attachment = nil
@file = file
@options = options
@instance = attachment.instance
@current_format = File.extname(attachment.instance.asset_file_name)
@basename = File.basename(@file.path, @current_format)
@whiny = options[:whiny].nil? ? true : options[:whiny]
end

def make
begin
# new record, set contents attribute by reading the attachment file
if(@instance.new_record?)
@file.rewind # move pointer back to start of file in case handled by other processors
file_content = File.read(@file.path)
@instance.send("#{@options[:contents]}=", file_content)
else
# existing record, set contents by reading contents attribute
file_content = @instance.send(@options[:contents])
# create new file with contents from model
tmp = Tempfile.new([@basename, @current_format].compact.join("."))
tmp << file_content
tmp.flush
@file = tmp
end

@file
rescue StandardError => e
raise PaperclipError, "There was an error processing the file contents for #{@basename} - #{e}" if @whiny
end
end
end
end

Views

The view code is simple, a new and edit form with a textarea for contents editing.
# uploads/new.html.erb
<%= form_for(:upload, :url => uploads_path,
:html => { :method => :post, :multipart => true }) do |f| %>

<input type="file" name="upload[asset]"> <%= f.submit 'upload', :disable_with => 'uploading ...' %>
<% end %>

# uploads/edit.html.erb
<%= form_for @upload do |f| %>
<%= f.text_area :asset_contents, :rows => 20, :cols => 100, :id => 'file_asset_contents' %>
<p><%= f.submit 'Save changes', :disable_with => 'saving ...' %>p>
<% end -%>

# reference upload URL always with timestamp
<%= @upload.asset.url(:original, true) %>

Some Gotchas

If you are using an Amazon S3 bucket, make sure you set it to be ‘world’ readable, so your uploaded files are publicly accessible. Also, the file_contents.rb processor should live in lib/paperclip/file_contents.rb. And for a Rails 3 add this to your load path, in config/application.rb
config.autoload_paths += %W(#{Rails.root}/lib)
I’ve been running this code with no issues in production for some time now. I should point out that I limit these editable uploads to ~3Mb-6Mb and you may have performance issues with larger files. Some solutions could be to use delayed_job (or something similar) to background process the task, and/or change the processor code to read/write one line at a time.