Skip to content

acts_as_list scope by ancestry doesn't work for root #78

Closed
JeanMertz opened this Issue Sep 16, 2011 · 4 comments

3 participants

@JeanMertz

After reading this ancestry wiki page I figured the following would tie in acts_as_list with ancestry:

class Page < ActiveRecord::Base
  has_ancestry orphan_strategy: :restrict, cache_depth: true
  acts_as_list scope: 'ancestry = \'#{ancestry}\''
end

The problem is that the position attribute doesn't get updated if the ancestry attribute is nil, which is the case for all root level records. Any idea how to solve this?

See this console example:

# root elements, position stays at 1

$ website.pages.create(name: "root 1", theme: theme)
 => #<Page id: 1, position: 1, ancestry: nil, ancestry_depth: 0, theme_id: 1, website_id: 1> 

$ website.pages.create(name: "root 2", theme: theme)
 => #<Page id: 2, position: 1, ancestry: nil, ancestry_depth: 0, theme_id: 1, website_id: 1> 

$ website.pages.create(name: "root 3", theme: theme)
 => #<Page id: 3, position: 1, ancestry: nil, ancestry_depth: 0, theme_id: 1, website_id: 1> 

$ root4 = website.pages.create(name: "root 4", theme: theme)
 => #<Page id: 4, position: 1, ancestry: nil, ancestry_depth: 0, theme_id: 1, website_id: 1>

# child elements, position increments on create()

$ root4.children.create(name: "child 1", theme: theme, website: website)
 => #<Page id: 5, position: 1, ancestry: "4", ancestry_depth: 1, theme_id: 1, website_id: 1> 

$ root4.children.create(name: "child 2", theme: theme, website: website)
 => #<Page id: 6, position: 2, ancestry: "4", ancestry_depth: 1, theme_id: 1, website_id: 1> 

$ root4.children.create(name: "child 3", theme: theme, website: website)
 => #<Page id: 7, position: 3, ancestry: "4", ancestry_depth: 1, theme_id: 1, website_id: 1> 
@JeanMertz

Well, I managed to "solve" the issue, but it feels like this should be easier so if anyone has any other solutions, please comment.

The problem was that the above scope would produce the following query if ancestry was nil:

SELECT "pages".* FROM "pages" WHERE (ancestry = '') ORDER BY position DESC LIMIT 1

What was needed, was for the query to search for NULL instead of an empty string.

Here is my solution to this problem: (my only concern with this is if it will work in production due to class caching)

  acts_as_list scope: '#{ancestry ? "ancestry = \'#{ancestry}\'" : \'ancestry IS NULL\'}'

Now the following situations produce different queries:

Page.create
SELECT "pages".* FROM "pages" WHERE (ancestry IS NULL) ORDER BY position DESC LIMIT 1

and

Page.first.children.create
SELECT "pages".* FROM "pages" WHERE (ancestry = '1') ORDER BY position DESC LIMIT 1

It's basically a solution within the confines of acts_as_list which only accepts a string with a custom query. I'd rather put this in a method and call that method on scope, but I couldn't find a way to do this without modifying the acts_as_list gem.

EDIT since we're talking raw queries here, and given the fact that I couldn't find anyone else bumping into this issue, I think it's good to point out that I am using PostgreSQL, it could be that this isn't an issue with sqlite and/or MySQL and that they both consider an empty string to be NULL?

@sytse
sytse commented Sep 18, 2011

You could also define the scope_condition yourself:

def scope_condition
  ancestry ? "ancestry = '#{ancestry}'" : 'ancestry IS NULL'
end

This is also relevant for SQLite and MySQL by the way.

@JeanMertz

Thanks @sytse, that makes a lot of sense, I didn't realize the gem simply adds some code to the Model you set acts_as_list on. Your code is a lot cleaner indeed.

@JeanMertz JeanMertz closed this Sep 18, 2011
@sj26
sj26 commented Aug 1, 2012

Technique on the wiki page has been updated—you can just use scope: [:ancestry] which treats ancestry as a hash condition and acts gracefully with roots.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.