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.

Thursday, June 16, 2011

Factory Girl steps for Cucumber with MongoMapper: what about associations?

Update:

I tried to propagate the fix to Plucky itself, in vain. So far, it's not fixed. Use this patch, if you experience problems migrating your project from ActiveRecord to MongoMapper or MongoMapper problems not properly working with queries, even though the article is about FactoryGirl associations (which might've been changed in FactoryGirl itself by now).

I also came across hidden issues with MongoMapper, such as a method not working, but the test is designed in a way to pass. If it's my decision, I will try MongoId next time.

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

If you have an association (an Article belonging to a User) and you write something like that in Cucumber:

Given the following article exists:
| title | user |
| My article | email: writer1@example.com |

then the Factory Girl steps will handle this association for you, creating both an Article and a User for your test if they are of the ActiveRecord class. But this will throw an exception with MongoMapper.

Digging into the Factory Girl steps code explains why. Factory Girl steps use User.find(:first, :conditions => attributes) but syntax such as find(:first...) doesn't exist for MongoMapper.

MongoMapper uses Plucky gem for all the find methods. I wrote the following monkey patch to account for the missing attributes:

/config/initializers/plucky_ext.rb
----------------------------------

Plucky::Query.class_eval do
alias_method :find_old, :find
def find_new(*args)
options = args.extract_options!
first = args.shift
args.push(options)

case first
when :first
first(*args)
when :last
last(*args)
when :all
all(*args)
else
args.unshift(first) unless first.nil?
find_old(*args)
end
end
alias_method :find, :find_new
end
Now the Factory Girl steps work with associations

Saturday, June 11, 2011

MongoMapper with rspec/shoulda hack

As far as I know, MongoMapper is currently not able to handle association tests for rspec/shoulda, such as most common belongs_to, has_many, has_one. I think they are working on it, but it's not there yet.

I wrote a hack to work with MongoMapper, so far it's able to handle my simple belongs_to association, and my rspec test passes all right when it has the association and fails otherwise. As I progress through more associations, I will keep posting if it needs improvements.

In the config/initializers folder create a file mongo_mapper_ext.rb and place the following code into it:

module MongoMapperExt
module Plugins
module Associations
module ClassMethods
def reflect_on_association(association)
associations[association].extend AssociationMethods if associations[association]
end
end
module AssociationMethods
def macro
@macro = derive_macro
end
def primary_key_name
@primary_key_name ||= options[:foreign_key] || derive_primary_key_name
end
def belongs_to?
@macro == :belongs_to
end

private
def derive_primary_key_name
if belongs_to?
"#{name}_id"
elsif options[:as]
"#{options[:as]}_id"
else
klass.foreign_key
end
end

def derive_macro
case self.class.name
when "MongoMapper::Plugins::Associations::BelongsToAssociation"
:belongs_to
when "MongoMapper::Plugins::Associations::ManyAssociation"
:has_many
when "MongoMapper::Plugins::Associations::OneAssociation"
:has_one
else
"unknown association"
end
end
end
end
end
end

MongoMapper::Document.send(:include, MongoMapperExt::Plugins::Associations)


Basically, it adds some methods from the ActiveRecord reflections which are missing in MongoMapper. Why not, if MongoMapper classes already contain suitable options to work with.

Friday, March 25, 2011

Struggling with FireFox SSL security - Root and Intermediate SSL certificates for nginx

If you installed a reliable SSL certificate for your site, and your pages look fine in IE and Chrome, you still may see a message in Firefox that your site is untrusted instead of your page. In this case, check if you installed the root and intermediate certificates, not just the site certificate. Those certificate should've come in the package from your SSL provider.

I had 1 root and 2 intermediate certificates from my provider, however I couldn't find any information how to install them for nginx. Nginx configuration doesn't have any options for those, compared to Apache server. I tried different ways, and eventually I succeeded.

Copy all your intermediate certificates and the root certificate into the same file. I copied them BACKWARDS - i.e. if you have 2 intermediate certificates, copy the second one first, then the other one below it, then the root certificate below it. You don't have to save this file. Open your .crt file with the site certificate and add the content of the previously created file below the site certificate. Save it and restart nginx. That's it.

Monday, March 7, 2011

Load testing RoR Applications with JMeter

RoR version 2.3.5, JMeter version 2.4

There are a few specifics using JMeter with RoR applications.

1.Handling forms
We have to take into account that forms are submitted using authenticity_token which is generated randomly with every form. The solution is to catch the authenticity token from the form page using a regular expression extractor, save it into a JMeter variable and send it with the form.

