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 compatibility between Mongoid::Paranoia and Mongoid::Slug #165

Merged
merged 2 commits into from
May 18, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# CHANGELOG

* Improve support for Mongoid::Paranoia (johnnyshields - #165)

## 3.2.2

## Bugfixes
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.mongoid4
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
source 'https://rubygems.org'

gem 'mongoid', '~> 4.0.0.beta1'
gem 'mongoid-paranoia', '>= 1.0.0.beta1'
gem 'mongoid-paranoia', github: 'simi/mongoid-paranoia'

gemspec
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,36 @@ unique = Mongoid::Slug::UniqueSlug.new(Book.new).find_unique(title)
# return some representation of unique
```


Mongoid::Paranoia Support
-------------------------

The [Mongoid::Paranoia](http://github.com/simi/mongoid-paranoia) gem adds "soft-destroy" functionality to Mongoid documents.
Mongoid::Slug contains special handling for Mongoid::Paranoia:
- When destroying a paranoid document, the slug will be unset from the database.
- When restoring a paranoid document, the slug will be rebuilt. Note that the new slug may not match the old one.
- When resaving a destroyed paranoid document, the slug will remain unset in the database.
- For indexing purposes, sparse unique indexes are used. The sparse condition will ignore any destroyed paranoid documents, since their slug is not set in database.

```ruby
class Entity
include Mongoid::Document
include Mongoid::Slug
include Mongoid::Paranoia
end
```

The following variants of Mongoid Paranoia are officially supported:
* Mongoid 3 built-in Mongoid::Paranoia
* Mongoid 4 gem http://github.com/simi/mongoid-paranoia

Mongoid 4 gem "mongoid-paranoia" (http://github.com/haihappen/mongoid-paranoia)
is not officially supported but should also work.


References
----------

[1]: https://github.com/rsl/stringex/
[2]: https://secure.travis-ci.org/hakanensari/mongoid-slug.png
[3]: http://travis-ci.org/hakanensari/mongoid-slug
Expand Down
68 changes: 64 additions & 4 deletions lib/mongoid/slug.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ def slug(*fields, &block)
else
set_callback :save, :before, :build_slug, :if => :slug_should_be_rebuilt?
end

# If paranoid document:
# - include shim to add callbacks for restore method
# - unset the slugs on destroy
# - recreate the slug on restore
# - force reset the slug when saving a destroyed paranoid document, to ensure it stays unset in the database
if is_paranoid_doc?
self.send(:include, Mongoid::Slug::Paranoia) unless self.respond_to?(:before_restore)
set_callback :destroy, :after, :unset_slug!
set_callback :restore, :before, :set_slug!
set_callback :save, :before, :reset_slug!, :if => :paranoid_deleted?
set_callback :save, :after, :clear_slug!, :if => :paranoid_deleted?
end
end

def look_like_slugs?(*args)
Expand Down Expand Up @@ -124,6 +137,16 @@ def queryable
scope_stack.last || Criteria.new(self) # Use Mongoid::Slug::Criteria for slugged documents.
end

# Indicates whether or not the document includes Mongoid::Paranoia
#
# This can be replaced with .paranoid? method once the following PRs are merged:
# - https://github.com/simi/mongoid-paranoia/pull/19
# - https://github.com/haihappen/mongoid-paranoia/pull/3
#
# @return [ Array<Document>, Document ] Whether the document is paranoid
def is_paranoid_doc?
!!(defined?(::Mongoid::Paranoia) && self < ::Mongoid::Paranoia)
end
end

# Builds a new slug.
Expand All @@ -135,18 +158,18 @@ def build_slug
orig_locale = I18n.locale
all_locales.each do |target_locale|
I18n.locale = target_locale
set_slug
apply_slug
end
ensure
I18n.locale = orig_locale
end
else
set_slug
apply_slug
end
true
end

def set_slug
def apply_slug
_new_slug = find_unique_slug

#skip slug generation and use Mongoid id
Expand All @@ -163,6 +186,35 @@ def set_slug
end
end

# Builds slug then atomically sets it in the database.
# This is used when working with the Mongoid::Paranoia restore callback.
#
# This method is adapted to use the :set method variants from both
# Mongoid 3 (two args) and Mongoid 4 (hash arg)
def set_slug!
build_slug
self.method(:set).arity == 1 ? set({_slugs: self._slugs}) : set(:_slugs, self._slugs)
end

# Atomically unsets the slug field in the database. It is important to unset
# the field for the sparse index on slugs.
#
# This also resets the in-memory value of the slug field to its default (empty array)
def unset_slug!
unset(:_slugs)
clear_slug!
end

# Rolls back the slug value from the Mongoid changeset.
def reset_slug!
self.reset__slugs!
end

# Sets the slug to its default value.
def clear_slug!
self._slugs = []
end

# Finds a unique slug, were specified string used to generate a slug.
#
# Returned slug will the same as the specified string when there are no
Expand All @@ -175,7 +227,15 @@ def find_unique_slug

# @return [Boolean] Whether the slug requires to be rebuilt
def slug_should_be_rebuilt?
new_record? or _slugs_changed? or slugged_attributes_changed?
(new_record? or _slugs_changed? or slugged_attributes_changed?) and !paranoid_deleted?
end

# Indicates whether or not the document has been deleted in paranoid fashion
# Always returns false if the document is not paranoid
#
# @return [Boolean] Whether or not the document has been deleted in paranoid fashion
def paranoid_deleted?
!!(self.class.is_paranoid_doc? and self.deleted_at != nil)
end

def slugged_attributes_changed?
Expand Down
22 changes: 22 additions & 0 deletions lib/mongoid/slug/paranoia.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module Mongoid
module Slug

# Lightweight compatibility shim which adds the :restore callback to
# older versions of Mongoid::Paranoia
module Paranoia
extend ActiveSupport::Concern

included do

define_model_callbacks :restore

def restore_with_callbacks
run_callbacks(:restore) do
restore_without_callbacks
end
end
alias_method_chain :restore, :callbacks
end
end
end
end
1 change: 1 addition & 0 deletions lib/mongoid_slug.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
require 'stringex'
require 'mongoid/slug'
require 'mongoid/slug/criteria'
require 'mongoid/slug/paranoia'
require 'mongoid/slug/unique_slug'
require 'mongoid/slug/slug_id_strategy'
8 changes: 8 additions & 0 deletions spec/models/paranoid_permanent.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class ParanoidPermanent
include Mongoid::Document
include Mongoid::Paranoia
include Mongoid::Slug

field :title
slug :title, permanent: true
end
169 changes: 169 additions & 0 deletions spec/mongoid/paranoia_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#encoding: utf-8
require "spec_helper"

describe "Mongoid::Paranoia with Mongoid::Slug" do

let(:paranoid_doc) { ParanoidDocument.create!(:title => "slug") }
let(:paranoid_doc_2) { ParanoidDocument.create!(:title => "slug") }
let(:paranoid_perm) { ParanoidPermanent.create!(:title => "slug") }
let(:paranoid_perm_2) { ParanoidPermanent.create!(:title => "slug") }
let(:non_paranoid_doc){ Article.create!(:title => "slug") }
subject{ paranoid_doc }

describe ".paranoid?" do

context "when Mongoid::Paranoia is included" do
subject { paranoid_doc.class }
its(:is_paranoid_doc?){ should be_true }
end

context "when Mongoid::Paranoia not included" do
subject { non_paranoid_doc.class }
its(:is_paranoid_doc?){ should be_false }
end
end

describe "#paranoid_deleted?" do

context "when Mongoid::Paranoia is included" do

context "when not destroyed" do
its(:paranoid_deleted?){ should be_false }
end

context "when destroyed" do
before { subject.destroy }
its(:paranoid_deleted?){ should be_true }
end
end

context "when Mongoid::Paranoia not included" do
subject { non_paranoid_doc }
its(:paranoid_deleted?){ should be_false }
end
end

describe "restore callbacks" do

context "when Mongoid::Paranoia is included" do
subject { paranoid_doc.class }
it { should respond_to(:before_restore) }
it { should respond_to(:after_restore) }
end

context "when Mongoid::Paranoia not included" do
it { should_not respond_to(:before_restore) }
it { should_not respond_to(:after_restore) }
end
end

describe "index" do
before { ParanoidDocument.create_indexes }
after { ParanoidDocument.remove_indexes }
subject { ParanoidDocument }

it_should_behave_like "has an index", { _slugs: 1 }, { unique: true, sparse: true }
end

shared_examples_for "paranoid slugs" do

context "querying" do

it "returns paranoid_doc for correct slug" do
subject.class.find(subject.slug).should eq(subject)
end
end

context "delete (callbacks not fired)" do

before { subject.delete }

it "retains slug value" do
subject.slug.should eq "slug"
subject.class.unscoped.find("slug").should eq subject
end
end

context "destroy" do

before { subject.destroy }

it "unsets slug value when destroyed" do
subject._slugs.should eq []
subject.slug.should be_nil
end

it "persists the removed slug" do
subject.reload._slugs.should eq []
subject.reload.slug.should be_nil
end

it "persists the removed slug in the database" do
subject.class.unscoped.exists(_slugs: false).first.should eq subject
expect{subject.class.unscoped.find("slug")}.to raise_error(Mongoid::Errors::DocumentNotFound)
end

context "when saving the doc again" do

before { subject.save }

it "should have the default slug value" do
subject._slugs.should eq []
subject.slug.should be_nil
end

it "the slug remains unset in the database" do
subject.class.unscoped.exists(_slugs: false).first.should eq subject
expect{subject.class.unscoped.find("slug")}.to raise_error(Mongoid::Errors::DocumentNotFound)
end
end
end

context "restore" do

before do
subject.destroy
subject.restore
end

it "resets slug value when restored" do
subject.slug.should eq "slug"
subject.reload.slug.should eq "slug"
end
end

context "multiple documents" do

it "new documents should be able to use the slug of destroyed documents" do
subject.slug.should eq "slug"
subject.destroy
subject.reload.slug.should be_nil
other_doc.slug.should eq "slug"
subject.restore
subject.slug.should eq "slug-1"
subject.reload.slug.should eq "slug-1"
end

it "should allow multiple documents to be destroyed without index conflict" do
subject.slug.should eq "slug"
subject.destroy
subject.reload.slug.should be_nil
other_doc.slug.should eq "slug"
other_doc.destroy
other_doc.reload.slug.should be_nil
end
end
end

context "non-permanent slug" do
subject { paranoid_doc }
let(:other_doc) { paranoid_doc_2 }
it_behaves_like "paranoid slugs"
end

context "permanent slug" do
subject { paranoid_perm }
let(:other_doc) { paranoid_perm_2 }
it_behaves_like "paranoid slugs"
end
end
20 changes: 0 additions & 20 deletions spec/mongoid/slug_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1018,25 +1018,5 @@ module Mongoid
end

end

context "Mongoid paranoia with mongoid slug model" do

let(:paranoid_doc) {ParanoidDocument.create!(:title => "slug")}

it "returns paranoid_doc for correct slug" do
ParanoidDocument.find(paranoid_doc.slug).should eq(paranoid_doc)
end

it "raises for deleted slug" do
paranoid_doc.delete
expect{ParanoidDocument.find(paranoid_doc.slug)}.to raise_error(Mongoid::Errors::DocumentNotFound)
end

it "returns paranoid_doc for correct restored slug" do
paranoid_doc.delete
ParanoidDocument.deleted.first.restore
ParanoidDocument.find(paranoid_doc.slug).should eq(paranoid_doc)
end
end
end
end