Monday, September 17, 2012

"cmtypes[st] is undefined" and other quick (and dirty) fixes for jqGrid working with a treetable

This is jqGrid version 4.4.0

1. If you tried to create a tree table, and it didn't work giving an error "cmtypes[st] is undefined" on line 1513 of jquery.jqGrid.src.js:


$(ts).jqGrid("SortTree", st, ts.p.sortorder, cmtypes[st].stype, cmtypes[st].srcfmt);

change this line to the following:

$(ts).jqGrid("SortTree", st, ts.p.sortorder, sorttype, srcformat);

It is working after that.

2. Expanding tree nodes with dynamically loaded local data.

If you create a tree grid with local data, you would notice that even if you don't expand nodes, it still creates hidden rows with this data. This causes a problem of unnecessarily slow loading if you have a lot of data but only want to expand tree nodes on demand, when a user clicks on a node.

First of all, don't load data for hidden rows initially. Second, override the click event. This event in the grid wasn't assigned by the "live" method, but when creating the rows, so we need to change it at the time when loading the data:


    grid[0].addJSONData({
           ...
    });
            
    $("div.treeclick").off("click").on("click", onExpand);

The onExpand function will be the following:

var onExpand = function(e) {
    var target = e.target || e.srcElement;
    var rowid = $(target).closest("tr").attr("id");
    var record = grid.getRowData(rowid);
    if(record.isLeaf == 'false' || !record.isLeaf) {
        if(record.expanded == 'true' || record.expanded == true) {
            grid.jqGrid("collapseNode", record);
            grid.jqGrid("collapseRow", record);
            record['expanded'] = false;
            grid.jqGrid("setRowData", rowid, record);
        } else {
            if(grid.jqGrid("getNodeChildren", record).length > 0) {
                grid.jqGrid("expandNode", record);
                grid.jqGrid("expandRow", record);
                $("tr[id^='"+rowid+"_']").show();
            } else {
                expand(rowid);
            }
            record['expanded'] = true;
            grid.jqGrid("setRowData", rowid, record);
        }
    }
    return false;
}

"expand" is your function to get new data to expand the node.

Please, note that we manually change the grid row to collapsed and expanded as, unfortunately, jqGrid stores node data in 2 separate places, so the functionality is not very consistent.

