codedecoder

breaking into the unknown…


Leave a comment

raw sql for rails joins

There are time when ActiveRecord query will not server our purpose and you needs to write raw sql query.

Lets understands with examples when such case arise and how to write raw sql. We will also see how irresponsibly written code crash your system.

One of our application facing a lots of out of memory exception in Heroku and also some of payments not at all getting render in UI as Heroku kills any process which take more than 30 seconds.

Now question is why any request will need more than 30 seconds to load ?.

I was given the task to find the issue and fix it.

The model association in the Application look something as below:

class Payment < ActiveRecord::Base
  has_many :ledgers, dependent: :destroy, autosave: true
  has_many :invoices, through: :ledgers, autosave: true
  has_many :remittances, dependent: :destroy, autosave: true
end

class Ledger < ActiveRecord::Base
  belongs_to :invoice
  belongs_to :payment
end

class Invoice < ActiveRecord::Base belongs_to :customer_information has_many :ledgers has_many :payments, :through => :ledgers
end

class Remittance < ActiveRecord::Base
  belongs_to :payment
end

The business requirement look straight forward, say you have raised invoice for any service and than client will send you Payment for a bunch of invoices. Ledger is the join table between invoice and payment.

Remittance is something like you received extra payment which you return back to Client.
Here the Remittance table do not have direct link with Invoice table. The money returned may be part on a invoice or without it. The Remittance table do have a name column which keep Invoice number and possibly match to a invoice in Invoice table.

Now let see, how current code is written and Problem with it.

Case 1 : Display all the invoices with a payment on UI

Existing Code look as below:

payment_ledgers = payment.ledgers.includes(:invoice).where(matched: true)
payment_ledgers.each do |ledger|
  payment_invoice = ledger.invoice
  if payment_invoice && payment_invoice.remaining_amount > 0        
    customer = payment_invoice.customer_information.customer
    customer_name = customer.legal_name.present? ?  customer.legal_name : customer.name         
    date = payment_invoice.date.strftime("%m/%d/%Y") if payment_invoice.try(:date)
    invoice_hash = {
      number: payment_invoice.number, date: date,
      customer_name: customer_name, remaining_amount: payment_invoice.remaining_amount.to_f.round(2),
      total_amount: payment_invoice.original_amount,applied_amount: payment_invoice.applied_amount,
      id: payment_invoice.id
    }
    invoice_hash.merge!({ledger_id: ledger.id, ledger_applied_amount: ledger.applied_amount.to_f.round(2)})
    invoices << invoice_hash
  end
end

So what developer did here is that. He fetched all the ledgers with a payment and eager loaded invoices so he do not needs to hit a query again on DB.He looping over them as he needs data from 2 other tables also – invoices and customer_informations.

But, this approach have below issue:
It will return say 500 ledgers and than 500 invoices also. So there will be 1000 object occupying your memory. Worst part is you are looping over the ledgers, so the memory will be not released till process complete.

The optimized code look as below:

# Find Payment's Ledger Invoices whose remaining amount greater than zero
# Query will pluck required columns from all the three tables - Ledger, Invoice, CustomerInformation
invoices_data = Ledger.includes(invoice: :customer_information)
                .where("ledgers.payment_id = ?  and matched = ? and invoices.remaining_amount > ?", payment.id, true, 0)
                .pluck('ledgers.id', 'ledgers.applied_amount', 'customer_informations.legal_name', 'customer_informations.name',
                 'invoices.id', 'invoices.number', 'invoices.date', 'invoices.remaining_amount', 'invoices.original_amount', 'invoices.applied_amount'
                )
#Convert invoices_data to array of hash
invoices_data.each do |inv_data|
  invoices << {
    ledger_id: inv_data[0], 
    ledger_applied_amount: inv_data[1].to_f.round(2),
    customer_name: inv_data[2].present? ? inv_data[2] : inv_data[3],
    id: inv_data[4],
    number: inv_data[5], 
    date: inv_data[6],
    remaining_amount: inv_data[7].to_f.round(2),
    total_amount: inv_data[8].to_f.round(2),
    applied_amount: inv_data[9].to_f.round(2)
  }

The above Code with includes generate below sql query:
#The Sql Query above will generate is:
SELECT ledgers.applied_amount, invoices.number, customer_informations.legal_name FROM “ledgers” LEFT OUTER JOIN “invoices” ON “invoices”.”id” = “ledgers”.”invoice_id” LEFT OUTER JOIN “customer_informations” ON “customer_informations”.”id” = “invoices”.”customer_information_id” WHERE “ledgers”.”payment_id” = $1 [[“payment_id”, 4]]

So what we do is:

  1. we removed any looping in ruby code.
  2. We fetched the needed data from 3 tables from DB query itself.
  3. We are not fetching ActiveRecord object but set of info as strings.