This article provides a detailed explanation how to do it. I only used \s+ instead of empty spaces to make it work.

2. We have to pay attention how the authenticity_token field is represented on the page in order to parse it right. You can look up this field if you enter your HTTP request for the page with this form into JMeter and open a View Results Tree listener (Add -> Listener -> View Results Tree). Run your test, click on the request and open the Response data tab in text format. Perform search in it for "authenticity_token" (you might have several on the page, if you have several forms).

For example, the field in my regular forms looked like:
<input name="authenticity_token" type="hidden" value="[actual value]" />

correspondingly, my regular expression for JMeter was:
<input\s+name="authenticity_token"\s+type="hidden"\s+value="(.*?)"\s*\/>


I also have a few AJAX forms that are shown on demand and stored as Javascript variables. In those cases I had all the quotes slash escaped, and the field looked like:
<input name=\"authenticity_token\" type=\"hidden\" value=\"[actual value]\" />

so my regular expression became (the escape slashes get escaped in their turn):
<input\s+name=\\"authenticity_token\\"\s+type=\\"hidden\\"\s+value=\\"(.*?)\\"\s*\/>


3. Handling AJAX requests
I figured out how to pass parameters into AJAX requests by running those requests in FireFox and watching what parameters are passed. For example, I had a few requests with "update" method, and Rails routing defines it as performed over "PUT". However, you have to enter it as a "POST" method, add a parameter "_method" to the request parameters and assign "put" to it. Bizarre?

Also, with the Rails routing and remote forms you have just a regular path, no .js in the end. I was able to see this path when running FireBug. In this case you need to add the right headers to the request by running "Add -> Config Element -> HTTP Header Manager". I added the following headers to my requests handling remote forms:
Accept = text/javascript, text/html, application/xml, text/xml, */
X-Requested-With = XMLHttpRequest
Content-Type = application/x-www-form-urlencoded; charset=UTF-8


4. Getting object ids
Sometimes after creating an object we need this object's id assigned by Rails in order to access this object (it might be needed in a request path etc.). We can extract this object id from a response page if it contains links with this id, using Regular Expression Extractor. The regular expression will depend on a particular case.

5. We can assign Regular Expression Extractor results to variables and use them in other Regular Expression Extractors. Unfortunately, JMeter has a bug when it's not able to read a variable value when it's used in a Regular Expression Extractor for AJAX responses (if the response is regular, it's fine). So, don't struggle for no reason, it's not going to work! Use something else. I tried to pass a variable for an object id to a regular expression when parsing an AJAX response and ended up just using \d+ . I luckily had a unique tag for this expression.

6. Uploading Images
JMeter has a problem with Rails and multipart forms, that's why it doesn't work if there is a form with an image field in it. Rails doesn't set content-type for text fields of multipart forms, so JMeter sets it up by default and as a result the text field content doesn't come right and the submission fails. I had to hack JMeter code according to this post and then building it from source. To build it from source, run the following commands in the JMeter folder:
ant download_jars
ant package

After rebuilding JMeter, it worked fine to upload images.

7. Number of Matches for Regular Expression Extractor
Sometimes we need to see if some parameter is on the page, sometimes we need to make sure that the number of occurrences of this parameter is within certain limits. In this case we need to figure out the number of matches instead of a match itself. The number of matches is represented by the regular expression VARNAME_matchNr. The group number should be set to -1. Note, they we can get EITHER a match number, or a certain match itself, but not both.
To check if there is a form on the page (i.e. there is an authenticity token), I used the following parameters in the Regular Expression Extractor:
Reference Name = TEST
Regular Expression = (<input\s+name="authenticity_token"\s+type="hidden"\s+value="(.*?)"\s*\/>)
Template = $1$
Match No. = -1
Default Value = null

Then the number of matches is TEST_matchNr. I couldn't get it working without a default value.

8. JMeter seems to have trouble using VARNAME_matchNr in the If Controller, when I wanted to check if there is a form on the page. It simply didn't see it. I had to create another variable to make it working. I used another Regular Expression Extractor to get the value of TEST_matchNr, where I specified 'Apply To - JMeter Variable - TEXT_matchNr. The other parameters were the following:
Reference Name = TEST_NUM
Regular Expression = (\d+)
Template = $1$
Match No. = 1

Then I used the variable TEST_NUM in the If Controller:
Condition = ${TEST_NUM} > 0