Code Monkey home page Code Monkey logo

ranked-model's Introduction

ranked-model is a modern row sorting library built for Rails 4.2+. It uses ARel aggressively and is better optimized than most other libraries.

Build Status

ANNOUNCING: Positioning, the gem

As maintainer of both Acts As List and the Ranked Model gems, I've become intimately aquainted with the strengths and weaknesses of each. I ended up writing a small scale Rails Concern for positioning database rows for a recent project and it worked really well so I've decided to release it as a gem: Positioning

Positioning works similarly to Acts As List in that it maintains a sequential list of integer values as positions. It differs in that it encourages a unique constraints on the position column and supports multiple lists per database table. It borrows Ranked Model's concept of relative positioning. I encourage you to check it out and give it a whirl on your project!

Installation

ranked-model passes specs with Rails 4.2, 5.0, 5.1, 5.2, 6.0 and 6.1 for MySQL, Postgres, and SQLite on Ruby 2.4 through 3.0 (with exceptions, please check the CI setup for supported combinations), and jruby-9.1.17.0 where Rails supports the platform.

To install ranked-model, just add it to your Gemfile:

gem 'ranked-model'

# Or pin ranked-model to git
# gem 'ranked-model',
#   git: '[email protected]:mixonic/ranked-model.git'

Then use bundle install to update your Gemfile.lock.

Simple Use

Use of ranked-model is straight ahead. Get some ducks:

class Duck < ActiveRecord::Base
end

Give them an order (integer column):

rails g migration AddRowOrderToDucks row_order:integer
rails db:migrate

IMPORTANT: The _order table column MUST allow null values. For the reason behind this requirement see issue#167

Put your ducks in a row:

class Duck < ActiveRecord::Base

  include RankedModel
  ranks :row_order

end

Order the Ducks by this order:

Duck.rank(:row_order).all

The ranking integers stored in the row_order column will be big and spaced apart. When you implement a sorting UI, just update the resource by appending the column name with _position and indicating the desired position:

@duck.update row_order_position: 0  # or 1, 2, 37. :first, :last, :up and :down are also valid

IMPORTANT: Note that you MUST append _position to the column name when setting a new position on an instance. This is a fake column that can take relative as well as absolute index-based values for position.

Position numbers begin at zero. A position number greater than the number of records acts the same as :last. :up and :down move the record up/down the ladder by one step.

So using a normal json controller where @duck.attributes = params[:duck]; @duck.save, JS can look pretty elegant:

$.ajax({
  type: 'PUT',
  url: '/ducks',
  dataType: 'json',
  data: { duck: { row_order_position: 0 } },  // or whatever your new position is
});

If you need to find the rank of an item with respect to other ranked items, you can use the {column_name}_rank method on the model instance. {column_name} is your resource ranking column.

Following on from our examples above, the row_order_rank method will return the position of the duck object in the list with respect to the order defined by the row_order column.

Duck.rank(:row_order).first.row_order_rank # => 0
Duck.rank(:row_order).third.row_order_rank # => 2

Complex Use

The ranks method takes several arguments:

class Duck < ActiveRecord::Base

  include RankedModel

  ranks :row_order,                  # Name this ranker, used with rank()
    column: :sort_order              # Override the default column, which defaults to the name

  belongs_to :pond
  ranks :swimming_order,
    with_same: :pond_id              # Ducks belong_to Ponds, make the ranker scoped to one pond

  ranks :row_order,
    with_same: [:pond_id, :breed]    # Lets rank them by breed

  scope :walking, where(walking: true )
  ranks :walking_order,
    scope: :walking                  # Narrow this ranker to a scope

  belongs_to :parent, class_name: 'Duck', optional: true
  ranks :child_order,
    unless: :has_no_parent?,         # Rank only ducks that have a parent. Alternatively a Proc or lambda can be passed, e.g. proc { parent.nil? }
    with_same: :parent_id

  def has_no_parent?
    parent.nil?
  end
end

When you make a query, add the rank:

Duck.rank(:row_order)

Pond.first.ducks.rank(:swimming_order)

Duck.walking.rank(:walking)

Drawbacks

While ranked-model is performant when storing data, it might cause N+1s depending on how you write your code. Consider this snippet:

ducks = Duck.all
ducks.map do |duck|
  {
    id: duck.id,
    position: duck.row_order_rank # This causes N+1!
  }
end

Every call to duck.row_order_rank will make a call to the DB to check the rank of that particular element. If you have a long list of elements this might cause issues to your DB.

In order to avoid that, you can use the rank(:your_rank) scope and some Ruby code to get the element's position:

ducks = Duck.rank(:row_order).all
ducks.map.with_index do |duck, index|
  {
    id: duck.id,
    position: index
  }
end

Single Table Inheritance (STI)

ranked-model scopes your records' positions based on the class name of the object. If you have a STI type column set in your model, ranked-model will reference that class for positioning.

Consider this example:

class Vehicle < ActiveRecord::Base
  ranks :row_order
end

class Car < Vehicle
end

class Truck < Vehicle
end

car = Car.create!
truck = Truck.create!

car.row_order
=> 0
truck.row_order
=> 0

In this example, the row_order for both car and truck will be set to 0 because they have different class names (Car and Truck, respectively).

If you would like for both car and truck to be ranked together based on the base Vehicle class instead, use the class_name option:

class Vehicle < ActiveRecord::Base
  ranks :row_order, class_name: 'Vehicle'
end

class Car < Vehicle
end

class Truck < Vehicle
end

car = Car.create!
truck = Truck.create!

car.row_order
=> 0
truck.row_order
=> 4194304

Migrations for existing data

If you use ranked_model with existing data, the following migration (for Rails 6) can be a starting point. Make sure to declare include RankedModel and ranks :row_order in your Duck before running the migration.

rails g migration AddRowOrderToDucks row_order:integer

Then, adjust the migration:

# e.g. file db/migrate/20200325095038_add_row_order_to_ducks.rb
class AddRowOrderToDucks < ActiveRecord::Migration[6.0]
  def change
    add_column :ducks, :row_order, :integer

    # Newest Duck shall rank "highest"" (be last).
    Duck.update_all('row_order = EXTRACT(EPOCH FROM created_at)')

    # Alternatively, implement any other sorting default
    # Duck.order(created_at: :desc).each do |duck|
    #   duck.update!(row_order: duck.created_at.to_i + duck.age / 2)
    # end
  end
end

Internals

This library is written using ARel from the ground-up. This leaves the code much cleaner than many implementations. ranked-model is also optimized to write to the database as little as possible: ranks are stored as a number between -2147483648 and 2147483647 (the INT range in MySQL). When an item is given a new position, it assigns itself a rank number between two neighbors. This allows several movements of items before no digits are available between two neighbors. When this occurs, ranked-model will try to shift other records out of the way. If items can't be easily shifted anymore, it will rebalance the distribution of rank numbers across all members of the ranked group.

Contributing

Fork, clone, write a test, write some code, commit, push, send a pull request. Github FTW!

The code is published under the MIT License.

The specs can be run with sqlite, postgres, and mysql:

bundle
appraisal install
DB=postgresql bundle exec appraisal rake

If no DB is specified (sqlite, mysql, or postgresql), the tests run against sqlite.

RankedModel is mostly the handiwork of Matthew Beale:

A hearty thanks to these contributors:

ranked-model's People

Contributors

andrewradev avatar aried3r avatar bnauta avatar brendon avatar codinganarchy avatar dependabot[bot] avatar geeknees avatar jamesalmond avatar jaredbeck avatar jguyon avatar kakra avatar mixonic avatar mjc-gh avatar mjonuschat avatar mkalygin avatar mrcasals avatar nextmat avatar pehrlich avatar petergoldstein avatar pragyadaga avatar rcugut avatar reu avatar robotex82 avatar rociiu avatar ursm avatar vinbarnes avatar vincedevendra avatar vjt avatar walf443 avatar willnathan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ranked-model's Issues

