RailsCasts Pro episodes are now free!

Learn more or hide this

Bardwin's Profile

GitHub User: abatko

Comments by Bardwin

Avatar

A common misconception is that a ||= b is equivalent to a = a || b, but it actually behaves like a || a = b.

Avatar

Good point (re: remember me from multiple machines).

Avatar

GitHub stopped building gems in October of 2009. They suggest RubyGems.org.

See RubyGems' docs on Distributing Gems

bash
-bash> gem push mygem-1.2.3.gem 
Enter your RubyGems.org credentials.
Don't have an account yet? Create one at http://rubygems.org/sign_up
   Email:   ^CERROR:  Interrupted

Create an account: http://rubygems.org/sign_up

If you want to use gem commands from the command line, you'll need a ~/.gem/credentials file, which you can generate using the following command:

bash
-bash> curl -u bart https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials
Enter host password for user 'bart':
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0    56    0    56    0     0     53      0 --:--:--  0:00:01 --:--:--   495

Try again:

bash
-bash> gem push mygem-1.2.3.gem 
Pushing gem to https://rubygems.org...
Successfully registered gem: mygem (1.2.3)

Note that you can only push a single gem version once; if you update your gem in any way, and want to update rubygems.org, you'll have to increment the version number, build the gem again, git add new/changed files, remove the old version from git tracking, and then push the new version.

Avatar

On the other hand there is an argument for putting the protection into the model rather than controller:

Protecting attributes from within controllers sounds like a setup to have multiple points of fault. If I have more than one controller making saves/updates on the same model (ignoring a debate over if that is good design), then I have to remember to protect attributes in multiple controllers. If I handle mass assignment from the model, there's a single, definitive barrier between submitted params and persistence, irrespective of where that data originated. -- @Eleo

@Eleo exactly. That's what I tried to say [to] dhh in my gist. Controller has nothing to do with unintended params. It doesnt filter. It handles, manages, controls business logic.
IMO You should write your controllers way described below:
1) have all critical fields protected/updateable - accessible
2) control saving new record in controller using role checks, dependencies and abilities. Not just by filtering :user_id
3) Never simplify business logic for "update". People got role checks in "create" but trying to manage just "update_attributes" in edit and that's why this has happened. If you check role - check it on create and on update.

Don't be lazy. sic! -- @homakov

See: Mass assignment vulnerability - how to force dev. define attr_accesible?

Avatar

Heads-up! New approach!

DHH advises against putting mass-assignment protection into the model:

The basic idea is to move mass-assignment protection out of the model and into the controller where it belongs.

The whole point of the controller is to control the flow between user and application, including authentication, authorization, and as part of that access control.

This new approach is an extraction of the slice pattern and we're calling the plugin for it strong_parameters (already available as a gem as well).

See: http://weblog.rubyonrails.org/2012/3/21/strong-parameters/

p.s. Here's the blog of the Russian programmer, Egor Homakov, who cleverly brought this vulnerability to everyone's attention. BTW, the rails team supposedly ignored him ("guys in rails issues ingored me and my issue"). So, kudos to Egor!

Avatar

Thanks! In my case I did something similar to the following:

locals: { show_price: defined?(show_price) ? show_price : false }

Avatar

In case anyone wants the coffeescript. Note that I kept the .submit bit in addition to .keyup in case the user hits submit.

products.js.coffee
jQuery ->
  # Ajax sorting and pagination on click
  $('#products td.sortable a, #products .pagination a').live('click', ->
    $.getScript(this.href)
    false
  )
  # Ajax search on submit
  $('#products_search').submit( ->
    $.get(this.action, $(this).serialize(), null, 'script')
    false
  )
  # Ajax search on keyup
  $('#products_search input').keyup( ->
    $.get($("#products_search").attr("action"), $("#products_search").serialize(), null, 'script')
    false
  )
Avatar

I decided against the Endless Page feature, and instead slightly adapted this RailsCast's code to provide a "Show more results" link, which would append results in much the same manner, but instead by click rather than reached-bottom-of-page detection.

The main change is the addition of the prepend line in products.js.coffee:

products.js.coffee
jQuery ->
  if $('.pagination').length
    $('#append_and_paginate').prepend('<a id="append_more_results" href="javascript:void(0);">Show more products</a>');
    $('#append_more_results').click ->
      url = $('.pagination .next_page').attr('href')
      if url
        $('.pagination').text('Fetching more products...')
        $.getScript(url)

Regarding the view, I only added a surrounding div:

products/index.html.erb
<div id="append_and_paginate">
  <%= will_paginate @products %>
</div>

And in products/index.js.erb I just changed the remove line to $('#append_and_paginate').remove(); so that both the "Show more results" link and pagination is removed when no more results are available.

Of course some css had to be added to make things look nice.

But anyway, the cool thing about this approach is that the "Show more products" link is only added by Javascript (hence only if Javascript is present and enabled). The click handler is bound to the DOM element with id="append_more_results", which is the id of the link tag that is prepended.

Avatar

I have a respond_to block in the index action of my controller, so I had to add format.js to render the RJS template associated with this action (i.e. /app/views/products/index.js.erb):