You can see that we manged to write DB query with ActiveRecord itself. We do not need any raw sql here…. Rails is great !

Case 2 : Show all Remittances with a payment on UI

Existing Code look as below:

payment_rmh = []
payment.remittances.order('remittance_sequence_number').each do |rmh|
  invoice_id = Invoice.find_by('lower(number) = ? ',rmh.name.downcase).try(:id) if rmh.name.present?
  payment_rmh << {name: rmh.name, amount: rmh.amount,invoice_id: invoice_id}
end

So what developer is doing here is first finding all remittances with Payment, looping over each remittance. Finding Invoice whose number match with remittance name.

Problem here is that Your DB query will become proportional to number of remittances. If 1000 remittances you will make 1000 call to DB. Your system start chocking with memory and execution time will increase.

NOTE: Never ever make DB query within a loop

Now, lets try to optimized the code by eager loading approach in last case:

Remittance.includes(invoice: :customer_information)
Remittance Load (31.0ms) SELECT “remittances”.* FROM “remittances”
ActiveRecord::AssociationNotFoundError: Association named ‘invoice’ was not found on Remittance; perhaps you misspelled it?

So we can see that, includes will not work here unlike last case, as there is no direct association between remittances and invoices. So ActiveRecord failed to generated the Query for us.

Here we need to move out of ActiveRecord queries and build our own raw sql query.

The optimized code look as below:

# Find Payment RMH
rmh_raw_query = "SELECT ri.name, ri.amount, inv.id FROM remittances ri LEFT JOIN invoices inv ON LOWER(ri.name) = LOWER(inv.number) WHERE ri.payment_id = #{payment.id} ORDER BY remittance_sequence_number ASC"
rmh_records = ActiveRecord::Base.connection.exec_query(rmh_raw_query).rows
payment_rmh = rmh_records.map{|rme| {name: rme[0], amount: rme[1].to_f.round(2),invoice_id: rme[2]}}

Here the way to execute raw sql query with rails to call the exec_query method of ActiveRecord::Base.connection.

We have called row method on result returned by exec_query method which basically will return all column you need as an array.

With the above optimization :

  1. 1000 remittances took less than half seconds as compared to 20 seconds earlier.
  2. It will be independent of number of children.Unlike earlier approach, it will always take less than 1 second even if no of remittances reach 1 lakhs or more.
  3. There will be no load on memory.
Advertisements


Leave a comment

Devise session out handling for ajax request rails

Devise work out of box for maintaining user session. But it fails to handle redirect in case of ajax request.

Problem  Description:

  1. User logged in to Rails Application using Devise Auth flow.
  2. Go to fill a form but left for some work and comes back in 15 minutes(divise timeout configured for 15 minutes).
  3. Try to submit the form through ajax call and you show the loder to indicate form got submitted.
  4. The loader keep rotating as the request intercepted in between as unauthorized(401 error in console).

Here Devise did its job and unauthorized the user. The only thing it failed to do is redirect user to login page. If you reload the page you can see that user redirected to login page. So our goal is to exhibit same behavior in ajax request also.

Solution:

The only thing we have to handle is that, make Devise to redirect to login page if user session got expired when we make Ajax call. I looked for the way Devise handling the redirect and got idea from this issue thread on devise. The redirect logic of devise in case of unauthentication is written in the file failure_app.rb .

The redirect logic is written as below in the file

def redirect_url
  if warden_message == :timeout
    flash[:timedout] = true if is_flashing_format?

    path = if request.get?
      attempted_path
    else
      request.referrer
    end

    path || scope_url
  else
    scope_url
  end
end

If you see it do not handle redirect for ajax request i,e of format .js.

The beauty of Ruby is that we can open any class and add our custom behavior. So lets customize the above class. Create a file lib/custom_failure_app.rb and add below line to it.

class CustomFailureApp < Devise::FailureApp 
  def redirect_url 
    if request.xhr? 
      send(:"new_#{scope}_session_path", :format => :js)
    else
      super
    end
  end
end

So we just told Devise that, if xhr request, redirect to login page with format js.

Now we have to configure Devise to use our CustomFailure APP – config/initializer/devise.rb

config.http_authenticatable = false
config.http_authenticatable_on_xhr = false
config.navigational_formats = ["*/*", :html, :js]

config.warden do |manager|
  manager.failure_app = CustomFailureApp
end

So next time whenever ajax request is made on session timeout user will be redirected to users/sign_in.js path.

create view/devise/sessions/new.js.erb file and add below line:

 window.location = "/"

At this point I was expecting that, things will start working now. But it is close but not working as expected. Below is the log:

