question on table-plus column sorting

59 views
Skip to first unread message

Montgomery Kosma

unread,
Jun 5, 2009, 9:57:27 PM6/5/09
to Hobo Users
I have table-plus working very well finally. However, one thing I've
found bugs me, and either I'm missing something or the call to
apply_scopes is violating DRY at some level.

My controller is as follows:

def index
@projects = Project.apply_scopes(:search => [params
[:search], :name], :order_by => parse_sort_param
(:name, :start_date, :target_date, :completion_date, :status, ...))
hobo_index @projects
end

I find that unless a field is explicitly specified in
parse_sort_param, the column heading shows up on the page as an active
link, but clicking on it does nothing (except to refresh the page and
return it to "default" sort order).

Seems like parse_sort_param ought to take something like an :all
parameter, to enable sorting by any of the columns that are displayed.

Is there a standard way to access the list of columns by which to
sort?

I was thinking one solution may be to make list of columns an
attribute of the model or the controller. The thing is, I'd need to
access it from my controller.rb for the apply_scopes call, and from
the index.dryml file for the table-plus "fields" argument. For that
matter, I'd probably want the same list accessible by my model, since
that's where I define fields for my call to acts_as_csv_exportable ...

thoughts?

Tom Locke

unread,
Jun 7, 2009, 4:18:10 AM6/7/09
to hobo...@googlegroups.com
> I was thinking one solution may be to make list of columns an
> attribute of the model or the controller.

Sounds like a reasonable idea. On the controller I think

Tom

hobo_hippy

unread,
Jun 23, 2009, 12:06:21 PM6/23/09
to Hobo Users
"I find that unless a field is explicitly specified in
parse_sort_param, the column heading shows up on the page as an active
link, but clicking on it does nothing (except to refresh the page and
return it to "default" sort order). "

I'm trying to get the sort to work as well, but neither the sort or
search work at all (the error message won't even show up when I search
something clearly not in the table). All that happens is a page
refresh when I click one of the column titles in hopes of sorting by
that column. I'm trying to create sortable table for my timesheets at
the timesheets index page. I pretty much followed the agility tutorial
to the letter, and the def index below is my best guess. even though i
specifically listed inside parse_sort_param, I'm not getting the
functionality of sort or search at all. Any words of wisdom?


def index
@timesheet = find_instance
@timesheets = Timesheet.apply_scopes(:search => [params
[:search], :title],
:order_by => parse_sort_param
(:client, :project, :user, :hours, :this, :created_at))
end


<index-page>
<collection: replace>
<div id="table-plus">
<table-plus with="&@timesheets" fields="client, project, user,
hours, this, created_at">
<empty-message:><h3>No timesheets match your criteria</h3></
empty-message:>
</table-plus>
</div>
</collection:>
</index-page>

hobo_hippy

unread,
Jun 23, 2009, 2:42:22 PM6/23/09
to Hobo Users
anyone? please?

Montgomery Kosma

unread,
Jun 23, 2009, 3:14:29 PM6/23/09
to Hobo Users
Hope this helps, here is an example from my working code, from my
Custodians class, which has both search and sort working.