When we initially build a node, we don't load it as a leaf because we're going to expand it(the icon wouldn't be a leaf). Unfortunately, jqGrid would go into an infinite clicking cycle (would trigger an event) if we don't make this node a leaf. So, make it temporary a leaf when expanding it! The function (a hack, as jgGrid stores data in 2 different places) is the following:

    //hack: set the parent as a leaf to prevent triggering another click
    function setLeaf(id, isleaf) {
        var record = grid.getRowData(id);
        var _index = grid.jqGrid("getGridParam", "_index");
        var data = grid.jqGrid("getGridParam", "data");
        data[_index[id]]["isLeaf"] = isleaf;
    }

Then setting the node data will look the following:

function setNode(rowid, gridData) {    
    //hack: set the parent as a leaf to prevent triggering another click
    function setLeaf(id, isleaf) {
        var record = grid.getRowData(id);
        var _index = grid.jqGrid("getGridParam", "_index");
        var data = grid.jqGrid("getGridParam", "data");
        data[_index[id]]["isLeaf"] = isleaf;
    }
        
    setLeaf(rowid, true);
    
    gridData.forEach(function(node) {
        var notleaf = !node['isLeaf'] || (node['isLeaf'] == 'false');
        grid.jqGrid("addChildNode", node["id"], node["parent"], node);
        setIcon(node["parent"], true);
        var record = grid.getRowData(node["id"]);
        if(notleaf) {
            record['isLeaf'] = false;
            record['expanded'] = false;
            grid.setRowData(node["id"], record);
            setIcon(node["id"], false, onExpand);
        }
    });
    return false;
}

The last piece will be getting the icons and handlers rights with all the messy business with the leaves-nonleaves. My own settings in jqGrid definition for the tree icons are: triangles for a parent, and no icon for a leaf.

treeIcons: {plus:'ui-icon-triangle-1-e', minus:'ui-icon-triangle-1-s', leaf:'ui-icon-none'}


I had to add my own css for no icon (in the css file):

.ui-icon-none {
    background-position: -250px -250px; 
}

So my function to get the icons right is the following:
//hack to get the icons right
function setIcon(id, expanded, clickhandler) {
    var _index = grid.jqGrid("getGridParam", "_index");
    var data = grid.jqGrid("getGridParam", "data");
    var $icon = $("tr[id='"+id+"']").find("div.ui-icon");
    if(expanded) {    
        $icon.removeClass("ui-icon-triangle-1-e");
        $icon.removeClass("tree-plus");
        $icon.addClass("ui-icon-triangle-1-s");
        $icon.addClass("tree-minus");
    } else {
        $icon.removeClass("ui-icon-triangle-1-s");
        $icon.removeClass("tree-minus");
        $icon.addClass("ui-icon-triangle-1-e");
        $icon.addClass("tree-plus");
    }
    if(clickhandler != undefined) {
        $icon.on("click", clickhandler);
    }
}

Hopefully, that's it for now!

Friday, June 22, 2012

A gem to create objects from external APIs

I published a gem to load into objects XML and JSON files provided over external APIs (urls). The gem is called api_object and available to be installed over "gem install api_object".

The gem is very easy to use.

  1. subclass your object to be loaded from ActiveApi::ApiObject
    class Station < ActiveApi::ApiObject 
    end 

  2. Specify the url to load the data from, optionally an action and a mode, an api key and parameters(options) for the url; such as the url would look like
    "http://<api_url>/<action>?&key=<api_key>&<parameter1=value1&parameter2=value2...>"

    This will be defined in the top object over the function "initialize_from_api". Options for this function:

    :url - specify url
    
    :action - specify action
    
    :mode - specify mode (such as 'verbose', 'terse' etc.)
    
    :key - api key
    
    :data_tags - specify tag parameters under which object data might be stored, for example <location value='San Francisco'> - "value" would be a data tag. :data_tags accepts a single value or an array. 
    
    :url_options - parameters
    

    The following is designed to generate real time departure estimates for BART stations:

    class Station
    
    initialize_from_api :url => "http://api.bart.gov/api/", :action => 'etd.aspx', :key => 'MW9S-E7SL-26DU-VV8V', :url_options => {:cmd =&gt; 'etd'}
    
    end
    

    In this example, the url generated to get real time departure estimates from the Richmond station will be: http://api.bart.gov/api/etd.aspx?cmd=etd&orig=RICH&key=MW9S-E7SL-26DU-VV8V

  3. Define class attributes and mapping of the attributes to the api where the api name is different. To define api simple type mappings, use "api_column <attribute name>, <api attribute name>". To define api association mapping, use "api_association <association attribute name>, <api attribute name>, :as => <association class name>". Either the second, or the third parameters could be omitted. If the third parameter is omitted, the class name will be the same as the attribute name.

    In the following example, a simple attribute name is "abbreviation", but the name defined in the api XML documents is "abbr". An association is defined in the attribute :est, the api mapping is :etd and it's an object of the class Estimate.

    class Station < ActiveApi::ApiObject 
    
    initialize_from_api :url => "http://api.bart.gov/api/", :action => 'etd.aspx', :key => 'MW9S-E7SL-26DU-VV8V', :url_options => {:cmd => 'etd'}
    
    attr_reader :name, :abbreviation, :date, :time, :est
    
    api_column :abbreviation, :abbr
    api_association :est, :etd, :as => Estimate
    
    end
    

  4. To load api data into an object, use the class method "get_results(options)". In the example, get real time estimates for the station Glen Park.

    data = Station.get_results(:orig => 'GLEN')
    

    Please, note that data loaded might be either a hash, or an array of hashes, depending on the api.

  5. Create an object from the data

    If the example, the data received is a hash, so create an object.

    station = Station.new(data)
    

    If the data is an array of hashes, then it might be used to create an array of objects

    stations = data.map {|d| Station.new(d)}
    

  6. The gem also allows to get location based data by ip. Please, refer to the documentation.

Saturday, March 31, 2012

Initialize Objects from a hierarchy of hashes (JSON format) in Ruby

There are some nice little functions to initialize Objects in Ruby from a hash. A good example could be found here.

But sometimes, when we read an external API (XML or JSON) and parse the result, we get data which is a set of nested hashes and arrays. Then we want to initialize an object in our application with this data. I wrote the following module to do that.

init_from_hash.rb
-----------------------------
module InitFromHash
  
  def initialize(*args)
    args.first.each do |k, v| 
      unless defined?(k).nil?    # check if it's included as a reader attribute
        result = v.instance_of?(Array) ? v.inject([]) {|arr, v1| arr << init_object(v1, k)} : init_object(v, k)
        instance_variable_set("@#{k}", result)
      end
    end if (args.length == 1 && args.first.is_a?(Hash)) 
  end
  
  def init_object value, klass
     value.instance_of?(Hash) ? (get_module_name + klass.to_s.capitalize).constantize.new(value) : value
  end
  
  def get_module_name
    (self.class.name =~ /^(.+::).+$/) ? $1 : ''
  end
    
end


Assumptions:
1. Our object also contains nested objects. For example, we have an author who has articles, and each article has comments.
2. Names of our classes match the keys in our data.
3. All the classes of objects to be initialized have the same module name.
4. We want only attributes specified in the reader_attr to be present

Example of usage:
Module Blog
  class Author
      include InitFromHash
      attr_reader :name, :article
  end
end

module Blog
  class Article
      include InitFromHash
      attr_reader :name, :abstract, :comment
  end
end

module Blog
  class Comment
      include InitFromHash
      attr_reader :text
  end
end


When we receive our data in the form:
data = {"author" => 'Mike', "article" => [{"name" => "Is there life on Mars",
"abstract" => "Probably", "comment" => [{"text" => "Really?"},
{"text" => "No way!"}]}, {"name" => "Milk is good for you", 
"abstract" => "Scientists discovered", "comment" => [{"text" => "Really?"},
{"text" => "No way!"}] }}


Then we can initialize our instance as simple as
author = Author.new(data)

Tuesday, February 28, 2012

Starting with Node.js - short introduction with resources

What is Node.js? It's a Javascript library run on the V8 engine, the same one used in Google Chrome; and it's used to create a server on a fly to run web apps.

Advantages of it:
- it's slim and fast
- it's event driven

1. First question I asked was where Node.js is useful.

Since Javascript is event driven, it's useful when a server action should be triggered on a user action. If we consider just a request/response model, then the response it returned to this particular user only. But sometimes we want updates for all the users, such as in a chat room or in an interactive game. Then we can take advantage of the Node.js event driven nature.

2. Installation

Follow those instructions to install Node.

3. Eclipse plugin for Node.js development

I'm an ex Java developer so I like using Eclipse for everything. VJET plugin can be downloaded from here . It's a plugin to develop Javascript projects. Don't forget to install the Node.js and other Javascript type libraries you might need.

I didn't find a way to start a server in Eclipse (is Javascript still considered front end only?), so just open a terminal, go to your workspace directory, then to your project and start your server from there.

4. Trying it out

There is a great beginner book to start with Node. Please, click and read the links provided in the text, they give extended information on the methodology.

5. Trying it out more

Build a simple chat using Node.js

Tuesday, November 8, 2011

Fancybox gallery for an element with a tooltip: why is the first title not showing?

Please, refer to this article as a reference how to create fancy tooltips for elements and to the Fancybox site to see how to configure it to display images and galleries.

The problem with the mentioned approach to create tooltips is that the "title" attribute gets reassigned and blanked out when we mouse over the element, but it could be used by other plugins such as Fancybox. Fancybox uses the "title" attribute to add a title to a zoomed in element. If this element also has a tooltip, then clicking on the element will result in the slide title not showing, since we didn't exit the element yet, and the "title" attribute value didn't get assigned back. If we have a gallery, then the titles are showing fine for the next elements since we didn't have the mouse over them.

We can fix this problem in a very simple way, force the mouse exit event once we click on the element. The "title" attribute is assigned back as the mouse leaves the element, as this way the problem will be fixed.

Fancybox has an onStart attribute where we can call this function.

(function ($) {

$(function() {

function closeTooltip() {
$(".slide-content:visible a").trigger("mouseleave");
}
$(".slide-content a").fancybox({
'transitionIn':'elastic',
'transitionOut':'none',
'speedIn':600,
'speedOut':200,
'overlayShow':true,
'overlayColor':'#000',
'cyclic':true,
'easingIn':'easeInOutExpo',
'margin':'10px',
'onStart':function() {closeTooltip();}
});
});
}) (jQuery);


"slide-content" is a wrapper for the slide (can be a different class name) and "a" is the link for the tooltip and the Fancybox element. Now the "title" attribute will get reassigned and the slide title will be showing fine.

Monday, November 7, 2011

Using Cloud Zoom with a slide show: why the lens doesn't work properly

I was trying to use the slideshow plugin from JQuery Tools with the Cloud Zoom plugin. In addition to having a slide show, I wanted to zoom in pictures in the slide show with Cloud Zoom. Please, refer to the links provided how to create a slideshow and configure Cloud Zoom for images.

But the lens didn't work for the slideshow, I was only able to catch a microscopic lens 2x2px in the top left corner of the image.

The reason why it works this way is that Cloud Zoom creates a wrapper and a "mousetrap" elements for an image after the image is loaded, with the size of the "mousetrap" corresponding to the image size. In my case, initially all the images in the slideshow are hidden ("display:none"), so the images sizes are 0, so the "mousetrap" element is created only 2x2px big.

To fix it, we need to activate Cloud Zoom for an image only when the corresponding slide is shown. To disable Cloud Zoom initially, replace 'class="cloud-zoom"' with 'class="_cloud-zoom"'.

We don't have any "show" events in Javascript, but luckily, we know that the slideshow uses the "show()" method to make a slide visible and we can override this method in JQuery in order to trigger an event.

//trigger an event on show
var _oldShow = $j.fn.show;
$j.fn.show = function() {
var hidden = this.find(":hidden").add(this.filter(":hidden"));
var result = _oldShow.apply(this, arguments);
hidden.filter(":visible").each(function(){
$j(this).triggerHandler("show"); //No bubbling
});
return result;
};


Add this function to the page and it will trigger an event when show() is called on an element or its children. I borrowed it from this discussion, other solutions can be also used.

We also need to catch this event when a slide is shown and activate Cloud Zoom for the image.

(function ($) {
function activateCloudZoom(elem) {
elem.attr("class", "cloud-zoom");
elem.CloudZoom();
elem.css("position", "static").css("display", "inline");

$j(function() {
//slideshow activation
$j(".slidetabs").tabs(".slides > div", {

// enable "cross-fading" effect
effect: 'fade',
fadeOutSpeed: "slow",

// start from the beginning after the last tab
rotate: true

// use the slideshow plugin. It accepts its own configuration
}).slideshow();

//Cloud Zoom activation
$(".slide-content").bind("show", function() {
activateCloudZoom($(this).find("._cloud-zoom"));
});
});
})(jQuery);


We change the image class name to "cloud-zoom" and attach the handler to it (it's normally done automatically on the document loading, but we didn't have that). I also changed some display parameters as the parameters set by Cloud Zoom interfere with the slide text. The "show" event is bound to the slide container on the document loading (it might have a different class name).

That's it! Now the lens will be showing normally corresponding to its configuration parameters.

Wednesday, July 6, 2011

Single table inheritance and testing with Cucumber and Factory Girl

To get familiar with Single Table Inheritance, please, refer to this article

Let's assume, we have a web site requiring authentication and we have a User class that allows every user to log in, created with the gems Clearance and Suspenders such as described here, with a user name field added.


Now, let's have 2 types of users with different functionality besides logging in: Company and Customer. Then we can have 2 classes inheriting from User.





class User < ActiveRecord::Base   

  include Clearance::User

end  



class Customer < User 

end  



class Company < User 

end 


In order to make the tables inherit, let's add a "type" column to the "users" table.

$ rails generate migration add_type_to_users type:string


The 2 types of users will be processed by the same controller UsersController and will be routed with the same paths.

users_controller_spec.rb

-----------------------------

require 'spec_helper'



describe UsersController, 'routes' do

it { should route(:get, 'users/1').to(:action => 'show', :id => 1) }

end


and the corresponding mapping in routes.rb:
resources :users, :controller => 'users', :only => [:show, :create]


To test the models, we need to create separate factories for different user types. We will use FactoryGirl and FactoryGirl inheritance.

spec/factories/user.rb

--------------------------

Factory.sequence :email do |n|

 "user#{n}@example.com"

end



Factory.define :user do |user|

 user.email            { Factory.next :email }

 user.name             { "user" }

 user.password         { "password" }

 user.password_confirmation { |instance| instance.password }

end



Factory.define :email_confirmed_user, :parent => :user do |user|

 user.after_build { warn "[DEPRECATION] The :email_confirmed_user factory is deprecated, please use the :user factory instead." }

end



#add those 2 definitions



Factory.define :company, :class => Company, :parent => :email_confirmed_user do |user|

   user.type   { "Company" }

end



Factory.define :customer, :class => Customer, :parent => :email_confirmed_user do |user|

   user.type             { "Customer" }

end


After that, we would like to write some Cucumber features to test a user creating a profile of a certain type. The features were already created by Clearance, we only need to put checks for the extra attributes "name" and "user type".

Feature: Sign up



 In order to get access to protected sections of the site

 As a visitor

 I want to sign up



 Background:

   When I go to the sign up page

   Then I should see an email field



 Scenario: Visitor signs up with invalid data

   When I fill in "Email" with "invalidemail"

   And I select "company" from "User type"

   And I fill in "User name" with "user"

   And I fill in "Password" with "password"

   And I fill in "Confirm password" with ""

   And I press "Sign up"

   Then I should see error messages





 Scenario: Visitor signs up with valid data

   When I fill in "Email" with "email@person.com"

   And I select "company" from "User type"

   And I fill in "User name" with "user"

   And I fill in "Password" with "password"

   And I fill in "Confirm password" with "password"

   And I press "Sign up"

   Then I should be on the "company" profile page for "user"

   And I should see "signed up"



 Scenario: Visitor tries to sign up without a user name

   When I go to the sign up page

   And I fill in "Email" with "email@person.com"

   And I select "company" from "User type"

   And I fill in "Password" with "password"

   And I fill in "Confirm password" with "password"

   And I press "Sign up"

   Then the "User name" field should have the "can't be blank" error




We make the first 2 scenarios pass by adding "type" and "user name" fields to the form, adding the "user name" to the model and the validation, creating a "show" view for "user", and defining the missing path. I leave all of it as an exercise. We will also need to override the create method for Clearance, since it doesn't handle User subtypes.

In UsersController add the following (it overrides the Clearance method)

 #overriden from Clearance to take care of inheritance

 def create

  @user = params[:user][:type].constantize.new params[:user]

  #used to be

  #@user = ::User.new params[:user]

   if @user.save

     flash_notice_after_create

     sign_in(@user)

     redirect_to(url_after_create)

   else

     render :template => 'users/new'

   end

 end


However, the third scenario still will have trouble passing since for some reason Rails refers to the inherited model when returning to the same page, instead of User. To fix this, we need to override the model_name method in User, so it returns the superclass model name.

class User < ActiveRecord::Base

 include Clearance::User



 validates_presence_of :name



 def self.inherited(child)

   child.instance_eval do

     def model_name

       self.superclass.model_name

     end

   end

   super

 end

end




Now, all the Cucumber tests should pass.