Completed 401 Unauthorized in 31ms (ActiveRecord: 18.2ms)

Started GET “/users/sign_in.js” for 127.0.0.1 at 2018-07-02 21:26:22 +0530
Processing by Customized::SessionsController#new as JS
Completed 406 Not Acceptable in 3ms (ActiveRecord: 0.0ms)

ActionController::UnknownFormat (ActionController::UnknownFormat):

So, the desired behavior is there, on ajax session expire user got redirected to users/sign_in.js . The error here is because new action of Devise session controller is defined as below:

  # GET /resource/sign_in
  def new
    self.resource = resource_class.new(sign_in_params)
    clean_up_passwords(resource)
    yield resource if block_given?
    respond_with(resource, serialize_options(resource))
  end

It is using respond_with which is not handling .js format. So I overrided it as below:

1 – Changed default devise routes

  devise_for :users, controllers: { sessions: 'customized/sessions' }

2 – Overrided the new method in controllers/customized/sessions.rb .

class Customized::SessionsController < Devise::SessionsController
    
  # GET /resource/sign_in
  def new
    self.resource = resource_class.new(sign_in_params)
    clean_up_passwords(resource)
    yield resource if block_given?
    respond_to do |format|
      format.js
      format.html
    end
  end
end

Now everything working fine. If session got expired while ajax request and user got redirected to login page.


Leave a comment

Subdomain on localhost Rails

Our product is in production and we decided to have a new subdomain which will target a different set of Users. The end goal is to have the same Code base but render different CSS based on subdomain.

Since our App is hosted on Heroku, the first thing I do is checked feasibility of two DNS pointing to same app, I raised the below ticket on heroku:

https://help.heroku.com/sharing/af2a96f9-1559-45c4-9459-db805fe29229

Heroku Confirmed that it is quite easy to do, so now we move ahead with the development.

Here the first problem is to get two URL pointing to our same App in local.

We all know and use below URL in local:

http://localhost:3000

We need another URL as below:

http://MBEportal.localhost:3000

You can achieve this with below steps:

1 – Login as admin user on terminal:

sudo su –

2 – Edit /etc/hosts file

nano /etc/hosts

The above command will open /etc/hosts file for you in terminal.

You will find few lines there, the important one is:

127.0.0.1 localhost

This is the line which basically map localhost to IP 127.0.0.1

Add a new line below it:

127.0.0.1 MBEportal.localhost

This is telling that the new DNS MBEportal.localhost should also map to IP 127.0.0.1

press cntr + x to exist editing

It will ask you to save before existing. Press Y to save the change.

3 -restart your rails server.

Now you can access your localhost at both the below URL

http://localhost:3000/

http://MBEportal.localhost:3000

4 – Parse Subdomain in Controller

So, at this point we have  simulated subdomain behavior. Also both hitting same code base as expected.

But still when I go to controller action and try to see subdomain, it return empty array

request.subdomains -> []

It look like our server treating http://MBEportal.localhost as a single domain.

This can be fixed by adding below line in environment/development.rb file

config.action_dispatch.tld_length = 0

Restart your server again and this time you will get the subdomain from URL

request.subdomains -> [“mbeportal”]

great ! now we can find the subdomain from request object and thus execute different logic or layout or any other thing specific to the particular domain.

Some other detail which you can read from request object are:

request.base_url -> http://MBEportal.localhost:3000
request.host -> mbeportal.localhost
request.domain -> localhost

 


Leave a comment

undefined method `eof?’ for #

Below is one of my API code in my new Rails application

module Api
  module V1
    class VendorOnboardingController < Api::BaseController
      include Concerns::Api::V1::VendorOnboardingApipie
      def enrollment
        result = Parse::Vendor.new(Hash.from_xml(request.body).deep_symbolize_keys!)
        ...remaining code----
      end
    end
  end
end

The API working fine locally and on server running thin as application server.After testing we moved the code to UAT server running on Passenger as application server and the API boomed with below error:

“undefined method `eof?’ for PhusionPassenger::Utils::TeeInput:0x000056122fc69a20”

After breaking a lot of head and changing code with hit and trial method realized the culprit code is request.body in below line of code.

result = Parse::Vendor.new(Hash.from_xml(request.body).deep_symbolize_keys!)

changing it to request.body.read as below fixed the issue.

result = Parse::Vendor.new(Hash.from_xml(request.body.read).deep_symbolize_keys!)

Understanding the Problem:
Rails communicate with application server through Rack.

Infact Rack sits between all the frameworks (Rails, Sinatra, Rulers) and all the app servers (thin, unicorn, Rainbows, mongrel) as an adapter. Now if you see  rack spec it says