I think your issue is in the controller. Applying to your situation,
I'd say (1) you need to add hobo_index and pass it @timesheets; and
(2) @timesheet is extraneous and doesn't really make sense on an index
page (since you're not working with a particular timesheet).

Here's mine (plus Special Added Bonus of an export-to-excel example).

class CustodiansController < ApplicationController
hobo_model_controller
auto_actions :all

def index
@custodians = Custodian.apply_scopes(:search => [params
[:search], :name], :order_by => parse_sort_param
(:name, :custodian_type, :last_name, :group, :location, :total_volume, :file_sets))
hobo_index @custodians
end

index_action :export


def export
@records = Custodian.all
if @records.count.zero?
flash[:notice] = "No #{self.model.name.pluralize} to export!"
elsif !@records.respond_to?(:to_csv)
flash[:notice] = "Export not supported for #
{self.model.name.pluralize}"
else
send_data @records.to_csv, :type => 'text/csv', :filename =>
self.model.name.pluralize + '.csv'
end
end

end


for completeness, here's my index.dryml. Only material difference
between mine and yours is that I've used the implicit scope rather
than specifying it using with=.

<index-page>
<after-top-page-nav:>
<div class="export-link">%>
<a href="/custodians/export">Export</a>
</div>
</after-top-page-nav:>

<collection: replace>
<table-plus fields="this, custodian_type, last_name, group,
location, total_volume, file_sets.count">
<empty-message:>No custodians have been created.</empty-
message:>
<file_sets_count-heading-link:>File Sets</file_sets_count-
heading-link:>
<file_sets_count-view: class="numeric-field"><%=
number_with_delimiter(this) %></file_sets_count-view:>
<count-view: class="numeric-field"><%= number_with_delimiter
(this) %></count-view:>
<total_volume-view: class="numeric-field"><%=
number_to_human_size(this) %></total_volume-view:>
</table-plus>
</collection:>
</index-page>


On Jun 23, 2:42 pm, hobo_hippy <87bee...@gmail.com> wrote:
> anyone? please?

hobo_hippy

unread,
Jun 23, 2009, 3:36:04 PM6/23/09
to Hobo Users
this stuff is extra, right? or was it necessary for this to work for
you?

hobo_hippy

unread,
Jun 23, 2009, 4:31:04 PM6/23/09
to Hobo Users
Thanks btw... still doesn't do anything though, and the export didn't
seem to work either (just gives me a 404 error). If I could get EITHER
of these two things to work, the sort OR the export, I'd be pretty
happy. When I click on a column title, the url adjusts appropriately
to end with sort=whatever_I_Just_Clicked but the table remains
unchanged. It is always sorted by created_at. what's going on?! Also,
I assume navigating to timesheets/export calls the method you defined
as export, so why do I get a 404 when I navigate there?

I'm still learning a lot, so thanks for helping me so far


<index-page>
<after-top-page-nav:>
<div class="export-link">%>
<a href="/timesheets/export">Export</a>
</div>
</after-top-page-nav:>

<collection: replace>
<div id="table-plus">
<table-plus fields="client, project, user, time, created_at, this">
<empty-message:><h3>No timesheets to display</h3></empty-
message:>
</table-plus>
</div>
</collection:>
</index-page>


class TimesheetsController < ApplicationController

hobo_model_controller

def index
@timesheets = Timesheet.apply_scopes(:search => [params
[:search], :title], :order_by => parse_sort_param
(:title, :time, :client, :project, :user))
hobo_index @timesheets
end

def export
@records = Timesheet.all
if @records.count.zero?
flash[:notice] = "No #{self.model.name.pluralize} to export!"
elsif !@records.respond_to?(:to_csv)
flash[:notice] = "Export not supported for #
{self.model.name.pluralize}"
else
send_data @records.to_csv, :type => 'text/csv', :filename =>
self.model.name.pluralize + '.csv'
end
end

auto_actions :all
index_action :export

end

hobo_hippy

unread,
Jun 23, 2009, 4:42:45 PM6/23/09
to Hobo Users
this is what my output looks like (and it won't change!!)

Updated At Client Project User Time Timesheet
June 23, 2009 19:54 Arther Test coding admin 54 tsheet1
June 23, 2009 19:55 Billy Test graphics admin 20 banner work
June 23, 2009 20:15 Arther Test coding bob 332 bo sheep
June 23, 2009 20:23 Arther Test coding admin 34 more from the
admin


the search button doesn't work either (even after fixing search param
to name since name is the string in fields do and title doesn't exist)

Matt Jones

unread,
Jun 23, 2009, 4:42:59 PM6/23/09
to hobo...@googlegroups.com
I'd recommend putting the auto_action / index_action stuff at the top
(after hobo_model_controller) rather than the bottom.

--Matt Jones

Montgomery Kosma

unread,
Jun 23, 2009, 5:16:15 PM6/23/09
to Hobo Users
yes, this stuff changes the column headings and performs calculations
for a couple of my columns. Note that I still can't get the
calculated columns to sort, because the calculations are not happening
in SQL.

Montgomery Kosma

unread,
Jun 23, 2009, 5:25:21 PM6/23/09
to Hobo Users
Since you've looked at the view and the controller, I'm starting to
suspect something in your model. I'm happy to take a look at yours,
and I've attached my Custodian model below in case you want to look
through it. .

As for csv export, my apologies, I should have mentioned you'll need a
couple other pieces. First, you need the rails plugin
acts_as_csvable. Second, you need to specify the export formats in
your model. See the acts_as_csv_exportable lines in my model below:

class Custodian < ActiveRecord::Base
hobo_model # Don't put anything above this

Custodian_type = HoboFields::EnumString.for
(:employee, :contractor, :group, :other)
Status = HoboFields::EnumString.for(:active, :inactive)

fields do
name :string, :unique
custodian_type Custodian_type
last_name :string
job_title :string
network_login :string
status Status
email :email_address
hire_date :date
hire_date_estimated :boolean
departure_date :date
departure_date_estimated :boolean
notes :text
timestamps
end

# custodian properties support through other tables
has_many :alternate_names, :dependent => :destroy, :accessible =>
true

belongs_to :location
belongs_to :group

# associations with other entities
has_many :project_custodians, :dependent => :destroy
has_many :projects, :through => :project_custodians

has_many :file_sets, :dependent => :destroy, :accessible => true

# --- Default sorting --- #
set_default_order "last_name, name"

# --- Import / Export --- #
acts_as_csv_exportable :default,
[:name, :last_name, :network_login, :custodian_type, :status, :project_list, :file_set_count, :total_megabytes, :total_items]
acts_as_csv_exportable :complete,
[:name, :custodian_type, :last_name, :alternate_names_list, :job_title, :network_login, :status, :email, :hire_date, :departure_date, :notes, :project_list, :file_set_count, :total_megabytes, :total_items]


def alternate_names_list
alternate_names.try.*.name.sort.join(', ') || ""
end

def project_list
projects.try.*.name.sort.join(', ') || ""
end

def file_set_count
file_sets.count
end

def total_megabytes
file_sets.try.*.size_megabytes.inject {|sum, elt| sum + (elt ||
0)} || 0
end

def total_volume
total_megabytes * 1024 * 1024
end

def total_items
file_sets.try.*.size_items.inject {|sum, elt| sum + (elt || 0)} ||
0
end

# --- Permissions --- #
include PermissionUser

# --- Validations --- #
validates_uniqueness_of :name, :allow_nil => false
validates_uniqueness_of :email, :allow_nil => true
validates_uniqueness_of :network_login, :allow_nil => true
validates_date :hire_date, :allow_nil => true, :before =>
[:departure_date, Proc.new {1.day.from_now.to_date}]
validates_date :departure_date, :allow_nil => true, :before =>
Proc.new {1.day.from_now.to_date}, :after => :hire_date

end

Montgomery Kosma

unread,
Jun 23, 2009, 5:32:03 PM6/23/09
to Hobo Users
A couple more thoughts.

1. Does your Timesheet class have a name field? It's not strictly
required but lots of hobo seems much happier if it is defined --
either as a field or by attaching :name => true to one of your other
fields (e.g., title in this case).

2. The most helpful tip Bryan offered me a while back was to jump into
the debugger to play around with things and see what's going on
internally. One thing you should try is to insert

require 'ruby-debug' ; debugger

into def index before the call to hobo_index. Take a look at the
value of @timesheets and see if it looks like what you would expect.
Once you're in the debugger, type

set autoeval

then you can eval ruby code at the command prompt. Try, e.g.,
@timesheets.count and @timesheets.*.name for starters.
>     elsif !...@records.respond_to?(:to_csv)

hobo_hippy

unread,
Jun 23, 2009, 5:36:26 PM6/23/09
to Hobo Users
that sounds like a good place to start messing with things - how do I
get to the debugger? does i use script/console somehow?

Montgomery Kosma

unread,
Jun 23, 2009, 6:52:17 PM6/23/09
to Hobo Users
it will run in the window where you are running your server ... so it
depends on your environment. I'm running windows, and usually have
mongrel running over port 3000 as a windows service. That means I
don't have a console window for debugging, so when I want to debug, I
launch a command prompt and run the server with

ruby script/server

kevinpfromnm

unread,
Jun 23, 2009, 6:56:52 PM6/23/09
to Hobo Users
Actually, Matt's suggestion of moving the auto actions to the top
might be quite relevant. Not sure how it handles it, but the auto
actions might override any customizations if the call is after the
other definitions.

hobo_hippy

unread,
Jun 24, 2009, 9:58:18 AM6/24/09
to Hobo Users
Sweet mamma jamma, Matt Jones, you rock!

Evidently it WAS as simply as moving auto_actions to the top. Works
perfectly now.

Montgomery Kosma,

I'm gonna work at getting the export to excel thing to work. That
would just make this sooo spiffy.

hobo_hippy

unread,
Jun 24, 2009, 10:19:26 AM6/24/09
to Hobo Users
So Montgomery Kosma,

def index
@timesheets = Timesheet.apply_scopes(:search => [params
[:search], :name], :order_by => parse_sort_param
(:name, :time, :client, :project, :user, :updated_at))
hobo_index @timesheets

I've got one little modification I'd like to make. the search only
looks at :name becuase that's stated here:

(:search => [params[:search], :name]

if I want to be able to search :name, :client, :project, and :user how
can I modify this? i tried just adding the fields, but that doesn't
work since client, project, and user are not @Timesheets, where I
already told it to look. Do I have to write another search method
inside index? How can I make it search across all models instead of
one model?

btw I get the following error message:

Mysql::Error: Unknown column 'timesheets.user' in 'where clause':
SELECT * FROM `timesheets` WHERE (((timesheets.name like '%CJ%') OR
(timesheets.user like '%CJ%') OR (timesheets.client like '%CJ%') OR
(timesheets.project like '%CJ%')))

/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
connection_adapters/abstract_adapter.rb:188:in `log'
/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
connection_adapters/mysql_adapter.rb:309:in `execute'
/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
connection_adapters/mysql_adapter.rb:563:in `select'
/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
connection_adapters/abstract/database_statements.rb:7:in
`select_all_without_query_cache'
/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
connection_adapters/abstract/query_cache.rb:60:in `select_all'
/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
connection_adapters/abstract/query_cache.rb:81:in `cache_sql'
/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
connection_adapters/abstract/query_cache.rb:60:in `select_all'
/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
base.rb:635:in `find_by_sql'
/usr/lib/ruby/gems/1.8/gems/hobo-0.8.7/lib/hobo/model.rb:293:in
`find_by_sql'
/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
base.rb:1490:in `find_every'
/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
base.rb:589:in `find'
/usr/lib/ruby/gems/1.8/gems/hobo-0.8.7/lib/hobo/model.rb:286:in `find'
/usr/lib/ruby/gems/1.8/gems/hobo-0.8.7/lib/hobo/scopes/
named_scope_extensions.rb:20:in `send'
/usr/lib/ruby/gems/1.8/gems/hobo-0.8.7/lib/hobo/scopes/
named_scope_extensions.rb:20:in `method_missing'
/usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/
base.rb:2003:in `with_scope'
(__DELEGATION__):2:in `__send__'
(__DELEGATION__):2:in `with_scope'
(__DELEGATION__):2:in `is_a?'
app/controllers/timesheets_controller.rb:9:in `index'

Montgomery Kosma

unread,
Jun 24, 2009, 12:40:40 PM6/24/09
to Hobo Users
Where I had a complex situation -- though perhaps less complex as what
you're after -- I successfully used named scopes instead of hobo's
apply_scopes. In my case I have a table-plus showing custodians on my
project page, and wanted to make the search box find custodians.

In my case, I added two named scopes to my join class
(ProjectCustodian):

named_scope :custodian_names_like, lambda { |search_for| (!
search_for.blank?) ? {:include => :custodian, :conditions =>
["custodians.name LIKE ?", "%#{search_for}%"] } : nil }

named_scope :sort_custodians_by, lambda { |sort_by|
if !sort_by.blank?
# fix custodian --> custodians
klass=sort_by.first.split(".").first
sort_by.first.gsub!(klass, klass.pluralize)
{:include => :custodian, :order => sort_by.join(' ')}
else
nil
end
}

and this is how I used them from my ProjectsController:

def edit
hobo_show do
@project = find_instance
order = parse_sort_param("custodian.name", "custodian.type",
"custodian.last_name")
@project_custodians =
@project.project_custodians.custodian_names_like(params
[:search]).sort_custodians_by(order)
end
end

Iain

unread,
Jul 1, 2009, 3:00:22 AM7/1/09
to Hobo Users
I don't know if this is too late, but I have a nice solution to the
duplication problem.

First, I replaced the default index-page tag with one with a table,
but configured so that the table is "opt-out" rather than "opt-
in" (eg. by default all model fields are shown)

<extend tag="index-page" attrs="skip">
<old-index-page merge>
<collection: replace>
<table-plus fields="*" skip="#{skip}" without-search-form>
<controls:/>
</table-plus>
</collection:>
</old-index-page>
</extend>

Here the key revelation was that you can use "*" to specify all
fields. Then, in the controller, I've made it accept all fields as
sort parameters:


include Hobo::RapidHelper

def index
hobo_index model.apply_scopes(:order_by => parse_sort_param
(*standard_fields(model)))
end


This uses the standard_fields method from RapidHelper on the model
field (that's inherited by all hobo controllers)

In this example I've turned off searching, because I haven't thought
of a good way of determining the correct field to search on
dynamically

I hope that helps!




On Jun 25, 2:40 am, Montgomery Kosma <mko...@gmail.com> wrote:
> Where I had a complex situation -- though perhaps less complex as what
> you're after -- I successfully used named scopes instead of hobo's
> apply_scopes.  In my case I have atable-plusshowing custodians on my
> project page, and wanted to make the search box find custodians.
>
> In my case, I added two named scopes to my join class
> (ProjectCustodian):
>
>   named_scope :custodian_names_like, lambda { |search_for| (!
> search_for.blank?) ? {:include => :custodian, :conditions =>
> ["custodians.name LIKE ?", "%#{search_for}%"] } : nil }
>
>   named_scope :sort_custodians_by, lambda { |sort_by|
>     if !sort_by.blank?
>       # fix custodian --> custodians
>       klass=sort_by.first.split(".").first
>       sort_by.first.gsub!(klass, klass.pluralize)
>       {:include => :custodian, :order=> sort_by.join(' ')}
>     else
>       nil
>     end
>   }
>
> and this is how I used them from my ProjectsController:
>
>   def edit
>     hobo_show do
>       @project = find_instance
>      order= parse_sort_param("custodian.name", "custodian.type",

Andy Orahood

unread,
Jul 1, 2009, 11:26:37 AM7/1/09
to Hobo Users
NICE! You've figured out some cool stuff. I would have loved to have
known about <table-plus fields="*"> a month ago!
And :order_by => parse_sort_param(*standard_fields(model))) rocks. You
should write this up as a recipe!

Thanks,
Andy
Reply all
Reply to author
Forward
0 new messages