# frozen-string-literal: true module Sequel module Plugins # The list plugin allows for model instances to be part of an ordered list, # based on a position field in the database. It can either consider all # rows in the table as being from the same list, or you can specify scopes # so that multiple lists can be kept in the same table. # # Basic Example: # # class Item < Sequel::Model(:items) # plugin :list # will use :position field for position # plugin :list, :field=>:pos # will use :pos field for position # end # # item = Item[1] # # # Get the next or previous item in the list # # item.next # item.prev # # # Modify the item's position, which may require modifying other items in # # the same list # # item.move_to(3) # item.move_to_top # item.move_to_bottom # item.move_up # item.move_down # # You can provide a :scope option to scope the list. This option # can be a symbol or array of symbols specifying column name(s), or a proc # that accepts a model instance and returns a dataset representing the list # the object is in. # # For example, if each item has a +user_id+ field, and you want every user # to have their own list: # # Item.plugin :list, :scope=>:user_id # # Note that using this plugin modifies the order of the model's dataset to # sort by the position and scope fields. Also note that this plugin is subject to # race conditions, and is not safe when concurrent modifications are made # to the same list. # # Additionally, note that unlike ruby arrays, the list plugin assumes that the # first entry in the list has position 1, not position 0. # # Copyright (c) 2007-2010 Sharon Rosner, Wayne E. Seguin, Aman Gupta, Adrian Madrid, Jeremy Evans module List # Set the +position_field+ and +scope_proc+ attributes for the model, # using the :field and :scope options, respectively. # The :scope option can be a symbol, array of symbols, or a proc that # accepts a model instance and returns a dataset representing the list. # Also, modify the model dataset's order to order by the position and scope fields. def self.configure(model, opts = OPTS) model.position_field = opts[:field] || :position model.dataset = model.dataset.order_prepend(model.position_field) model.scope_proc = case scope = opts[:scope] when Symbol model.dataset = model.dataset.order_prepend(scope) proc{|obj| obj.model.where(scope=>obj.send(scope))} when Array model.dataset = model.dataset.order_prepend(*scope) proc{|obj| obj.model.where(scope.map{|s| [s, obj.get_column_value(s)]})} else scope end end module ClassMethods # The column name holding the position in the list, as a symbol. attr_accessor :position_field # A proc that scopes the dataset, so that there can be multiple positions # in the list, but the positions are unique with the scoped dataset. This # proc should accept an instance and return a dataset representing the list. attr_accessor :scope_proc Plugins.inherited_instance_variables(self, :@position_field=>nil, :@scope_proc=>nil) end module InstanceMethods # The model object at the given position in the list containing this instance. def at_position(p) list_dataset.first(position_field => p) end # When destroying an instance, move all entries after the instance down # one position, so that there aren't any gaps def after_destroy super f = Sequel[position_field] list_dataset.where(f > position_value).update(f => f - 1) end # Find the last position in the list containing this instance. def last_position list_dataset.max(position_field).to_i end # A dataset that represents the list containing this instance. def list_dataset model.scope_proc ? model.scope_proc.call(self) : model.dataset end # Move this instance down the given number of places in the list, # or 1 place if no argument is specified. def move_down(n = 1) move_to(position_value + n) end # Move this instance to the given place in the list. Raises an # exception if target is less than 1 or greater than the last position in the list. def move_to(target, lp = nil) current = position_value if target != current checked_transaction do ds = list_dataset op, ds = if target < current target = 1 if target < 1 [:+, ds.where(position_field=>target...current)] else lp ||= last_position target = lp if target > lp [:-, ds.where(position_field=>(current + 1)..target)] end ds.update(position_field => Sequel::SQL::NumericExpression.new(op, position_field, 1)) update(position_field => target) end end self end # Move this instance to the bottom (last position) of the list. def move_to_bottom lp = last_position move_to(lp, lp) end # Move this instance to the top (first position, position 1) of the list. def move_to_top move_to(1) end # Move this instance the given number of places up in the list, or 1 place # if no argument is specified. def move_up(n = 1) move_to(position_value - n) end # The model instance the given number of places below this model instance # in the list, or 1 place below if no argument is given. def next(n = 1) n == 0 ? self : at_position(position_value + n) end # The value of the model's position field for this instance. def position_value get_column_value(position_field) end # The model instance the given number of places below this model instance # in the list, or 1 place below if no argument is given. def prev(n = 1) self.next(n * -1) end private # Set the value of the position_field to the maximum value plus 1 unless the # position field already has a value. def _before_validation unless get_column_value(position_field) set_column_value("#{position_field}=", list_dataset.max(position_field).to_i+1) end super end # The model's position field, an instance method for ease of use. def position_field model.position_field end end end end end