Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix slug regeneration behaviour #334

Merged
merged 2 commits into from Apr 16, 2013

Conversation

urbanautomaton
Copy link
Contributor

Hi,

Firstly, apologies: this is a bit of an epic, but it's taken me a while to get my head round it, so I've tried to explain thoroughly.

I noticed some odd behaviour recently when trying to force slug regeneration, which I traced down to Model#should_generate_new_friendly_id?. This returns inconsistent values depending on the order of certain operations. An example to recreate:

class User
  friendly_id :name, :use => :slugged
end
# Clearing slug on a previously-saved instance
> u = User.create(:name => "geoff")
=> #<User id: 1, name: "geoff", slug: "geoff">
> u.slug
=> "geoff"
> u.slug = nil
=> nil
> u.should_generate_new_friendly_id?
=> false

# Clearing slug on a freshly-loaded instance
> u2 = User.find("geoff")
=> #<User id: 1, name: "geoff", slug: "geoff">
> u2.slug
=> "geoff"
> u2.slug = nil
=> nil
> u2.shoud_generate_new_friendly_id?
=> true

This divergence of behaviour leads to oddities like this test, in which a record needs to be saved twice after its slug is cleared before being restored.

It turns out this was being caused by the mis-ordered setting of @current_friendly_id, which occurs in a before_save lifecycle callback. However #set_slug, which calls #should_generate_new_friendly_id to check whether a new slug is needed, is called in a before_validation callback, which happens prior to before_save.

This means that as far as #should_generate_new_friendly_id is concerned, @current_friendly_id doesn't represent the current friendly_id at all, but rather the friendly_id at the time this particular instance of the record was last saved (if at all).

This could be fixed by setting @current_friendly_id in a prepended before_validation callback. However this would be pointless, as then it would always be identical to the current value of the slug column, which #should_generate_new_friendly_id also fetches.

The attached commits provide updated tests for the regeneration behaviour, and address the issue by only checking the current value of the slug column in #should_generate_new_friendly_id?, removing current_friendly_id completely.

Unfortunately, however, this breaks a test in SimpleI18n:

test "set friendly_id should fall back default locale when none is given" do
  transaction do
    journalist = I18n.with_locale(:es) do
      Journalist.create!(:name => "Juan Fulano")
    end
    journalist.set_friendly_id("John Doe")
    journalist.save!
    assert_equal "john-doe", journalist.slug_en
  end
end

The reason this worked before was another bug in #should_generate_new_friendly_id?, whereby if current_friendly_id was set, only that would be compared against the slug base - the current slug column value was not checked at all. So in the above test, the following happened:

  1. A Journalist is created in :es locale, slug_es and @current_friendly_id set to "juan-fulano"
  2. friendly_id is set in :en locale with supplied base text "John Doe", slug_en set to "john-doe"
  3. #set_slug invoked again during final #save! call
  4. #should_generate_new_friendly_id compares normalized name ("juan-fulano") with current_friendly_id - they match, so returns false.
  5. Save continues without overwriting slug_en

Now, however, the last two steps go:

  • #should_generate_new_friendly_id compares normalized name ("juan-fulano") with current locale slug value, "john-doe". They don't match, so it returns true.
  • #set_slug overwrites the slug_en column with a newly-generated slug based on the record name, setting it back to "juan-fulano"

I'm not really sure how to fix this, or if it even can be fixed. Because the SimpleI18n module doesn't do translation, and has no way of keeping track of base texts for different locales over multiple model loads, even if we fix this particular test failure so that slug_en isn't overwritten, the next time the model is loaded and saved in the :en locale, it'll get splatted for sure.

So. A pull request with some fixes, but also a breakage and a conundrum. Sorry about this - I'd be glad to hear your thoughts...

Cheers,
Simon

Simon Coffey added 2 commits November 5, 2012 15:18
Modify slug regeneration test so that record should not have to be saved
twice for its slug to be regenerated, and added failing tests for the
expected behaviour of #should_generate_new_friendly_id?
The #should_generate_new_friendly_id? predicate, called as part of the
before_validation callback #set_slug, accesses the current_friendly_id
attribute. However this is only set in a before_save callback, meaning
it is only present if a held instance of a model has previously been
saved.

The predicate's comments indicated that it checked both this value and
the current slug column's value against a regenerated slug base, to see
if a new slug was needed. However it only checked one -
current_friendly_id if set, otherwise the current slug column value.

Fixing both of the above demonstrated that current_friendly_id was in
fact not needed, however, as when forced to be set before #set_slug was
called, it was always identical to the current slug column.

This commit removes current_friendly_id completely, and simplifies the
predicate's check to compare the slug base with only the existing slug
column value.

All tests pass except one in SimpleI18n, which appears to rely on the
earlier behaviour. This will be addressed in the next commit.
@parndt
Copy link
Collaborator

parndt commented Dec 6, 2012

Wow! Thanks. Sounds like callback hell alright!

So, @urbanautomaton in your estimation does your patch create any new problems?

@xymbol
Copy link
Collaborator

xymbol commented Dec 6, 2012

@urbanautomaton 👍 That logic needs to be consistent. And thanks for getting the Peugeot brand right. ;-)

@urbanautomaton
Copy link
Contributor Author

@parndt, well, all the other tests pass, plus the new ones, and we've been using this patch in production since shortly after I submitted the pull request without observing issues. That's not to say there aren't any, obviously - the logic in related areas is still somewhat opaque, but I think the behaviour of #should_generate_new_friendly_id? is now at least internally consistent.

Really not sure what to do about Simplei18n though - I just don't think the behaviour described by the failing test is achievable.

@parndt
Copy link
Collaborator

parndt commented Apr 16, 2013

@norman I'm happy for this to be merged if you are.

@norman
Copy link
Owner

norman commented Apr 16, 2013

@parndt sure, we'll still need to address the failing test in SimpleI18, but let's move forward and get this into master so it goes out in 4.1.

norman added a commit that referenced this pull request Apr 16, 2013
@norman norman merged commit d59c38a into norman:master Apr 16, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants