Be a Zeek

Posts tagged ‘Rails’

Spree Uname Login/Signup

Spree default provides login/signup with just email.

But now a days many E-Coms are taking a trend to introduce username or phone number as alternate login options.

This article is all about how to use alternate options (other than email) for login/signup in a Spree app. I am targeting username here but using other options will be much similar to that.

Start with adding DB migration to add uname attribute to User model.

$ rails g migration add_uname_to_spree_users
class AddUnameToSpreeUsers < ActiveRecord::Migration
  def up
    add_column :spree_users, :uname, :string
    add_index :spree_users, :uname

    Spree::User.where(uname: nil).each do |user|
      user.send(:set_uname)
      user.save!
    end
  end

  def down
    remove_index :spree_users, :uname
    remove_column :spree_users, :uname
  end
end

In migration, you will notice a loop to set_uname for existing users.

# In User model (user_decorator.rb)
def set_uname
  if self.uname.blank? && self.email.present?
    # Fetch string prior to @ from emailID
    username = self.email.split(/@/)[0]
    # remove chars other than numbers and alphabets
    username = username.titleize.gsub(/[^a-zA-Z0-9]/, '')
    count = 0
    # add counter with username if some other user already has it
    while Spree::User.find_by('lower(uname) = :value', value: username.downcase)
      username += (count += 1).to_s
    end

    self.uname = username
  end
end

This set_uname ensures username for each user to be extracted out from its email and only allowing alphabets and numbers to be a part of uname. You can change logic here to suit your need.

If you want to auto assign username to user, you can

# In User model (user_decorator.rb)
before_validation :set_uname, on: :create

We are set now to code/customise Spree login/signup process.

Signup
If you want user to set uname of their choice while signing up set

# In config/initializers/spree.rb
Spree::PermittedAttributes.user_attributes.push(:uname)

Update signup form accordingly to have uname text field.

Login
Change login form email text_field to login text field,

# CHANGE
<%= f.email_field :email %>
# TO
<%= f.text_field :login %>

Add login authentication key to devise,

# In config/initializers/devise.rb
Devise.setup do |config|
  config.authentication_keys = [ :login ]
end

Change the way devise finds user,

# In User model (user_decorator.rb)
def self.find_for_database_authentication(warden_conditions)
  conditions = warden_conditions.dup
  if login = conditions.delete(:login)
    where(conditions.to_hash).where('lower(uname) = :value OR lower(email) = :value', value: login.downcase).first
  else
    conditions[:email].downcase! if conditions[:email]
    where(conditions.to_hash).first
  end
end

And you should be good to go.

Advertisements

Permittribute

https://github.com/techzeek/permittribute

We sometimes face scenarios where we need same permitted attributes for a model in different controllers and end up re-writing all attributes again (violating DRY).

Permittribute is meant to:

  • reuse same permitted attributes at different locations/controllers.
  • scope permitted attributes base on roles like admin, api etc.
  • group permitted attributes at a single reference point in their respective scopes.

Usage

This is used with rails/strong_parameters which is now incorporated since rails 4.x.
We use strong parameters something like:

#in controllers/articles_controller.rb
def article_params
  params.require(:article).permit([:title, :author_id, :published_at, :publication])
end
#in controllers/admin/articles_controller.rb
def article_params
  params.require(:article).permit([:title, :author_id, :published_at, :publication])
end
# in controllers/api/articles_controller.rb
def article_params
  params.require(:article).permit([:title, :author_id, :published_at, :publication])
end

With Permittribute we can do:

# in lib/permittribute/default.rb
Permittribute.configure do
  config.articles = [:title, :author_id, :published_at, :publication]
  # Use as #permittribute_articles
end
# in lib/permittribute/admin.rb
Permittribute.configure(:admin) do
  config.articles = [:title, :author_id, :published_at, :publication]
  # Use as #permittribute_admin_articles
end
# in lib/permittribute/api.rb
Permittribute.configure(:api) do
  config.articles = [:title, :author_id, :published_at, :publication]
  # Use as #permittribute_api_articles
end

And

# in controllers/articles_controller.rb
def article_params
  params.require(:article).permit(permittribute_articles)
end
# in controllers/admin/articles_controller.rb
def article_params
  params.require(:article).permit(permittribute_admin_articles)
end
# in controllers/api/articles_controller.rb
def article_params
  params.require(:article).permit(permittribute_api_articles)
end

Getting Started

Add this line to your application’s Gemfile:

gem 'permittribute'

And then execute:

$ bundle

Or install it yourself as:

$ gem install permittribute

After you have installed Permittribute or added it to your Gemfile, you need to run the generator:

$ rails generate permittribute:install
  • This will create three configurations file, lib/permittribute/default.rb, lib/permittribute/admin.rb and lib/permittribute/api.rb. These contains some example configurations.
  • adds config.eager_load_paths += ["#{Rails.root}/lib/permittribute"] in config/application.rb. To eager load created config files.

Rails tricky timezones

Configuring timezones in rails can be tricky if one doesn’t understand the actual config settings and their use.

Ruby’s Time can also get really confusing when used in Rails app.

Consider Time.now and Time.zone.now

Time is a ruby library hence Time.now will return system’s time. To change that for your Rails app use time_zone config.

Say, I have a server in UTC and all my users are in IST then my rails app time_zone should be like,

#config/application.rb
config.time_zone = ‘IST’

In Rails console,
Time.now #returns time in UTC
=> 2015-07-12 11:26:05 +0000

Time.zone.now #returns time in IST
=> Sun, 12 Jul 2015 16:56:46 IST +05:30

In fact, in rails use Time.current which is a mix of both. Read code here.

ActiveRecord and DB timezones
(Considering MySQL here)

By default, our DB picks system’s timezone (here UTC) and our application here is using IST. Hence when we save a time using Rails app in IST, it actually is saved in UTC in our DB.

So ideally, if we have a time saved in DB as 2015-07-12 11:26:05 +0000 then we should retrieve this in our rails app (in IST) as datetime object Sun, 12 Jul 2015 16:56:05 IST +05:30.

Tricky active_record.default_timezone

There is another setting in Rails, config.active_record.default_timezone, with default value as :local (picks system’s timezone). This config is responsible to identify timezone for our ActiveRecord datetime objects.

So lets say, our DB is on another server with system timezone as UTC and our application is now on a server with system and application timezone as IST, which now means Time.now and Time.zone.now are same.

In this case, config.active_record.default_timezone has value as IST (:local) which will tell our system that the datetime values we are getting from DB are in IST timezone. Hence it will just convert the timezone of the value and not the datetime value itself resulting in wrong results,

  • Actual datetime value in DB, 2015-07-12 11:26:05 +0000 (which is considered in IST timezone by our app due to active_record.default_timezone)
  • Retrieved datetime value in app, Sun, 12 Jul 2015 11:26:05 IST +05:30

To handle these situation we just have to set,

#config/application.rb
config.active_record.default_timezone = :utc

And now the expectation of ActiveRecord datetime object’s timezone from our app will be of UTC which is actually same as DB’s timezone.