products_controller.rb
  def index
    @products = Product.order("name").page(params[:page]).per_page(10)

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @products }
      format.js
    end
  end
Avatar

Is the following change justifiable?

from:

faqs_controller.rb
def sort
  params[:faq].each_with_index do |id, index|
    Faq.update_all({position: index+1}, {id: id})
  end
  render nothing: true
end

to:

faqs_controller.rb
def sort
  Faq.transaction do
    Faq.connection.execute("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE")
    params[:faq].each_with_index do |id, index|
      Faq.update_all({position: index+1}, {id: id})
    end
  end
  render nothing: true
end

Ref: http://rhnh.net/2010/06/30/acts-as-list-will-break-in-production

Avatar

@Alexander Zubkov, thanks a lot for posting your module! I tried it out and it works! My preference though, is not for max+1 but max of a particular set +1. So I used your module as inspiration, but ended up doing this manually as follows.

edition_content.rb
class EditionContent < ActiveRecord::Base
  before_save :set_position

  protected

  def set_position
    self.position ||= 1 + (EditionContent.where('edition_id=?',edition_id).maximum(:position) || 0)
  end
end

This works just as I want, but I wonder if there's a way to make this into a module! You'd have to be able to tell the callback method (set_position) which column (in this case "edition_id") to use in the where clause. I'm too new to RoR to be able to figure this out. Any suggestions?

Avatar

Let's say you wanted to add sort order to the results in my comment above, then you could do this:

models/project.rb
def self.search(search)
  if search
    where('name LIKE ?', "%#{search}%").order('created_at DESC')
  else
    order('created_at DESC') # note: default is all, just sorted
  end
end

And then you could refactor like this:

models/project.rb
def self.search(search)
  arel = order('created_at DESC') # note: default is all, just sorted
  arel = arel.where('name LIKE ?', "%#{search}%").order('created_at DESC') if search.present?
  arel
end

And if you also wanted to search description you might do this:

models/project.rb
def self.search(search)
  arel = order('created_at DESC') # note: default is all, just sorted
  arel = arel.where('name LIKE ? OR description LIKE ?', "%#{search}%", "%#{search}%").order('created_at DESC') if search.present?
  arel
end

Next, change your controller to pass the whole params rather than just params[:search]):

projects_controller.rb
def index
  @projects = Project.search(params)
end

Then update your search method accordingly:

models/project.rb
def self.search(params)
  arel = order('created_at DESC') # note: default is all, just sorted
  arel = arel.where('name LIKE ? OR description LIKE ?', "%#{params[:search]}%", "%#{params[:search]}%").order('created_at DESC') if params[:search].present?
  arel
end

Why do all this? Because if you had more fields in your search view, you could easily search them too! For instance, let's say you change the above view to also include min price and max price:

projects/index.html.erb
<% form_tag projects_path, :method => 'get' do %>
  <p>
    <%= text_field_tag :search, params[:search] %>
    <%= text_field_tag :min_price, params[:min_price] %>
    <%= text_field_tag :max_price, params[:max_price] %>
    <%= submit_tag "Search", :name => nil %>
  </p>
<% end %>

Then in the search method, you do:

models/project.rb
def self.search(params)
  arel = order('created_at DESC') # note: default is all, just sorted
  arel = arel.where('name LIKE ? OR description LIKE ?', "%#{params[:search]}%", "%#{params[:search]}%").order('created_at DESC') if params[:search].present?
  arel = arel.where('price >= ?', params[:min_price]) if params[:min_price].present?
  arel = arel.where('price <= ?', params[:max_price]) if params[:max_price].present?
  arel
end

And for style you could change your view to use search_field_tag, size, and placeholder:

projects/index.html.erb
<% form_tag projects_path, :method => 'get' do %>
  <p>
    <%= search_field_tag :search, params[:search],  size: 20, placeholder: 'keywords' %>
    <%= search_field_tag :min_price, params[:min_price], size: 12, placeholder: 'min. price' %>
    <%= search_field_tag :max_price, params[:max_price], size: 12, placeholder: 'max. price' %>
    <%= submit_tag "Search", :name => nil %>
  </p>
<% end %>

I hope this helps someone. For more info, see: http://railscasts.com/episodes/111-advanced-search-form-revised

Avatar

Please copy/paste the controller's index action, and the model's search method.

Avatar

For Rails 3 compatibility, the ActiveRecord queries in the search method should look like this:

models/project.rb
  def self.search(search)
    if search
      where('name LIKE ?', "%#{search}%")
    else
      all
    end
  end

See http://railscasts.com/episodes/202-active-record-queries-in-rails-3 and http://m.onkey.org/active-record-query-interface

Avatar

This is due to a Rails 3 change:

Helpers like form_for or div_for that insert content from a block use <%= now

See "7.4.2 Helpers with Blocks" in http://edgeguides.rubyonrails.org/3_0_release_notes.html

Avatar

Refer to "ranked model" in @Jean's comment above

Avatar

In case anyone gets stumped by task.save(false), as of rails 3 use task.save(validate: false).

See 3.0 release notes: save(false) is deprecated, in favor of save(:validate => false) and the API.

The related error you may see is

NoMethodError (undefined method `[]' for false:FalseClass)