The input stream is an IO-like object which contains the raw HTTP POST data. When applicable, its external encoding must be “ASCII-8BIT” and it must be opened in binary mode, for Ruby 1.9 compatibility. The input stream must respond to gets, each, read and rewind.

So in my local or with thin the request body was always coming as a StringIO object, so not causing any problem when the subsequent line of my code operating on it, but with passenger some how, this is not the case so code is breaking.

calling request.body.read will explicitly convert the request body to StringIO object and so fixed the problem.


					
		
	


1 Comment

fatal error: error writing to /tmp/ccAtSusl.s: No space left on device

Recently get a frustrating error on one of the server. I introduced a new gem amatch in my rails application. I do not see any issue on local, dev or UAT server instance in installing the gem through the bundler, But on QC the bundler failed with below error:

Installing amatch 0.3.0 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

creating Makefile

make "DESTDIR=" clean

make "DESTDIR="
compiling amatch_ext.c
amatch_ext.c:1661:1: fatal error: error writing to /tmp/ccAtSusl.s: 
No space left on device
}
^
compilation terminated.
make: *** [amatch_ext.o] Error 1

make failed, exit code 2

Since the error clearing saying that No space left on device, I checked the space on server.

$df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/xvda1       35G  6.1G   27G  19% /
none            4.0K     0  4.0K   0% /sys/fs/cgroup
udev            1.9G   12K  1.9G   1% /dev
tmpfs           375M  304K  375M   1% /run
none            5.0M     0  5.0M   0% /run/lock
none            1.9G     0  1.9G   0% /run/shm
none            100M  4.0K  100M   1% /run/user
overflow        1.0M     0  1.0M   0% /tmp

But I see there is a lot of space on the disk

Since, the error say problem while writing to tmp folder, I checked permission of temp folder, thinking may we it causing problem.

$ ls -l / | grep tmp
drwxrwxrwt 3 root root 4096 Nov 19 13:51 tmp

But it also showing all the permissions of read and write.

Then I tried to remove all the content of tmp folder thinking , may be something is there which taking all the space