scoping by multiple values

Hi, I am trying to get the following scope to work, but it isn't working and I have no clue how to implement this with ranked-model, any thoughts?

  scope :ranked_scope, lambda { |page| where(ancestry: page.ancestry, website_id: page.website_id) }
  ranks :position, scope: :ranked_scope

Touch the model when reassigning

Hi,

We are looking at using this gem and it looks like a great fit. We would like to be able to tell what models have changed as a result of the rebalancing. This is because we store the ordering column in a search index and don't want to have to update the entire list when a rebalance occurs.

Would you take a patch upstream that included a config option touch_record_when_rebalancing?

Thanks,
Adam

rank() doesn't order well

I have followed your tutorial but after the drag n drop and the update_attributes, if I refresh the page, nothing happens, even though, the row_order has been updated.

Model:

class EventParam < ActiveRecord::Base
  extend Enumerize
  include RankedModel

  enumerize :html_type, :in => [:text, :number, :date],
        :predicates => { :prefix => true }, :default => :text

  serialize :options, Hash
  ranks :row_order

  # Associations
  belongs_to :event

  # Validations
  validates :name, :html_type, :label, :presence => true
  validates :name, :uniqueness => { :case_sensitive => false, :scope => :event }

  # Callbacks
  before_validation :fix_name


  def get_field_for_locale(field, locale = I18n.locale)
    return eval(self[field])["#{locale}"]
  end

  def print_parameters
    return {:id => id, :name => name }
  end

  private

  def fix_name
    self.name = name.parameterize
  end
end

Sorting (in events_controller.rb):

def edit
  @params =   @event.event_params.rank(:row_order).page(params[:page]).per(::Rails.application.config.item_number)
end

It gives this:

#<ActiveRecord::AssociationRelation [
#<EventParam id: 32, name: "test-1", html_type: "text", required: false, default_value: "test 1", options: {}, position: 1, label: "{'fr' => 'test 1', 'en' => 'test 1'}", event_id: 11, row_order: 0>, 
#<EventParam id: 33, name: "test-2", html_type: "text", required: false, default_value: "test 2", options: {}, position: 2, label: "{'fr' => 'test 2', 'en' => 'test 2'}", event_id: 11, row_order: 4194304>, 
#<EventParam id: 34, name: "test-3", html_type: "text", required: false, default_value: "test 3", options: {}, position: 3, label: "{'fr' => 'test 3', 'en' => 'test 3'}", event_id: 11, row_order: 0>
]>

event_params_controller.rb:

def update_row_order
    @param = EventParam.find(params[:id])
    @param.update_attribute :row_order_position, params[:row_order_position]
    @param.save!

    render nothing: true
end

So everything works fine, except for the ranking part, so the table never update when I refresh the page.

wrong attribute update

This is very nice gem, but has a liitle bug. Attribute used for ranking is wrongly updated. I found this when implementing some sortable tables in rails. List of users is ordered by position - attribute use by ranked-model. Here is how the table look like:
table1

After taking element with position 15 to 5 every element gets updated, but look what happened to element with position > 15, it gets also added 1 so now there is a break (17th element after 15th, no element with position 16).
table2
After some modification of order it gets really annoying, because you dont know exactly how its going to work.

Another mistake is that it doesnt work for two elements at all. Position is getting max integer and min integer value.

I am using Postgres with Rails 4, if you want to see my implementation I can add link to the project.

Error creating records

Hi guys,
I've just had a stab at using ranked-model on 1.9.2 + Rails 3.0.4 with little success, hopefully I'm missing something simple here:

ActiveRecord::StatementInvalid: Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1: SELECT  id, waitlist_position FROM `applications` ORDER BY #<Arel::Nodes::Ordering:0x00000100ce5440> ASC LIMIT 1

This is for when I attempt to create an Application in console.

class Application < ActiveRecord::Base 
    include RankedModel
    ranks :waitlist_position
end

I've nuked everything out the terminal / made sure I'm using a medinteger (vs. plain integer) with no luck.
Also tried the gem vs. master on here.

Cheers,
Thomas

Negative position shows child before parent in `roots_and_descendants_preordered`

I accidentally set the *_position to -1, causing the actual position to be negative and the child showed before the parent in roots_and_descendants_preordered, self_and_descendants_preordered it's behaving correctly.

  Child A
Parent
  Child B
  Child C

Similarly doing Model.update_all :position => RankedModel::MIN_RANK_VALUE will set all positions to -8388607 and make all children show before its parent.

  Child A
  Child B
  Child C
Parent

Basic methods?

I have to switch gem from acts_as_list to ranked-model, because I need to restrict the scope.

I cannot find some of the basic methods for ranked-model though.

