Search code examples
ruby-on-railsrubyruby-on-rails-5acts-as-list

Is there a way to scope acts_as_list based on column in another table?


TLDR: Is there a way to scope acts_as_list into another table as such

class SprintTodo < ApplicationRecord
  belongs_to :sprint
  belongs_to :todo
  acts_as_list scope: [:sprint, :todo.status]
end

I have two tables with one joining table.

  1. Todo(name, position, status, parent, children, ...)
  2. SprintTodo(todo_id, sprint_id, position)
  3. Sprint(name, start_date, end_date, ...)

Todo has its own position based on its parents (tree) while SprintTodo holds the position as in Kanban Board based on its status.

The problem I am facing right now is that I cannot reach into Todo table to scope it that way. One solution (although a bad one) is to replicate Todo status in SprintTodo as well but that would be bad design.

Is there any other way I can scope it on status?


Solution

  • It'll probably be simpler to add a status column to SprintTodo instead. But there is a way:

    class SprintTodo < ApplicationRecord
      belongs_to :todo
      belongs_to :sprint
    
      acts_as_list scope: "sprint_todos.id IN (\#{todo_status_sql}) AND sprint_todos.sprint_id = \#{sprint_id}"
    
      def todo_status_sql
        SprintTodo.select(:id).joins(:todo).where(todo: { status: todo.status }).to_sql
      end
    end
    
    Sprint.create!
    Todo.create!([{ status: :one }, { status: :one }, { status: :two }, { status: :two }])
    Sprint.first.todos << Todo.all
    Sprint.create!
    Sprint.second.todos << Todo.create(status: :one)
    
    
    >> SprintTodo.all.as_json(only: [:position, :sprint_id], include: {todo: {only: [:status, :id]}})
    => 
    [{"position"=>1, "sprint_id"=>1, "todo"=>{"id"=>1, "status"=>"one"}},
     {"position"=>2, "sprint_id"=>1, "todo"=>{"id"=>2, "status"=>"one"}},
    
     {"position"=>1, "sprint_id"=>1, "todo"=>{"id"=>3, "status"=>"two"}},
     {"position"=>2, "sprint_id"=>1, "todo"=>{"id"=>4, "status"=>"two"}},
    
     {"position"=>1, "sprint_id"=>2, "todo"=>{"id"=>5, "status"=>"one"}}]
    #             ^               ^                               ^
    #     positioned        by sprint                 and todo.status  
    

    https://www.rubydoc.info/gems/acts_as_list/0.8.2/ActiveRecord/Acts/List/ClassMethods#acts_as_list-instance_method


    Update

    I didn't see anything in acts_as_list code to support reordering on status change. The change happens in Todo, but all the callbacks to update position are in SprintTodo:

    https://github.com/brendon/acts_as_list/blob/v1.1.0/lib/acts_as_list/active_record/acts/callback_definer.rb#L6-L16

    First approach is just create a new SprintTodo:

    class Todo < ApplicationRecord
      has_many :sprint_todos
    
      after_update do
        new_sprint_todo = sprint_todos.first.dup
        sprint_todos.first.destroy
        new_sprint_todo.position = nil
        new_sprint_todo.save!
      end
    end
    

    The other way is to trigger some of those callbacks manually:

    class SprintTodo < ApplicationRecord
      attr_accessor :scope_changed
      #...
    end
    
    class Todo < ApplicationRecord
      has_many :sprint_todos
    
      before_update do
        sprint_todo = sprint_todos.first
        sprint_todo.scope_changed = true
        sprint_todo.send :check_scope
        sprint_todo.save!
      end
    end
    

    Have to set @scope_changed, otherwise check_scope won't do anything:
    https://github.com/brendon/acts_as_list/blob/v1.1.0/lib/acts_as_list/active_record/acts/list.rb#L430-L441