$ rm -fr /tmp/*

But again got the same error.

Now googling for some time on space distribution and partitioning on linux, came to know about overflow partition .

And I got the problem, you can see that the in the df -h command output above, the tmp is mounted on overflow . This will happen if your server continuously keep writing to tmp folder, and if you have not given a separate partition to tmp .  When system deduct some partition or memory issue it automatically mount the tmp folder to overflow partition with size of 1 MB, so that things keep working. But there is no automatic reversal i,e bringing back the tmp to root partition.

Now this could be happened on our server over the time. We have consumed our disk space, system moved tmp folder to overflow, later on we free the space by deleting unused file or increasing space, thus having free space as shown above in df -h command. But tmp folder didn’t moved back from overflow partition and having size of 1 MB. Since amatch need more then 1 MB, it is failing with space issue.

So solution is simple, unmount the tmp folder and mount it again.

# umount /tmp
umount: /tmp: device is busy.
        (In some cases useful info about processes that use
         the device is found by lsof(8) or fuser(1))

You need to kill the process using tmp folder, get the list of those process, with below command:

# lsof /tmp
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
Passenger 6730 root   13rW  REG   0,23        0 14247683 /tmp/passenger

Kill the running process 6730 and try to unmount again.

# kill -9 6730
# umount /tmp

Mount tmp folder again and see the disk space again.

# mount -a
# df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/xvda1       35G  6.1G   27G  19% /
none            4.0K     0  4.0K   0% /sys/fs/cgroup
udev            1.9G   12K  1.9G   1% /dev
tmpfs           375M  308K  375M   1% /run
none            5.0M     0  5.0M   0% /run/lock
none            1.9G     0  1.9G   0% /run/shm
none            100M  4.0K  100M   1% /run/user

You can see that, there is no separate overflow partition for tmp folder as it now mounted on the root partition, thus it can use all the available memory in the main partition.

This time amatch didn’t reported any issue and installed successfully.

CHECKLIST IF YOU FACE THIS PROBLEM:

  1. Check permission of folder
  2. Check available space.Free space and see if solve your problem
  3. If on overflow partition unmount and mount it again.

Resources:
http://unix.stackexchange.com/questions/15024/umount-device-is-busy-why
http://www.tldp.org/LDP/intro-linux/html/sect_03_01.html


Leave a comment

mixin in ruby : include prepend extend

Ruby do not support multiple inheritance directly, but implement it through Mixin . Mixin in ruby is achieved with include, prepend and extend. prepend is introduced with Ruby 2.0. Lets see, how they work and differ from each other.

Lets define few module and classes. We will experiment with them on irb or Rails console and try to understand their behaviour

module Introduction
  def introduce
    puts "Iam Arun"
  end
end

module Welcome
  def greet
    puts "Hi..How are you"
  end
end

module Host
  def serve_food
    puts "chiken biryani"
  end
end

module SeeOff
  def bye
    puts "Byee..."
  end
end

class Person
  prepend Introduction
  include Welcome
  include Host
  extend  SeeOff
end

class Celebrity
  
end

Copy paste above code in irb and keep experimenting as you proceed below :

1 => include and prepend add the module method as instance method, while extend add it as class method.

Calling as class method :

2.2.1 :058 > Person.introduce
NoMethodError: undefined method `introduce’ for Person:Class

2.2.1 :059 > Person.greet
NoMethodError: undefined method `greet’ for Person:Class

2.2.1 :060 > Person.bye
Byee…

You can see that, only bye method of module SeeOff worked as it is extended , so added as class method

Calling as instance method

2.2.1 :061 > Person.new.introduce
Iam Arun

2.2.1 :062 > Person.new.greet
Hi..How are you

2.2.1 :063 > Person.new.bye
NoMethodError: undefined method `bye’ for #<Person:0x8edec04>

Here include and prepend added the module method as instance method to the class, so it worked, but bye method of SeeOff module failed as it is added as a class method so can’t be call on the instance of Person.

Now considering the above behaviour, you may think when to use mixin as class method(using extend) and when to use as instance method(using include or prepened). It depended on the feature a module is providing and how you want to reuse it in your class. Infact in a ordinary class also, you may use the below thumb rule to define a method as a instance method or a class method.

Define a method as class method if you say want to use it within different methods of a class. say prity_print can be a class method
as it can be used in any method to well format the content before retuning it.

Second scenario is that the method behaviour not depend on the instance variable, for example it is not good idea to make full_name as class method as it depend on user’s first_name and last_name which differ from user to user, But say active_users can be a class method as it do not depend on any individual user

2. Adding module method at runtime : use extend to add method only to a specific instantiated object and use include or prepend to add instance methods to all the instance .

Above, the Celebrity class do not have any method, nor it has included or extended any of the module. Now say we want to add bye method at run time to Celebrity.

2.2.1 :065 > Celebrity.bye
NoMethodError: undefined method `bye’ for Celebrity:Class

> Celebrity.extend SeeOff
=> Celebrity
2.2.1 :067 > Celebrity.bye
Byee…

Now say. we have Katrina and Deepika as two celebrity.

2.2.1 :074 > katrina = Celebrity.new
=> #<Celebrity:0xe317eb0>
2.2.1 :075 > deepika = Celebrity.new
=> #<Celebrity:0xe3161f0>

But we want to greet katrina only

2.2.1 :076 > katrina.extend Welcome
=> #<Celebrity:0xe317eb0>

2.2.1 :077 > katrina.greet
Hi..How are you

2.2.1 :078 > deepika.greet
NoMethodError: undefined method `greet’ for #<Celebrity:0xe3161f0>

So you can see that, when extend act on katrina i,e an object already instantiated it add greet method of Welcome as instance method to it(remember it add module method as class method to person). Basically, extend can add a module method as class Method or instance method depending on who called it at the runtime. You can check that , we have extended SeeOff as the class method above, so it will not available to katrina or deepika who are basically a instance of Celebrity class.

katrina.bye
NoMethodError: undefined method `bye’ for #<Celebrity:0xe29b964>

Now say, you want to introduce yourself to any celebrity, thus the introduce method must be available to all its instance. In this case we have to use include.

2.2.1 :116 >Celebrity.include Introduction

2.2.1 :116 > salman = Celebrity.new
=> #<Celebrity:0xe26d848>
2.2.1 :117 > salman.introduce
Iam Arun

2.2.1 :118 >katrina.introduce
Iam Arun

Notice that, Introduction module is mixed to Celebrity class after creation of katrina object, but its introduce method also get available to it, this is because in ruby methods are accessed by reference, any object just contain its own variable and refer for any method in its class, so if some method is added later on then also it will be accessible to a object.

3 => Just like all classes are instances of Ruby’s Class, all modules in Ruby are instances of Module.Module is the superclass of Class, so this means that all classes are also modules, and can be used as such.

You can easily check it by calling ancestors method on Person class

2.2.1 :068 > Person.ancestors
=> [Introduction, Person, Host, Welcome, Object, PP::ObjectMixin, Delayed::MessageSending, ActiveSupport::Dependencies::Loadable, V8::Conversion::Object, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]
2.2.1 :069 >

You can note down below from the above result

  1. extend do not add the module to the ancestors chain
  2. prepend add the module as child of the class in which it is called , see that Person become ancestor of Introduction
  3. include add the ancestors in the reverse order of their call. Host include in last, so it become first ancestor of Person

As you know we can override any method in ancestors class in its child class, and also use super to keep behaviour of ancestor intact and at the same time adding our own behaviour. Let rewrite our initial code to explain this.

module Introduction
  def introduce
    puts "Iam Arun"
    super
  end
end

module Welcome
  def greet
    puts "Hi..How are you"
  end
end

module Host
  def serve_food
    puts "chiken biryani"
  end
end

module SeeOff
  def bye
    puts "Byee..."
  end
end

class Person
  prepend Introduction
  include Welcome
  include Host
  extend  SeeOff
  
  def serve_food
    super
    puts "try lassi"
  end
  
  def introduce
    puts "Happy to meet you"
  end
end

 

Now you can get below result when call different methods of the Person class.

2.2.1 :110 > person = Person.new
=> #<Person:0xb4cd9d58>

2.2.1 :111 > person.serve_food
chiken biryani
try lassi

2.2.1 :112 > person.introduce
Iam Arun
Happy to meet you

And finally, let’s see an alternative way of extending a class. I do not use it myself as I believe in keeping the things simpler. But if you go through, gems and different library, you can see the use of self.included(base) .

Basically included is a callback , which get triggered whenever a module is included into another module or class. The argument base passed to it is basically the target module or class i,e that in which the module is included. Don’t get confused as initially I was. See the example below:

module Welcome
  def self.included(base)
    base.extend SeeOff
  end
  
  def greet
    puts "Hi..How are you"
  end
end

module SeeOff
  def bye
    puts "Byee..."
  end
end

class Person
  include Welcome
end

Note that, In Person class, we have only included the Welcome module, but in the Welcome module we have defined included method within which we have extended the SeeOff class with the line base.extend SeeOff , since Welcome module is included in Person class, when included callback get fired, Person will be become base class for it.So base.extend SeeOff will evaluated as Person.extend SeeOff .

You can check the result in console now:

2.2.1 :037 > Person.bye
Byee…

So you can see that bye method of SeeOff class get added as class method to Person class.

That’s all about mixin in ruby.. 🙂


2 Comments

Rails vs React : porting view in React

React is js framework to manage dynamic user interface . If you are a Rails Developer, you have implemented it numerous time with partial refresh.

Consider a simple scenario where a User comment on a picture or video or slide. You need to show the newly added comment in the comments list without reloading the page.

With rails you do it with partial refresh of the comment list.

It works well and you will be happy till you have tried React. Few of the Problem with Rails partial refresh is as below:

  1. You need to redraw the partial DOM, every time it get refreshed
  2. You need to reinitialize all the needed js on the partial Refresh

If data to render is large, User can easily see the Flicker on page while the partial is getting refreshed.

It give bad user experience. You can easily avoid it by using React. Infact you can completely replace Rails view with React view while using Rails for other great benefit it provides for web development.

Here, I will not going to explain React in detail, but just show you the changes you need to do or adopt to migrate from Rails view to React view. I will first show you the Rails implementation of dynamic listing of comments and then tell you the changes you need to do to make it work in React. If you already know about partial refresh in Rails, you can skip it and go directly to React Implementation

Rails Implementation : Partial Refresh of Dynamic component of View

View : (wordpress not supporting ruby view tag, so I have add the view code as image)
# slide detail page(app/views/slides/show.html.erb)

slide_show

This is slide show page(only relevant part is shown), rendering the partial list comments at top, followed by a input filled for writing comment and ‘Add’ button to submit the comment. At bottom we have a form with some hidden inputs.When user click on add button we populate the comment field of this form and submit it through js.

Below is the partial listing the comments
#list_comments partial(app/views/slides/_list_comments.html.erb)

list_comment_partial

A important thing in this partial is use of highlight_mentioned_user helper for displaying comment. Remember when you mention a user name in FB comment it get highlighted. Same is achieved here with the helper method, which basically highlight the name of user mentioned in the comment. Since this method can be used anywhere, I have placed it in application helper

#app/Helper/application.rb

module ApplicationHelper

 def highlight_mentioned_user(mentioner, content)
     mentioned_users = mentioner.mentionees(User)
     mentioned_users.each do |user|
     first_name = user.first_name
     linked_name = '<a href="javascript:void(0)" class="user_highlight">' + first_name + '</a>'
     content.gsub!(first_name, linked_name)
   end
   content
 end

end

Now let us see the js code which handle submit of comment to controller
#app/assets/javascript/slide.js

function add_comment() {
 if ($("#slide_comment").val() === '') {
   alert("Comment can't be blank");
   return false;
 }
 else {
   var mentioned_user = $('#slide_comment').mentionsInput('getMentions');
   var mentioned_user_ids = [];
   $.each(mentioned_user, function () {
     mentioned_user_ids.push(this.uid);
   });
   $("#slide_comment_mentioned_user").val(mentioned_user_ids.join(","));
   $("#comment").val($("#slide_comment").val());
   //mix panel event for Comment Added
   create_mix_panel_event("comment_added");

   //mix panel event for @mention Used in Comment
   if(mentioned_user_ids.length > 0) {
     create_mix_panel_event("@mention_used_in_comment");
   }
 
   $('#form_slide_add_comment').trigger('submit.rails');
   }
};

So we have add_comment() function in js, which is binded to Add button. When user click this button, it do basic validations , other needed stuff and submit the form.

On submit of form, data passed to controller action which look as below.

# app/controllers/slides_controller.rb

class SlidesController < ApplicationController
  def add_comment
   @slide = Slide.find(params[:slide_id])
   @user_who_commented = current_user
   comment = Comment.build_from(@slide, @user_who_commented.id, params[:comment])
   mentioned_user_ids = params[:slide_comment_mentioned_user].split(',').reject(&:empty?)
   if comment.save
     slide_comment_mention_notification(params[:id], @slide, comment, @user_who_commented, mentioned_user_ids)
   end
  end
end 

Now once control reach the controller action, since it is ajax call i,e request is of type XHR, Rails will render add_comment.js.erb file . In this file we will refresh the partial. It look as below

# app/views/slides/add_comment.js.erb

$('#slide_comments_swap').html('<%= escape_javascript(render partial: "slides/list_comments" ,locals: {slide: @slide} ) %>');
$('#slide_comment').val('');
$("#slide_comment").mentionsInput('clear');

Here we have refreshed the partial and cleared the input field , thus completing the process.

PORTING RAILS VIEW IN REACT :

Let start with React. Use this link to configure Reactt with your Rails App .

We are going to replace rails partial refresh with React, so we do not need the partial  _list_comments.html.erb anymore. Delete the _list_comment.html.erb partial .

The slide show page now look as below :

slide_show_react
You can see that, our view code reduced to one react component. Also it look familiar with the way we use partial in Rail , SlideComments is the name of the React Component(equivalent to _list_comments.html.erb of rails) . We have then passed the required data for the component.

One thing to note here that React accept data as Array of Hash. When you are using rails, you have a no of way to get your data in view itself, But within React component there is no rail code. For example you have passed comments collection to list_comments partial and within that you get the name of user who has commented with comment.user.full_name . But this is not possible with the data passed to React.

In Rails view we are showing each comment with the detail : commenter pic, commenter full_name, comment date and comment text. We need all these in React view also, So you can see that I have created react_slide_comments helper to which I am passing the comments collection, which then return the needed data. The Helper code look as below :

module SlidesHelper
  include ApplicationHelper
  
  # create needed detail to display in react's component
  def react_slide_comments(comments)
    react_comments = []
    comments.each do |comment|
      react_comments << react_slide_comment(comment)
    end
    react_comments
  end
  
  # get data of individual comment
  def react_slide_comment(comment)
    {
      id: comment.id,
      commenter_name: comment.user.full_name,
      commenter_img: comment.user.picture.expiring_url(60, :thumb),
      comment_date: comment.created_at.strftime('%B %d, %Y'),
      comment_text: highlight_mentioned_user(comment, comment.body)
    }
  end
  
end

So at this point, we have all the required data earlier used in the Rails view. In the slide Helper we have included the Application Helper at the top as it contain the method highlight_mentioned_user used for highlighting users mentioned in a comment .

NOTE : you need to create exactly the same DOM structure as you are using in Rails. The only difference is that you will not write as HTML but as React node in JS.

I suggest everyone to use Coffee Script to write the React code, as there is two many lines of code and become cumbersome if writing in pure js.

First we will create the component SlideComments . For this create the coffee file slide_comments.js.coffee in components folder

# app/assets/javascript/components/slide_comments.js.coffee

@SlideComments = React.createClass
  getInitialState: ->
    comments: @props.comments
    project_scoped: @props.project_scoped
    project_id: @props.project_id
    slide_id: @props.slide_id
    
  getDefaultProps: ->
    comments: []
    
  addSlideComment: (response) ->
    if response.comment_saved
      comments = React.addons.update(@state.comments, { $push: [response.comment] })
      @setState comments: comments
    
  render: ->
    React.DOM.div
      className: 'slide_comments_swap'
      React.DOM.ul
        className: 'comments'
        for comment in @state.comments
          React.createElement SlideComment, key: comment.id, comment: comment
      React.createElement SlideCommentForm, project_id: @state.project_id, slide_id: @state.slide_id,  handleNewRecord: @addSlideComment    

In render part you can see that, we are creating same DOM structure as in HTML. For each comment we have created SlideComment component displaying detail of each comment.

# app/assets/javascript/components/slide_comments.js.coffee

@SlideComment = React.createClass
  rawMarkup: ->
    { __html: @props.comment.comment_text, sanitize: true }
    
  render: ->
    React.DOM.li
      className: 'slide_comment'
      React.DOM.img
        className: 'img-circle avatar pull-left'
        width: '30'
        src: @props.comment.commenter_img
      React.DOM.div
        className: 'comment-info'
        React.DOM.h3
          className: 'commenter_name'
          @props.comment.commenter_name
        React.DOM.em
          className: 'comment_date'
          @props.comment.comment_date
        React.DOM.p
          className: 'comment_text'
          dangerouslySetInnerHTML: @rawMarkup()

If you see closely, we have created the same HTML structure as the deleted _slide_comments.html.erb partial.An important thing to note here that we have used dangerouslySetInnerHTML of React in place of raw of rails. Both basically render raw html.

We also have a input box with Add button to add comment in the original Rails view. It get handled in SlideCommentForm component.

# app/assets/javascript/components/slide_comment_form.js.coffee

@SlideCommentForm = React.createClass
  getInitialState: ->
    comment_body: ''
    
  handleKeyDown: (e)->
    if e.key == 'Enter'
      @handleCommentAdd()
      e.preventDefault()
  
  handleCommentAdd: ->
    if $('#slide_comment').val() == ''
      alert 'Comment can\'t be blank'
    else
      mentioned_user = $('#slide_comment').mentionsInput('getMentions')
      mentioned_user_ids = []
      $.each mentioned_user, ->
        mentioned_user_ids.push @uid
      $.ajax
        method: 'post'
        url: "/projects/#{@props.project_id}/slides/#{@props.slide_id}/add_comment"
        dataType: 'JSON'
        data:
          comment: $('#slide_comment').val()
          slide_comment_mentioned_user: mentioned_user_ids.join(',')
        success: (response) =>
          #trigger event for comments count update
          postal.publish
            channel: 'slide'
            topic: 'comment.add'
            data:
              comments_count: response.comments_count
          #mix panel event for Comment Added
          create_mix_panel_event 'comment_added'
          #mix panel event for @mention Used in Comment
          if mentioned_user_ids.length > 0
            create_mix_panel_event '@mention_used_in_comment'
          @props.handleNewRecord response
        complete: ->
          $('#slide_comment').val('');
          $("#slide_comment").mentionsInput('clear');
     
  render: ->
    React.DOM.div
      className: 'input-group'
      React.DOM.input
        id: 'slide_comment'
        className: 'form-control'
        type: 'text'
        placeholder: 'Add a Comment'
        name: 'comment_body'
        onKeyPress: @handleKeyDown
      React.DOM.div
        className: 'input-group-btn'
        React.DOM.button
          id: 'add_slide_comment'
          className: 'btn btn-info'
          onClick: @handleCommentAdd
          'Add'

Here you can see that we have bind onClick event to handleCommentAdd method, and place all the js logic in it like giving alert if no comment is typed and finally making the ajax call to submit the comment to server.

Delete the js code in slide.js as it is ported to React .

When user add a comment it will go to controller through ajax call . The controller code now look as below, we need to add a single line to existing controller code

  def add_comment
    @slide = Slide.find(params[:slide_id])
    @user_who_commented = current_user
    comment = Comment.build_from(@slide, @user_who_commented.id, params[:comment])
    mentioned_user_ids = params[:slide_comment_mentioned_user].split(',').reject(&:empty?)
    if comment.save
      slide_comment_mention_notification(params[:id], @slide, comment, @user_who_commented, mentioned_user_ids)
      comment.create_activity key: 'project.slide_commented', owner: current_user, recipient: comment.commentable, project_id: params[:id]
    end
    render json: {comment_saved: comment.persisted?, comment: react_slide_comment(comment)}
  end

The bolded line is the only line we need to change in controller, as it now need to return the newly added comment which get appended to DOM by React.

Earlier when we are doing it Rails way, we are handling the DOM refresh in add_comment.js.erb file. It is not needed anymore. Delete the add_comment.js.erb file

You are done with porting your dynamic view from Rails to React.You can summarize the steps in below line

  1. Replace Rails partial with React Component
  2. Pass needed data collection as Array of Hash
  3. Move the js code(like handling click of add button ) within React
  4. Delete add_comment.js.erb file handling partial refresh in Rails

That’s all … all your css, js, model, controller etc code will not need any change.

My aim here is to just explain the amount of work needed to move from Rails to React. I will soon explain each line of React code in detail in some other Blog.

Reference:

https://facebook.github.io/react/

https://facebook.github.io/react/docs/component-specs.html

https://facebook.github.io/react/docs/tags-and-attributes.html