Archive

Posts Tagged ‘nesting’

jquery pure templates

November 1st, 2010 2 comments

This story will be about jquery pure templates.

During developing new project I found great library Pure by beebole and I was going to write about it in first place. but with time when using it I found more and more issues with it. My first idea was to fix it, and I started doing that, but in the middle of debugging I found out it’s not that easy, 20KB of sources is hard to change.

So I have decided to write my own code, using very similar idea but different approach. My library is still pure, does not require putting fancy markers into html in manner to fill it with data, it is enough to write code which can be matched by jquery. My first try was with putting selectors directly into data description like:

data = {'a#my':"my url",'a#my@href':"#/url"}

After I got proof of concept in only 3 hours of coding I thought it would be good to allow mapping any keys to a map of selectors, so it is no more required to return selectors as keys of JSON:

data2 = {'name':"my url",'link':"#/url"}

using simple map file:

map2 = {'name':'a#my','link':'a#my@href'}

So how to call it ? Simple :) just include lib in the header:

<script src="jquery-pure-template.js" type="text/javascript" 
        charset="utf-8"></script>

And call render on jquery matching elements:

$('.user').render(data);

or using map file:

$('.user').render(data2,map2);

Thats all needs this html:

<div class="user"><a href="#"></a></div>

and finally you will get as output this html:

<div class="user"><a href="#/url">my url</a></div>

This was simple example, but it will also work with arrays, and nested arrays too, just give it a chance, and keep in mind don’t force html to change the flow, try to make data keep the flow, not view.

Ofcourse there might be certain problems with performance with the chosen approach, first of all performance. Arrays over 500 elements tend to perform quite bad, but using nesting arrays can hold up to 5000 elements without big problem on decent computer. Additionally putting id’s into collection should additionally give some 20% speed up in finding elements by jquery.

I know there are faster solutions, that can probably render lots of data, but mine is simple, only 100 lines now (70 in first version), this makes it extremely easy to maintain and change, hawing less code is better then more :)

This post is also available in Polish version.
Software described here is a plugin for jquery.

Deep associations in rails activerecord

May 22nd, 2010 Comments off

Some time ago I wrote about complex associations, now time to add another method and corrections.

First the finder_by_sql, in that particular case It was necessary to add

:readonly => true

So the code looks now like this:

  has_many :roles, 
    :readonly => true, 
    :finder_sql =&gt; '
SELECT roles.name FROM roles
INNER JOIN responsibilities ON roles.id = responsibilities.role_id
INNER JOIN assigments ON responsibilities.group_id = assigments.group_id
WHERE assigments.user_id = #{id}
GROUP BY roles.id
  '

There is one realy big downside of using finder_sql – it does not work with find_by_… or named scopes, so this forced me to continue searching and this is the result:

  def roles
    Role.scoped(
     {
       :joins => { :responsibilities => { :group => { :assigments => :user } } },
       :conditions => {"users.id" => id},
       :group => "roles.id"
     }
   )
  end

and now I can write:

user.roles.by_name(:admin).count

where by_name is an named scope

  named_scope :by_name, lambda { |type| {:conditions => [ "roles.name = ", type.to_s ] } }

complex associations in rails activerecord

January 30th, 2010 1 comment

During playing with CanCan from ryanb show in last railscasts I did planned some models and find me in quite strange situation – I knew the connection, but I could not easily get the result.

My models look something like:

class User < ActiveRecord::Base
  has_many :assigments
  has_many :groups, :through => :assigments
end
class Assigment < ActiveRecord::Base
  belongs_to :user
  belongs_to :group
end
class Group < ActiveRecord::Base
  has_many :assigments
  has_many :users, :through => :assigments
  has_many :responsibilities
  has_many :roles, :through => :responsibilities
end
class Responsibility < ActiveRecord::Base
  belongs_to :role
  belongs_to :group
end
class Role < ActiveRecord::Base
  has_many :responsibilities
  has_many :groups, :through => :responsibilities
end

So my first try was to get roles through groups manually:

User.first.groups.map{|g| 
  g.roles
}.flatten.map{|r|
  r.name.to_sym
}.uniq

But this solution is waste of resources, for system with a lot of possible roles it might be very inefficient, it takes first all groups of user and then ittereting through them gets all it’s roles. it will generate a lot of database queries.
So after some searching I got following code:

User.first.roles.map{|r|r.name.to_sym}.uniq

To make it working I used one quite nice feature of Active record – :finder_sql – so in user.rb I have added following line:

has_many :roles, :finder_sql => 'SELECT r.* FROM users u LEFT JOIN assigments a ON u.id=a.user_id LEFT JOIN responsibilities res ON a.group_id=res.group_id LEFT JOIN roles r ON res.role_id=r.id'