How do I get the position of an item relative to the others? (In AAL it's simply: position.)

And how would I remove an item to be included in the ranking set? (In AAL: remove_from_list.)

Ranking with_same through association

Here's my setup

# area.rb
class Area < ActiveRecord::Base
  has_many :tasks
end

# task.rb
class Task < ActiveRecord::Base
  belongs_to :area
  has_many :project_tasks
end

# project_task.rb
class ProjectTask < ActiveRecord::Base
  belongs_to :task
  belongs_to :project # not relevant for this example, for for completeness sake
  has_one :area, through: :task

  ranks :area_order, with_same: :area_id # Doesn't work because there's not an area_id column on project_tasks
end

I'm not sure how to solve this. I could cache area_id on the project_task, but that seems messy, and I'd then have to deal with making sure that always stays in sync. Is there anyway to use RankedModel's with_same feature with a joined attribute?

Also, thanks for making this awesome gem! It's saved me tons of time.

position accessor is private in 1.9

You cannot mass assign the position accessor because it is defined in a private method so respond_to? returns false (a bug in 1.9 most probably.) Is there any particular reason why ranks is private and not protected?

Not working for the existing data

I've added a ranked_model to the existing data and filled in row_order with 0 as a default value (remember that we can't have null values #73).

# initial state:
Duck.count # 4
Duck.update_all('row_order = 0')

# test cases (revert to initial stage before each):
@duck.update_attribute :row_order_position, 0 # works fine
@duck.update_attribute :row_order_position, 1 # does nothing
@duck.update_attribute :row_order_position, 2 # does nothing
@duck.update_attribute :row_order_position, 3 # works fine

I've solved this issue by updating row_order for existing data with an id value.
This case should be handled or at least documentation updated.

This issue probably describes the same problem noted in #67.

NoMethodError: undefined method `-' for nil:NilClass

I have ranked-model working fine in development, but in production I'm getting the following error ( console output );

@project.update_attribute(:position_position, 1)
NoMethodError: undefined method `-' for nil:NilClass
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/ranked-model-0.0.5/lib/ranked-model/ranker.rb:119:in `update_index_from_position'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/ranked-model-0.0.5/lib/ranked-model/ranker.rb:52:in `handle_ranking'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/ranked-model-0.0.5/lib/ranked-model.rb:31:in `block in handle_ranking'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/ranked-model-0.0.5/lib/ranked-model.rb:30:in `each'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/ranked-model-0.0.5/lib/ranked-model.rb:30:in `handle_ranking'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activesupport-3.0.6/lib/active_support/callbacks.rb:415:in `_run_save_callbacks'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/callbacks.rb:273:in `create_or_update'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/persistence.rb:39:in `save'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/validations.rb:43:in `save'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/attribute_methods/dirty.rb:21:in `save'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/transactions.rb:240:in `block (2 levels) in save'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/transactions.rb:292:in `block in with_transaction_returning_status'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/connection_adapters/abstract/database_statements.rb:139:in `transaction'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/transactions.rb:207:in `transaction'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/transactions.rb:290:in `with_transaction_returning_status'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/transactions.rb:240:in `block in save'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/transactions.rb:251:in `rollback_active_record_state!'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/transactions.rb:239:in `save'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/activerecord-3.0.6/lib/active_record/persistence.rb:117:in `update_attribute'
    from (irb):2
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/railties-3.0.6/lib/rails/commands/console.rb:44:in `start'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/railties-3.0.6/lib/rails/commands/console.rb:8:in `start'
    from /home/deploy/apps/my_app/shared/bundle/ruby/1.9.1/gems/railties-3.0.6/lib/rails/commands.rb:23:in `<top (required)>'
    from script/rails:6:in `require'
    from script/rails:6:in `<main>'

sql joins order by isn't including the table name, causes ambiguous column error when using select or pluck

When you join tables the ranked model doesn't include table alias in the order by so you get error:

Column 'row_order' in order clause is ambiguous

You only get this bug when attempting to access columns from result using pluck or select etc...

model.rb
include RankedModel
ranks :track_order, column: :row_order

## scopes ##
default_scope rank(:track_order)

Cause sql gen'd for:

Track.joins(:videos).where(videos: {fan_id: fan_id, workflow_state: [:submitted, :pending, :approved]}).pluck(:id)
SELECT `tracks`.`id` FROM `tracks` INNER JOIN `videos` ON `videos`.`track_id` = `tracks`.`id` WHERE `videos`.`fan_id` = ? AND `videos`.`workflow_state` IN ('submitted', 'pending', 'approved') ORDER BY row_order

Was able to override by doing this on include:

# model.rb
include RankedModel
ranks :track_order, column: 'tracks.row_order'

## scopes ##
default_scope rank(:track_order)

The above change generates sql correctly:

SELECT `tracks`.`id` FROM `tracks` INNER JOIN `videos` ON `videos`.`track_id` = `tracks`.`id` WHERE `videos`.`fan_id` = ? AND `videos`.`workflow_state` IN ('submitted', 'pending', 'approved') ORDER BY tracks.row_order

Gem should probably always alias order by clauses just to be safe of possible ambiguous column errors.

can't validate for presence

Hi, I would like to ensure my ranked model never has nil in the ranked position, but for some reasons if I do that it doesn't populate the position attribute with a default value:

class Item < ActiveRecord::Base
  include RankedModel
  ranks :position

  validates :text, :position, presence: true
end

And these are my specs:

it "should not be valid without position" do
  @item.text = "Test text for item"
  @item.position = nil
  @item.should_not be_valid
  @item.should have( 1 ).errors_on( :position )
end

it "should have relevant default position" do
  @item.text = "Test text for item"
  @item.save
  @item.should be_valid
end

Results in:

should have relevant default position (FAILED - 1)
should not be valid without position

Failures:

1) Item creation: should have relevant default position
 Failure/Error: @item.should be_valid
   expected #<Item id: nil, text: "Test text for item", position: nil, created_at: nil, updated_at: nil> to be valid, but got errors: Position can't be blank
 # ./spec/models/item_spec.rb:25:in `block (3 levels) in <top (required)>'

Obviously if I remove the validation inside my model the default position is populated but the validation fails.
Can I have both behaviors? Is this a bug?

Support models having non-default primary key column

It looks like ranked-model hard-codes the primary key as 'id'. I'm trying to use a legacy database and the tables have primary keys named "#{table_name}id".

Example:

class Image < ActiveRecord::Base
  self.table_name = "topicimages"
  self.primary_key = "imageid"
end

Apparently I cannot just rename my columns as the old application still needs to access those tables.

read row_order_position => nil

when i use an_object.row_order_position, i have nil but an_object.row_order return 4194304.

row_order_position isn't an accesor to have the position of the object ? how can I get it with ranked-model ?

actually, I use this :
AllObject.all.find_index(an_object)

Relative re-ordering?

Does ranked-model support something like move_higher and move_lower like the acts_as_list gem does?

Getting position

Hi,

I have an article model like this:

# == Schema Information
# Schema version: 20110705143543
#
# Table name: articles
#
#  id                         :integer         not null, primary key
#  issue_id                   :integer
#  issue_order                :integer
#  rubric_id                  :integer
#  heading                    :string(255)
#  teaser                     :text
#  body                       :text
#  preview_image_file_name    :string(255)
#  preview_image_content_type :string(255)
#  preview_image_file_size    :integer
#  preview_image_updated_at   :datetime
#  main_image_file_name       :string(255)
#  main_image_content_type    :string(255)
#  main_image_file_size       :integer
#  main_image_updated_at      :datetime
#  created_at                 :datetime
#  updated_at                 :datetime
#

class Article < ActiveRecord::Base
  include RankedModel
  ranks :issue_order, :with_same => :issue_id

  default_scope :order => 'issue_order DESC'

  belongs_to :issue
  belongs_to :rubric

  has_attached_file :preview_image
  has_attached_file :main_image

  validates_attachment_presence :preview_image
  validates_attachment_presence :main_image

  validates :body,        :presence => true
  validates :heading,     :presence => true
  validates :issue_id,    :presence => true
  validates :rubric_id,   :presence => true
  validates :teaser,      :presence => true

  def label
    heading
  end

  def issue_label
    issue.label
  end  

  def rubric_label
    rubric.label
  end  

  def rubric_color
    rubric.color
  end  
end

I created some articles:

Article.create!([
  { 
    :issue                 => issue, 
    :issue_order_position  => :first,
    :rubric                => Rubric.where(:name => "Zu Besuch bei...").first,
    :heading               => "Mousse T.", 
    :teaser                => "Der House-Meister", 
    :body                  => "DJmag trifft sich mit Peppermint Jams Labelboss Mousse T. anlรคsslich des 18-jรคhrigen Labelgeburtstag.",
    :preview_image         => File.open(File.join(Rails.root, 'db', 'assets', 'article', 'mousse_t_preview.jpg')),
    :main_image            => File.open(File.join(Rails.root, 'db', 'assets', 'article', 'mousse_t.jpg')) 
  },
  { 
    :issue                 => issue,
    :issue_order_position  => :first,
    :rubric                => Rubric.where(:name => "Special").first,
    :heading               => "Ibiza",
    :teaser                => "Ein balearischer Summer of Love!",
    :body                  => "Was ist schรถner als ein Sonnenaufgang an der Playa D'en Bossa nach 24 Stunden auf den Beinen? Oder charmanter als Alfredo, wenn er original balearische Beats in der dunstigen Wรคrme von Spaces-Premier Etage 25 Jahre spรคter spielt? Oder am Flughafen nach einer ganztรคgigen Session im DC10 auf den Heimflug zu warten? Deine FรผรŸe und Beine sind dreckverschmiert und Deine verzerrten Gesichtszรผge verstecken sich hinter dunklen Schatten - hast Du jemals schon eine solch intensive Mischung an Gefรผhlen erlebt? Diese Mixtur aus Euphorie, noch-bleiben-mรถchten und Angst-vor-der-Arbeit am nรคchsten Morgen? Nun, das ist Ibiza. Kein Ort auf der Welt - auch nicht die neuen Mรถchtegern-Locations, die gerade รผberall aus dem Boden sprieรŸen - kรถnnen ihr jemals gleichkommen. Sie ist ein Zufluchtsort, ein hedonistischer Traum und ein Wirbelwind aus Sonne, Meer und Sonics, ein Wegbereiter fรผr Underground-Stars, die sich dort mit den grรถรŸten, glamourรถsesten Namen der Dance-Szene die Klinke in die Hand geben.",
    :preview_image         => File.open(File.join(Rails.root, 'db', 'assets', 'article', 'ibiza_preview.jpg')),
    :main_image            => File.open(File.join(Rails.root, 'db', 'assets', 'article', 'ibiza.jpg')) 
  },
  { 
    :issue                 => issue,
    :issue_order_position  => :first,
    :rubric                => Rubric.where(:name => "Arts & Culture").first,
    :heading               => "Electro Punx",
    :teaser                => "Jetzt dreht Techno vรถllig durch!",
    :body                  => "Die elektronische Musikszene im Jahre 2011: Techno ist lรคngst Main-stream geworden, Electro-House erobert international die Charts und obwohl es unzรคhlige Facetten der elektronischen Musik gibt, schreit keine davon mehr wirklich \"Revolution!\". Doch halt! Schauen wir doch mal รผber die Landesgrenzen zu unseren Nachbarn in Belgien und Frankreich. Und schon werden wir fรผndig. Denn hier hat sich mit Acts wie Kap Bambino, Mon Corlonel und The Subs in den letzten Jahren eine kleine, aber umso wildere Electro-Szene gebildet, fรผr die elektronische Musik viel mehr als nur Ausdruck von grenzenlosem Hedonismus ist und deren Acts mit viel Lust auf Krach, Krawall und oft auch politischen Statements den weiten Weg aus kleinen Kellerclubs bis auf die groรŸen Festivals geschafft haben. Die Szene ist bestens vernetzt und steht mit ihrer herrlich durchgeknallten Wildheit in bester Tradition groรŸer Vorbilder. So beziehen sich viele der Acts auf die Urvรคter des Electropunk, wie das legendรคre New Yorker Projekt \"Suicide\" und vielen Tracks hรถrt man auch an, dass hier neben belgischen Techno auch Industrial von Front 242 bis Nizzer Ebb als Grundlage fรผr die - รผbrigens - hรถchst party-tauglichen Sounds diente. Wir stellen euch die wichtigsten Acts der Szene vor und schauen auch, wer sich im Deutschsprachigen Raum traut, richtig Krawall zu machen. Macht euch bereit fรผr einen Besuch im Mosh Pit der Dancemusic, inklusive Stage-Diving, entblรถรŸter Oberkรถrper und jeder Menge rotziger Punk-Attitude!",
    :preview_image         => File.open(File.join(Rails.root, 'db', 'assets', 'article', 'electro_punx_preview.jpg')), 
    :main_image            => File.open(File.join(Rails.root, 'db', 'assets', 'article', 'electro_punx.jpg')) 
  },
  { 
    :issue                 => issue,
    :issue_order_position  => :first,
    :rubric                => Rubric.where(:name => "Fashion & Style").first,
    :heading               => "Blutsgeschwister",
    :teaser                => "Geschรคftsfรผhrerin Karin Ziegler plaudert ein wenig aus dem Nรคhkรคstchen und erzรคhlt uns die Geschichte zum Label.",
    :body                  => "Karin Ziegler und Stephan Kรผnz sind die beiden Kรถpfe der Blutsgeschwister. Stephan hat Kommunikationswissenschaften studiert und im Anschluss ein Beratungsunternehmen im Bereich Marketing aufgebaut. Mehrere Jahre war er Marketingleiter beim grรถรŸten Sportmode- und Sportartikelunternehmen im Alpenraum - der Sportler AG mit 500 Mitarbeitern. Seit August 2010 ist er bei Blutsgeschwister fรผr Personal & Operations, Finance, Vertrieb & Retail, Marketing, Logistik und Einkauf verantwortlich. Was Karin vorher gemacht hat erzรคhlt sie uns noch im Interview.",
    :preview_image         => File.open(File.join(Rails.root, 'db', 'assets', 'article', 'blutsgeschwister_preview.jpg')), 
    :main_image            => File.open(File.join(Rails.root, 'db', 'assets', 'article', 'blutsgeschwister.jpg')) 
  },
  { 
    :issue                 => issue,
    :issue_order_position  => :first,
    :rubric                => Rubric.where(:name => "Feature").first,
    :heading               => "Barem",
    :teaser                => "Die Ruhe nach dem Sturm",
    :body                  => "\"There is nothing better than a clear blue sky after the Storm\", und damit kรถnnte der aus Argentinien stammenden Barem gar nicht so falsch liegen. Drei Jahre hat der Minus-Artist seit seinem letzen Release auf sich warten lassen. Nun ist er mit der ersten Singleauskopplung \"Blue\" und dem dazugehรถrigen, erstaunlich reif klingendem Albumdebรผt \"After the storm\", zurรผck aus dem Studio. Mit uns plaudert er รผber die aktuelle Entwicklung der Szene, den kommenden Sommer und das neue Album...",  
    :preview_image         => File.open(File.join(Rails.root, 'db', 'assets', 'article', 'barem_preview.jpg')), 
    :main_image            => File.open(File.join(Rails.root, 'db', 'assets', 'article', 'barem.jpg'))
  }
])

The ordering works as desired, but I can't get the position:

robo@Robo-VirtualBox:~/rails/djmag.de$ rails c
Loading development environment (Rails 3.0.7)
ruby-1.9.2-p136 :001 > Article.last.issue_order_position
 => nil 

Any clues?

Thanks in advance!

Cheers,

Roberto

position column changed when using `update_attributes`

If I update any attribute of my model it also updates the position column, manual_order, ranking the model last in the list.

puts document.sections.rank(:manual_order).map { |sec| "#{sec.name} (#{sec.manual_order})" }.join(', ')                               

document.sections[1].update_attributes(:name => 'X')                                                                                                                                                                                                          

puts document.sections.rank(:manual_order).map { |sec| "#{sec.name} (#{sec.manual_order})" }.join(', ')  

# => A (0), B (4194304), C (6291456), D (7340032) 
# => A (0), C (6291456), D (7340032), X (7864320)    

As you can see I changed the name of the second section, but it has also updated the manual_order column, putting it last. Here is the SQL generated:

UPDATE "document_sections" SET "name" = 'X', "manual_order" = 7864320, "updated_at" = '2012-12-10 20:12:50.912158' WHERE "document_sections"."id" = 2

This is how I am using ranked-model in my Section class:

ranks :manual_order, :with_same => :document_id

I noticed there is no problem when updating attributes of the first or last item in the list, only when updating those in the middle.

How to find out if an object is the first or last in the list?

If I have a ranked model how do I know if any one object is first or last in the list so I can prevent displaying 'up' / 'down' links in the UI for the first and last items?

e.g

<% @items.each do | item | %>
  <%= link_to 'Up', up_items_path(item) unless item.first? %>
  <%= link_to 'Down', down_items_path(item) unless item.last? %>
<% end %>

Update order without AJAX

I'm ordering only a selection of a master list, and storing the selection and its position in a different model (i.e., Master and Copy, and I want Copy ordered).

The easiest way I've found to achieve this is to let ranked-model do the initial ordering (i.e., no AJAX, just the JQueryUI.sortable list in the form). It works perfectly for create, and I think it'd work okay for updating too if I can find a way to re-save the positions on update (again, without AJAX). And, to complicate things further, entries in the selection can be changed, effectively deleting one ordering integer from the initial order, because it's stored on a relationship table.

I've tried resetting the positions on update in the controller to no avail:

Master.copies.each { |copy| copy.update_attribute :position, 0 }

However, deleting the copies on update and resetting them like so achieves the desired result:

def update
  @master.copies.destroy_all
  if @copy.update_attributes(copy_params)
  ...
end

Any ideas how to achieve what I'm after? Perhaps its out of scope for this gem, which is a shame because otherwise it's perfect.

To ask the this question in the most basic form: how is the order created? Can it be replicated for update?

:with_same on polymorphic field

First of all great job with this gem!

I have the following code

class CollectionFollower < ActiveRecord::Base

include RankedModel
ranks :row_order, with_same: :follower_id

belongs_to :collection
belongs_to :follower, polymorphic: true

validates_uniqueness_of :post_id

validates_presence_of :content_id, :post_id

end

and I'd like the with_same to act on the follower object (follower_id and follower_type).
Is this possible?

Thanks!

Provide a way to change min_rank and max_rank values

This are currently defined as

  # Signed MEDIUMINT in MySQL
  #
  MAX_RANK_VALUE = 8388607
  MIN_RANK_VALUE = -8388607

in https://github.com/mixonic/ranked-model/blob/master/lib/ranked-model.rb#L8

But it will be nice if we could change them as options of the rank method. E.g.:

ranks ..., min_rank_value: 0, max_rank_value: 2147483647

This to play nice with other gems (closure_tree's deterministic ordering) and to increase the range if necessary. For example postgre's integer ranges from -2147483648 to +2147483647.

How to make it work with ancestry?

I'd like to sort child categories within their parents. I use the ancestry gem for generating categories.

Would ranked-model play well with it? If so, how would I go about setting the scope for sorting within parent categories?

Also, do you tell it which position to use if you were to move a category from say.. the bottom of the list to the middle somewhere?

Inserting at a specific position gives different results depending on initial position

Hi,

I'm having following problem:

Assume I have 5 ranked objects (a, b, c, d, f). When I take the first object (a) and set the position to 4, it gets ranked after object f.
When I take the last element, and I set the position to 0 it gets ranked before the first element.

In other words, when I set an object to a position, sometimes it gets inserted before the object at the given position, sometimes after.

Am I doing something wrong? How can I insert an object before another object consistently?

Thanks in advance!

Cheers,
roberto

A simpler way to add ordering

Instead of:
Duck.rank(:row_order).all
You could put in your model:
default_scope order(:row_order)

This gives you the correct behavior everywhere, don't need to patch your controller.

RankedModel breaks rails console: `NameError: uninitialized constant Product::RankedModel`

Hey! I'm running into a strange error after including RankedModel. Everything within the app works perfectly, i'm able to resort and so on, but now my console no longer works when booting up rails c

My Product class looks like so:

class Release < ActiveRecord::Base
  include RankedModel
  ranks :row_order

  extend FriendlyId
  friendly_id :album, use: :slugged

  belongs_to  :artist, class_name: 'Artist', foreign_key: 'artist_id'
  has_many    :products
  has_many    :gallery_images
  accepts_nested_attributes_for :gallery_images

  mount_uploader :cover_photo, CoverPhotoUploader

  validates :album,   presence: true
  validates :catalog, presence: true
end

But when I boot up the console and try to execute any action on any model that is using RankedModel I receive an error:

irb(main):003:0> Product.all
NameError: uninitialized constant Product::RankedModel
    from /Users/damassi/Sites/UnseenWorlds/usw-backend/app/models/product.rb:3:in `<class:Product>'
    from /Users/damassi/Sites/UnseenWorlds/usw-backend/app/models/product.rb:1:in `<top (required)>'
    from (irb):3
    from /Users/damassi/.rvm/gems/ruby-2.1.0/gems/railties-4.1.4/lib/rails/commands/console.rb:90:in `start'
    from /Users/damassi/.rvm/gems/ruby-2.1.0/gems/railties-4.1.4/lib/rails/commands/console.rb:9:in `start'
    from /Users/damassi/.rvm/gems/ruby-2.1.0/gems/railties-4.1.4/lib/rails/commands/commands_tasks.rb:69:in `console'
    from /Users/damassi/.rvm/gems/ruby-2.1.0/gems/railties-4.1.4/lib/rails/commands/commands_tasks.rb:40:in `run_command!'
    from /Users/damassi/.rvm/gems/ruby-2.1.0/gems/railties-4.1.4/lib/rails/commands.rb:17:in `<top (required)>'
    from /Users/damassi/Sites/UnseenWorlds/usw-backend/bin/rails:8:in `<top (required)>'
    from /Users/damassi/.rvm/rubies/ruby-2.1.0/lib/ruby/site_ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
    from /Users/damassi/.rvm/rubies/ruby-2.1.0/lib/ruby/site_ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
    from -e:1:in `<main>'

Looking around on StackOverflow I can't seen to find any pointers, so I thought I would log this here. Also, my project is very basic so I don't think there are any unknown interferences.

Thanks!

Sort based on another column / batch re-rank

Not sure if this is a very common use-case, but I thought I'd mention it in case you find it worth thinking about.

I ended up needing to re-rank all elements based on another column in the table. E.g.

idrow_orderother_column
909
1041943041
1162914562

And you want to re-rank based on other_column ASC:

idrow_orderother_column
962914569
1001
1141943042

One way to do this in ranked-model would be like

Duck.order(:other_column => :asc).all.each_with_index do |duck, index|
  duck.update_attribute :row_order_position, index
end

For my case I ended up solving this using raw SQL which isn't the most desirable, but it is definitely efficient. It uses UPDATE FROM which I don't think is very portable, and ROW_NUMBER(), to basically just flip around the row_order column according to the desired sort order:

UPDATE ducks SET row_order = src.t1_roworder FROM
  (SELECT * FROM
    (SELECT *, row_order t1_roworder, ROW_NUMBER() OVER (ORDER BY row_order ASC) t1_rownum FROM ducks) t1 JOIN
    (SELECT *, ducks.id t2_id, ROW_NUMBER() OVER (ORDER BY other_column ASC) t2_rownum FROM ducks) t2
  ON t1_rownum = t2_rownum
  ORDER BY t2_id
  ) AS src
WHERE ducks.id = src.t2_id

Ranker#rearrange_ranks produces invalid SQL when column is named 'order'

When using an order column named 'order':

class PlaylistPosition < ActiveRecord::Base
  include RankedModel

  ranks :order, with_same: :playlist_id

  belongs_to :playlist
  belongs_to :story
end

ranked-model produces this query

UPDATE "playlist_positions" SET order = order - 1 WHERE "playlist_positions"."id" IN (SELECT "playlist_positions"."id" FROM "playlist_positions" WHERE "playlist_positions"."playlist_id" = 7 AND ("playlist_positions"."order" <= 8388607)  ORDER BY "playlist_positions"."order" ASC);
ERROR:  syntax error at or near "order"
LINE 1: UPDATE "playlist_positions" SET order = order - 1 WHERE "pla...

in Ranker#rearrange_ranks with Postgres 9.4.0 / Rails 4.2.0.

I've fixed it by simply renaming the column, but I wanted to report it anyway. SQL syntax is not my strongest suit, but could the fix be to simply quote the column name?

I do not understand ranked-model

It seems the big feature of ranked-model is to be able to efficiently change the symbolic position of an item in a list.

But when i ask for the symbolic _position, i get a nil. What use is there to change by symbolic position if i do not know the elements symbolic positions?

EDIT : I searched in github and found a few project using ranked-model, and i am even more lost as it seems they use ranked-model to sort by default. If i understand correctly, it rank by creation, wich is strange because you can use created at to order. So in addition if you could show me a few project that are good examples of how to use ranked-model it would be great.

Does ranked-model with :through associated models?

I'm having some trouble here.

I have HABTM relationship between List, Item and Entry models such that:

class List
  has_many :entries
  has_many :items, :through => :entries
end

class Item
  has_many :entries
  has_many :lists, :through => :entries
end

class Entry
  include RankedModel
  ranks :row_order, :with_same => :list_id

  belongs_to :list
  belongs_to :item
end

Now lets say I have a List with 3 items.

If I query like list.entries.rank(:row_order) it returns the items sorted correctly. But now lets say I re-position the first item to the end of the list with list.entries.first.update_attributes(:row_order_position => :last) now I reload the entry from the DB using list = List.find_by_id(list.id).

The ranked order is returned correctly with list.entries.rank(:row_order) but if I try and access Items directly via the :through relation ala list.items the array is not ordered. I cannot figure out a way to rank(:row_order) by default or explicitly in this situation.

Any help would be appreciated.

P.S. I cannot figure out the way to use current_position method. I've tried calling it as a class and instance method on both the List, Item and Entry model objects to no avail.

error; maybe just for postgres...

1.9.2p290 :019 >   BlogCategory.find 1
  BlogCategory Load (0.4ms)  SELECT "blog_categories".* FROM "blog_categories" WHERE "blog_categories"."id" = $1 LIMIT 1  [["id", 1]]
 => #<BlogCategory id: 1, name: "Living", description: "", blog_id: 1, created_at: "2012-04-20 04:03:33", updated_at: "2012-04-20 04:03:33", slug: "living", row_order: nil> 
1.9.2p290 :020 > bc.row_order_position = 5
 => 5 
1.9.2p290 :021 > bc.save!
   (0.4ms)  BEGIN
   (0.4ms)  SELECT 1 FROM "blog_categories" WHERE (LOWER("blog_categories"."name") = LOWER('Living') AND "blog_categories"."id" != 1 AND "blog_categories"."blog_id" = 1) LIMIT 1
  BlogCategory Load (0.4ms)  SELECT id, row_order FROM "blog_categories" WHERE ("blog_categories"."id" != 1) ORDER BY "blog_categories"."row_order" ASC LIMIT 2 OFFSET 4
  BlogCategory Load (35.6ms)  SELECT id, row_order FROM "blog_categories" WHERE ("blog_categories"."id" != 1) ORDER BY "blog_categories"."row_order" DESC LIMIT 1
   (0.2ms)  ROLLBACK
TypeError: nil can't be coerced into Fixnum
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/ranked-model-0.0.5/lib/ranked-model/ranker.rb:119:in `-'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/ranked-model-0.0.5/lib/ranked-model/ranker.rb:119:in `update_index_from_position'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/ranked-model-0.0.5/lib/ranked-model/ranker.rb:52:in `handle_ranking'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/ranked-model-0.0.5/lib/ranked-model.rb:31:in `block in handle_ranking'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/ranked-model-0.0.5/lib/ranked-model.rb:30:in `each'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/ranked-model-0.0.5/lib/ranked-model.rb:30:in `handle_ranking'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activesupport-3.1.3/lib/active_support/callbacks.rb:395:in `_run_save_callbacks'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activesupport-3.1.3/lib/active_support/callbacks.rb:81:in `run_callbacks'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activerecord-3.1.3/lib/active_record/callbacks.rb:264:in `create_or_update'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activerecord-3.1.3/lib/active_record/persistence.rb:57:in `save!'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activerecord-3.1.3/lib/active_record/validations.rb:56:in `save!'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activerecord-3.1.3/lib/active_record/attribute_methods/dirty.rb:33:in `save!'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activerecord-3.1.3/lib/active_record/transactions.rb:246:in `block in save!'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activerecord-3.1.3/lib/active_record/transactions.rb:295:in `block in with_transaction_returning_status'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activerecord-3.1.3/lib/active_record/connection_adapters/abstract/database_statements.rb:192:in `transaction'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activerecord-3.1.3/lib/active_record/transactions.rb:208:in `transaction'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activerecord-3.1.3/lib/active_record/transactions.rb:293:in `with_transaction_returning_status'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/activerecord-3.1.3/lib/active_record/transactions.rb:246:in `save!'
    from (irb):21
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/railties-3.1.3/lib/rails/commands/console.rb:45:in `start'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/railties-3.1.3/lib/rails/commands/console.rb:8:in `start'
    from /Users/Riggs/.rvm/gems/ruby-1.9.2-p290@rails311/gems/railties-3.1.3/lib/rails/commands.rb:40:in `<top (required)>'
    from script/rails:6:in `require'
    from script/rails:6:in `<main>'1.9.2p290 :022 > 

Support for ranking by STI parent

I'd like to reopen the discussion on #7

I'm not sure if you understand the issue. Considering the Element-NobleGas-TransitionMetal example you've used in STI spec, ranked_model already supports ranking within each child model. That works great.

However, think of a chemistry lab, which has several Element objects, which could be any combination of NobleGas and TransitionMetal objects. In this case, you will be ranking via the STI's parent class, which is Element in this case. This does not work as expected now.

I have a failing test case which demonstrates this, and I have also patched the latest master with the patch by @tanordheim, with the spec slightly modified.

https://github.com/adheerajkumar/ranked-model/commits/

Support models having composite primary keys

I'm trying to use a table from a legacy DB which has no singular primary key. Instead I used the composite-primary-keys gem to use this table in Rails. When trying to use ranked-model, the SQL breaks (do not look too much at the ugly field names, that will be changed later when the legacy applications using the DB become obsolete):

irb(main):050:0> pos = Article.find(34129).topic_article_links.find_by_topicid(348)
  Article Load (0.6ms)  SELECT `topicarticles`.* FROM `topicarticles` WHERE `topicarticles`.`articleid` = 34129 LIMIT 1
  TopicArticleLink Load (0.5ms)  SELECT `group_topic_article`.* FROM `group_topic_article` WHERE `group_topic_article`.`articleid` = 34129 AND `group_topic_article`.`topicid` = 348 LIMIT 1
=> #<TopicArticleLink topicid: 348, articleid: 34129, status: "AKTIV", article_prio: 0.0>
irb(main):051:0> pos.article_order_position = 0
=> 0
irb(main):052:0> pos.save
   (0.5ms)  BEGIN
  TopicArticleLink Load (11.8ms)  SELECT `group_topic_article`.`id`, `group_topic_article`.`article_prio` FROM `group_topic_article` WHERE `group_topic_article`.`topic_id` IS NULL AND (`group_topic_article`.`id` != 348, 34129) ORDER BY `group_topic_article`.`article_prio` ASC LIMIT 1
   (0.1ms)  ROLLBACK
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'group_topic_article.id' in 'field list': SELECT  `group_topic_article`.`id`, `group_topic_article`.`article_prio` FROM `group_topic_article`  WHERE `group_topic_article`.`topic_id` IS NULL AND (`group_topic_article`.`id` != 348, 34129) ORDER BY `group_topic_article`.`article_prio` ASC LIMIT 1

TopicArticleLink is the join-model between Article and Topic. The simplified models look like this:

class TopicArticleLink < ActiveRecord::Base
  self.table_name = "group_topic_article"
  self.primary_keys = :topicid, :articleid

  belongs_to :topic, :foreign_key => "topicid"
  belongs_to :article, :foreign_key => "articleid"

  include RankedModel
  ranks :article_order, :column => :article_prio, :with_same => :topicid
end

class Article < ActiveRecord::Base
  self.table_name = "topicarticles"
  self.primary_key = "articleid"

  has_many :topic_article_links, :foreign_key => "articleid", :dependent => :destroy
  has_many :topics, :through => :topic_article_links
end

class Topic < ActiveRecord::Base
  self.primary_key = "topicid"
  has_many :topic_article_links, :foreign_key => "topicid", :dependent => :delete_all
  has_many :articles, :through => :topic_article_links
end

RankedModel::InvalidField for perfectly valid ActiveRecord instance

I'm seeing some weird RankedModel behavior:

ruby 2.0.0p195 (2013-05-14 revision 40734) [x86_64-darwin12.3.0]
rails (4.0.0.rc1)
ranked-model (0.2.0)
class Column < ActiveRecord::Base
  include RankedModel
  ranks :display, with_same: :board_id

  belongs_to :board
  scope :ordered, -> { rank(:display) }
end
$ b = Board.create
=> #<Board id: 2, title: nil, data: {}, created_at: "2013-05-18 06:25:41", updated_at: "2013-05-18 06:25:41">

$ c1 = Column.create(title: 'c1', board: b)
=> #<Column id: 10, title: "c1", display: 0, board_id: 2, data: {}, created_at: "2013-05-18 06:26:04", updated_at: "2013-05-18 06:26:04">

$ c2 = Column.create(title: 'c2', board: b)
RankedModel::InvalidField: No field called "board_id" found in model

If I pry what's happening, I see the following:

$ c1 = Column.create(title: 'c1', board: b)
$ instance.respond_to?(ranker.with_same)
=> true
$ instance
=> #<Column id: nil, title: "c1", display: 0, board_id: 2, data: {}, created_at: nil, updated_at: nil>
$ exit
=> #<Column id: 10, title: "c1", display: 0, board_id: 2, data: {}, created_at: "2013-05-18 06:26:04", updated_at: "2013-05-18 06:26:04">

$ c2 = Column.create(title: 'c2', board: b)
$ instance.respond_to?(ranker.with_same)
=> true
$ instance
=> #<Column id: nil, title: "c2", display: 0, board_id: 2, data: {}, created_at: nil, updated_at: nil>
$ exit
#  Column Load (0.9ms)  SELECT "columns"."id", "columns"."display" FROM "columns"
# WHERE "columns"."board_id" = 2 AND "columns"."display" = 0
# ORDER BY "columns"."id" ASC LIMIT 1
$ instance.respond_to?(ranker.with_same)
=> false
$ instance
=> #<Column id: 10, display: 0>
$ exit
RankedModel::InvalidField: No field called "board_id" found in model

What seems to be happening is that on creating the second column, it actually checks the other columns to see if things need to shift around. However, the query (which I added in the stack trace above) only selects the id and display fields, leaving out the board_id field. This in return results in instance.respond_to?(ranker.with_same) failing.

This seems like too big a bug to have gone unnoticed for so long, so I fully expect the problem to be on my end, but I can't figure out what could be causing this. I am not doing anything special.

Updating the readme for beginners like myself

Not really an issue, but I believe, for certain people, a slightly more detailed readme could be of use in diagnosing some of the issues I had when setting this (otherwise very easy to use) plugin up.

For instance, you include having to set up the row_order column as an integer, but don't mention having to also set up a row_order_position column as an integer.

Also, setting those both to default to 0 instead of nil tripped me up for a short while.

Obviously not huge issues, but it might help adoption by those of us who might have otherwise given up on a great plugin because they weren't sure where to look for the solution (the answer being in the console and logs to see where the problem was for me, and then look at the source code to see what was expected- but again, this is merely to help out beginners.

Let me know what you think - if you're busy, I'd be happy to write a slightly modified readme and submit it to you.

Regardless, thanks for the great plugin.

Steve

Ordering from last up one, moves it to second

Rails 4.2, Ruby 2.2, ranked-model 0.4

I'm using ranked-model on several tables and have noticed a weird behavior. When I have at least 4 items and try to reorder the last item up one position, it gets bumped up to second place.

I was able to reproduce in a controller test, so I think it is a bug. I can make an example app this weekend if need be.

Here is one model:

class Tag < ActiveRecord::Base
  include RankedModel
  ...
  ranks :row_order
  default_scope { order(:row_order) }
 ...
end

This test doesn't pass, it results in [first, fourth, second, third]. I expect [first, second, fourth, third]

it "orders the tags" do
  first = create(:tag)
  second = create(:tag)
  third = create(:tag)
  fourth = create(:tag)

  put :update, { id: fourth.to_param, tag: { row_order_position: 'up' } }
  expect(Tag.all).to eq [first, second, fourth, third]
end

Ordering down works as expected.

Is it possible to use simple ordering by another field with rank?

I want to sort a list of posts by position (with the help of ranked-model) and by their published date.
It works fine with select: Post.rank(:position).order(:published_at) produces SELECT "posts".* FROM "posts" ORDER BY "posts".position ASC, posts.published_at DESC.

But I can't setup it to work with updates.

Let me explain:
I have a list of posts on a page in a sortable list (based on this tutorial). They're ordered by position and published_at fields. (As I said before).
At first, posts are ordered only by date, because there're no values in position column in the database. (I assumed from the previous issues that I don't need it, right?)
Let's say, I want to move some post above another post. It updates position field on this post, but it makes it above all of the posts instead. (because position fields on other posts are empty). so, it doesn't work as expected.

I tried to create initial values for position:

Post.order(published_at: :desc).find_each {|post| post.update_attribute :position_position, 0 }

but when I try to update the post, ranked-model places it in the wrong position in the list.

so, my question: Is it possible to use this gem with ordering by another column. (not based on ranked-model)

Allow for scope with argument to access model instance

I was wondering if you see a legitimate use case to allow for scopes that accept an argument, which represent the current instance RankedModel is working with.

For example, I made this modification:

@@ -198,7 +199,11 @@ module RankedModel
         @finder ||= begin
           _finder = instance_class
           if ranker.scope
-            _finder = _finder.send ranker.scope
+            if _finder.method(ranker.scope).arity == 0
+              _finder = _finder.send ranker.scope
+            else
+              _finder = _finder.send ranker.scope, instance
+            end
           end
           case ranker.with_same
             when Symbol

This allows me to do something like this:

class Story < ActiveRecord::Base
  include RankedModel
  ranks :priority, scope: :same_swimlane_as

  scope :same_swimlane_as, ->(story) do
    stories = self.where(column_id: story.column_id)

    # find all stories that have swimlane labels
    labeled_stories = stories.joins("INNER JOIN swimlanes ON
      ((string_to_array(swimlanes.data -> 'labels', ',')) &&
       (string_to_array(tracker -> 'labels', ',')))")

    # get stories with same swimlane labels as current story
    if sllabels = Label.new(story).swimlaneizers.join(',') and sllabels.present?
      labeled_stories.where("string_to_array(swimlanes.data -> 'labels', ',') @> '{#{sllabels}}'")

    # get only stories without swimlane labels for 'default' swimlane...
    elsif labeled_stories.any?
      stories.where("stories.id NOT IN (?)", labeled_stories)

    # ...or just all labels if no non-default swimlane stories exist in column
    else
      stories
    end
  end
end

This is just a small (rough) example of how this could be used. At first I checked for arity == 1, but I found out that the scope actually returned an arity of -1, meaning it accepts a variable number of arguments. Perhaps this is because it's a lambda, which works differently?

Anyway, I found it useful, and use it in my fork, if this is something you want to implement (and possibly know of a cleaner way to do so), that would be great ๐Ÿ‘

Using `rank` breaks the `last` method

I've found out that when using rank the last method can't add DESC to the order clause, thus acting the same way as first. For now, I've just stopped using the rank method in favor of AR's order, is there any problem with this?

Error on trying to sort records with row_order = null

class AddRowOrderToImages < ActiveRecord::Migration
  def change
    add_column :images, :row_order, :integer
  end
end

class Image < ActiveRecord::Base
  # ...
  include RankedModel
  ranks :row_order, with_same: [:attachable_id, :attachable_type]
end

@image.update_attribute :row_order_position, 0 
# raises NoMethodError (undefined method `-' for nil:NilClass):

I've solved it migrating to :null => false, :default => 0 for row_order column.

Solutions:

  • update README with example migration with specified :null => false, :default => 0
  • respect null value during row_order recalculations.

License missing from gemspec

Some companies will only use gems with a certain license.
The canonical and easy way to check is via the gemspec
via e.g.

spec.license = 'MIT'
# or
spec.licenses = ['MIT', 'GPL-2']

Bundler now generates gems with a default 'MIT' license. There is even a License Finder
to help companies ensure all gems they use meet their licensing needs. This tool depends on license information being available in the gemspec.
Including a license in your gemspec is a good practice, in any case.

If you need help choosing a license, github has created a license picker tool

How did I find you?

I'm using a script to collect stats on gems, originally looking for download data, but decided to collect licenses too,
and make issues for gemspecs not specifying a license as a public service :)
So far it's going pretty well.
I've written a blog post about it

Gem doesn't load in Rails 4 engine.gemspec

Hi, since a Rails 4 --mountable engine is a .gemspec, and since the recommended place to put gems is in the .gemspec, this is more bug-ish than a bug. I've seen it in other libraries too.

In the engine_name.gemspec file, this loads but is inaccessible by Rails.
s.add_dependency 'ranked-model'

The only way to fix it is to put it into the Gemfile. Kind of a bummer when all of the other gems are in the gemspec!

Not sorting to last position

I just installed the ranked-model plugin, but I see some strange things going on (using Rails 3.0.7).
When I do a entity.update_attribute :row_order_position, :last it doesn't move the element to the last position. But another strange thing is the distribution of the elements (directly after creating those). The last elements in the row (existing of about 100 items) have row_order of ..., 8388599, 8388600, 8388601, 8388602 ... 8388607. So no space in between at all.
Moving items in a row with 10 elements or so seems to work fine. It shows only up when using > 80 elements ... and only when trying to move to a near the end of the row position.
My model uses ranks :row_order, :with_same => :grid_id.

Default value for the position column?

Seems like nil values should be avoided for the position column to avoid problems with ranked_model. (IMHO This should be mentioned in docs!)

Would this be the recommended practice assuming we would want new items to be at the bottom as default?

t.integer :sort_order, :default => RankedModel::MAX_RANK_VALUE

Ranks next to each other

After 22 items in my ranked-model, the ranks already are exactly next to each other!

Why is this?
Am I doing something wrong?
Or is this thing not build for lists larger than 20 items?

My initial list is created in order.
I basically append new items to the bottom.
No reordering until I have enough clips in my list.

Can I make ranked-model re-rank all my items in the list?

Here is my model:

class ScheduledClip < ActiveRecord::Base
  include RankedModel

  belongs_to :video
  belongs_to :scheduled_block
  attr_accessible :duration, :position, :uuid, :is_ad, :is_filler, :xml, :video_id, :scheduled_block_id, :nb_frames, :frame_rate

  ranks :block_order, :column => :position, :with_same => :scheduled_block_id
end

This is the result of ScheduledBlock.find(30).scheduled_clips.rank(:block_order)

position: 0, video_id: 8811, block_order: -73
position: 1, video_id: 8802, block_order: 4194231
position: 2, video_id: 8988, block_order: 6291383
position: 3, video_id: 7830, block_order: 7339959
position: 4, video_id: 8094, block_order: 7864247
position: 5, video_id: 8040, block_order: 8126391
position: 6, video_id: 8196, block_order: 8257463
position: 7, video_id: 11888, block_order: 8322999
position: 8, video_id: 9031, block_order: 8355767
position: 9, video_id: 8815, block_order: 8372151
position: 10, video_id: 9084, block_order: 8380343
position: 11, video_id: 7822, block_order: 8384439
position: 12, video_id: 8977, block_order: 8386487
position: 13, video_id: 8914, block_order: 8387511
position: 14, video_id: 9109, block_order: 8388023
position: 15, video_id: 7828, block_order: 8388279
position: 16, video_id: 8233, block_order: 8388407
position: 17, video_id: 8106, block_order: 8388471
position: 18, video_id: 8216, block_order: 8388503
position: 19, video_id: 7836, block_order: 8388519
position: 20, video_id: 8877, block_order: 8388527
position: 21, video_id: 8953, block_order: 8388531
position: 22, video_id: 8795, block_order: 8388533
position: 23, video_id: 11886, block_order: 8388534
position: 24, video_id: 8108, block_order: 8388535
position: 25, video_id: 8222, block_order: 8388536
position: 26, video_id: 8162, block_order: 8388537
position: 27, video_id: 11821, block_order: 8388538
position: 28, video_id: 9081, block_order: 8388539
position: 29, video_id: 9124, block_order: 8388540
position: 30, video_id: 8985, block_order: 8388541
position: 31, video_id: 12805, block_order: 8388542
position: 32, video_id: 8805, block_order: 8388543
position: 33, video_id: 8782, block_order: 8388544
position: 34, video_id: 8916, block_order: 8388545
position: 35, video_id: 11821, block_order: 8388546
position: 36, video_id: 9131, block_order: 8388547
position: 37, video_id: 11317, block_order: 8388548
position: 38, video_id: 9013, block_order: 8388549
position: 39, video_id: 12094, block_order: 8388550
position: 40, video_id: 8111, block_order: 8388551
position: 41, video_id: 8082, block_order: 8388552
position: 42, video_id: 8070, block_order: 8388553
position: 43, video_id: 7830, block_order: 8388554
position: 44, video_id: 8843, block_order: 8388555
position: 45, video_id: 8958, block_order: 8388556
position: 46, video_id: 9004, block_order: 8388557
position: 47, video_id: 11821, block_order: 8388558
position: 48, video_id: 9085, block_order: 8388559
position: 49, video_id: 8806, block_order: 8388560
position: 50, video_id: 8986, block_order: 8388561
position: 51, video_id: 7824, block_order: 8388562
position: 52, video_id: 8200, block_order: 8388563
position: 53, video_id: 8163, block_order: 8388564
position: 54, video_id: 7983, block_order: 8388565
position: 55, video_id: 11821, block_order: 8388566
position: 56, video_id: 8092, block_order: 8388567
position: 57, video_id: 8004, block_order: 8388568
position: 58, video_id: 8144, block_order: 8388569
position: 59, video_id: 7826, block_order: 8388570
position: 60, video_id: 9020, block_order: 8388571
position: 61, video_id: 8817, block_order: 8388572
position: 62, video_id: 8839, block_order: 8388573
position: 63, video_id: 7831, block_order: 8388574
position: 64, video_id: 8999, block_order: 8388575
position: 65, video_id: 8834, block_order: 8388576
position: 66, video_id: 9019, block_order: 8388577
position: 67, video_id: 7826, block_order: 8388578
position: 68, video_id: 8868, block_order: 8388579
position: 69, video_id: 8909, block_order: 8388580
position: 70, video_id: 9003, block_order: 8388581
position: 71, video_id: 11820, block_order: 8388582
position: 72, video_id: 12332, block_order: 8388583
position: 73, video_id: 12324, block_order: 8388584
position: 74, video_id: 12320, block_order: 8388585
position: 75, video_id: 7826, block_order: 8388586
position: 76, video_id: 8121, block_order: 8388587
position: 77, video_id: 8218, block_order: 8388588
position: 78, video_id: 8109, block_order: 8388589
position: 79, video_id: 11820, block_order: 8388590
position: 80, video_id: 8788, block_order: 8388591
position: 81, video_id: 9087, block_order: 8388592
position: 82, video_id: 8785, block_order: 8388593
position: 83, video_id: 11784, block_order: 8388594
position: 84, video_id: 8076, block_order: 8388595
position: 85, video_id: 8132, block_order: 8388596
position: 86, video_id: 8160, block_order: 8388597
position: 87, video_id: 7828, block_order: 8388598
position: 88, video_id: 8945, block_order: 8388599
position: 89, video_id: 9050, block_order: 8388600
position: 90, video_id: 8871, block_order: 8388601
position: 91, video_id: 11886, block_order: 8388602
position: 92, video_id: 11741, block_order: 8388603
position: 93, video_id: 11740, block_order: 8388604
position: 94, video_id: 11742, block_order: 8388605
position: 95, video_id: 11820, block_order: 8388606
position: 96, video_id: 11754, block_order: 8388607

Assigning new position through nested forms and mass assignment

I'm trying to update the position by using nested attributes, fields_for and a hidden field <ranker-name>_position. It works well as long as I also update one of the regular fields, but if only the position field is updated, no change will be saved to the database. Rails just ignores to update the record.

Simplified code:

class Image < ActiveRecord::Base
  include RankedModel
  ranks :image_order, :column => :sort_by, :with_same => :article_id
  default_scope rank(:image_order)
  attr_accessible :description, :image_order_position
end

class Article < ActiveRecord::Base
  has_many :images
  accepts_nested_attributes_for :images_attributes
end

Views and coffeescript:

$ ->
  $('table#images').disableSelection()
  $('table#images').sortable
    items: 'tr.image'
    axis: 'y'
    update: -> $('tr.image', this).each (index) -> $('input.position_value', this).val(index)

module ArticlesHelper
  def setup_images_for article
    article.images.build while article.images.to_a.count {|i| i.new_record? } < 5
    article.images.each.with_index {|image,index| image.image_order_position = index }
    article.images
  end
end

<%= form.fields_for :images, setup_images_for(form.object) do |image_field| -%>
  <%= content_tag_for :tr, image_field.object do %>
    <td>
      <%= image_field.hidden_field :image_order_position, :class => 'position_value' %>
      ...

In the log I see the position values come in right but database updates are only done for the images table if I send changes for the description field. If that field is left as-is, Rails just ignores that I also passed image_order_position and does not touch the database.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.