This makes it possible to get all the roles for a user just in one call, everything calculated on database side. Unfortunately there is one down side of this – the SQL code might be not portable to other databases, so use it only when You are sure You will stick to one database.
There is also other way around, it allows to get the same data in two sql calls, without using SQL queries, just on pure ActiveRecord usage:

Responsibility.find_all_by_group_id(
  u.assigments(:select=>'assigments.group_id').map{|a|a.group_id},
  :select=>'roles.name',
  :joins=>:role
).map{|r| r.name.to_sym }.uniq

There are many ways to archive the same goal, knowing them is only an part – knowing how they work, makes us aware how to chose the path.

on 2009-01-31 13:15 added:
Another way going out of the Group class:

Group.find(
  :all,
  :select=>'roles.name',
  :joins=>[:users,:roles],
  :conditions=>{:users=>{:id=>1}}
).map{|g|g.name.to_sym}.uniq

Did You liked this post or maybe not, vote on it at dzone.

Rails auto_complete nested list

June 9th, 2009 1 comment

Yesterday I was implementing auto completion for categories. The idea was to get possibility to write by hand categories but keep them as has_many_through association in database.

After six hours of coding I got this working – that is why I’m against rails, with such big amount of plugins, gems and blogs you have to spent a lot of time to do small things.

This code works with rails 2.3

So first we have to install auto_complete plugin which was quite handy:

script/plugin install auto_complete
script/plugin discover

Second we need to define migration:

  create_table :book_categories do |t|
    t.references :books
    t.references :categories
  end
  create_table :books do |t|
    t.string :title
  end
  create_table :categories do |t|
    t.string :name
  end

At third step we define models:

class Book < ActiveRecord::Base
  has_many :book_categories, :dependent => :destroy
  has_many :categories, :through => :book_categories
end
class BookCategory < ActiveRecord::Base
  belongs_to :book
  belongs_to :category
end
class Category < ActiveRecord::Base
  has_many :book_categories, :dependent => :destroy
  has_many :books, :through => :book_categories
end

Time for forth step – faking Book properties:

class Book < ActiveRecord::Base
has_many :book_categories, :dependent => :destroy
has_many :categories, :through => :book_categories
def categories_list
self.categories.map{|c| c.name}*”, ”
end

def categories_list=(list)
list_names = list.split(‘,’).map{ |e| e.strip }.uniq
list_existings = Category.find(:all, :conditions => [ ‘name IN (?)’, list_names ], :select => ‘id,name’ )
list_existings_names = list_existings.map { |e| e.name }
list_add = list_names-list_existings_names
list_new = list_existings.map { |e| e.id }
list_old = self.categories.map { |e| e.id }.uniq
list_add.each do |name|
self.categories << Category.new(:name => name)
end
self.categories << Category.find(:all, :conditions => [ ‘id IN (?)’, list_new-list_old ], :select => ‘id’ )
self.categories.delete Category.find(:all, :conditions => [ ‘id IN (?)’, list_old-list_new ], :select => ‘id’ )
end
end

Now we can have some rest from coding and play a bit with new models using “script/console”:

>> Category.new(:name => 'a').save
>> Category.new(:name => 'b').save
>> Category.new(:name => 'c').save
>> Category.all.map{|c| [c.id,c.name] }
=> [[1, "a"], [2. "b"], [3, "c"]]
>> b = Book.new(:title=>'some book')
>> b.categories_list='a,b,c'
>> b.categories
=> [#<Category id: 1>, #<Category id: 2>, #<Category id: 3>]
>> b.categories_list='a,c,d'
>> b.categories
=> [#<Category id: 1>, #<Category id: 3>, #<Category id: nil, name: "d">]
>> b.save
>> Category.all.map{|c| [c.id,c.name] }
=> [[1, "a"], [2. "b"], [3, "c"], [4, "d"]]

Fifth step is to write edit/new view:

<% form_for(@book) do |f| %>
  <div>
    <%= f.label :title, t(:book_label_title) %><br />
    <%= f.text_field :title %>
  </div>
  <div>
    <%= f.label :categories_list, t(:book_label_category) %><br />
    <%= text_field_with_auto_complete :book, :categories_list, {}, { :method => :get } %>
  </div>
<% end %>

Six step is to define action in controller:

class BooksController < ApplicationController
  def auto_complete_for_book_categories_list
    list = params['book']['categories_list'].split(',').map{ |e| e.strip }

    name = list.pop

    find_options = {
      :conditions => [ "name LIKE ?", '%' + name + '%' ],
      :order => "name ASC",
      :limit => 10,
      :select => 'id, name',
    }

    @items = Category.find(:all, find_options).select { |e| !list.include?(e.name) }.
      map { |e| list.push(e.name); e.name=(list*', '); list.pop(); e }

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
  #other actions go here ...
end

The seventh and last step is to add routing:

map.resources :books, :collection => { :auto_complete_for_book_categories_list => :get }

Hope that this will help someone.

Categories: Development Tags: , ,