pseudocephalopod 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -1
- data/Gemfile +11 -2
- data/README.md +1 -1
- data/Rakefile +32 -6
- data/lib/generators/pseudocephalopod/slug_migration/slug_migration_generator.rb +1 -1
- data/lib/generators/pseudocephalopod/slug_migration/templates/{migration.rb → migration.erb} +0 -0
- data/lib/generators/pseudocephalopod/slugs/slugs_generator.rb +1 -1
- data/lib/generators/pseudocephalopod/slugs/templates/{migration.rb → migration.erb} +0 -0
- data/lib/pseudocephalopod/active_record_methods.rb +28 -25
- data/lib/pseudocephalopod/caching.rb +35 -2
- data/lib/pseudocephalopod/finders.rb +1 -1
- data/lib/pseudocephalopod/memory_cache.rb +12 -6
- data/lib/pseudocephalopod/slug.rb +4 -0
- data/lib/pseudocephalopod/slug_history.rb +7 -6
- data/lib/pseudocephalopod/version.rb +1 -1
- data/lib/pseudocephalopod.rb +5 -1
- data/metrics/.gitignore +0 -0
- data/test/caching_test.rb +56 -39
- data/test/helper.rb +21 -0
- data/test/is_sluggable_test.rb +130 -72
- data/test/model_definitions.rb +2 -11
- data/test/pseudocephalopod_test.rb +15 -1
- data/test/slug_history_test.rb +60 -13
- metadata +18 -17
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -4,6 +4,15 @@ source :gemcutter
|
|
4
4
|
gem "activerecord", "= 3.0.0.beta2"
|
5
5
|
gem "reversible_data"
|
6
6
|
gem "uuid"
|
7
|
-
gem "shoulda"
|
8
7
|
gem "sqlite3-ruby", :require => "sqlite3"
|
9
|
-
|
8
|
+
|
9
|
+
gem "shoulda", :require => nil
|
10
|
+
gem "redgreen", :require => nil if RUBY_VERSION < "1.9"
|
11
|
+
gem "rcov", :require => nil
|
12
|
+
gem "reek", :require => nil
|
13
|
+
gem "roodi", :require => nil
|
14
|
+
gem "flay", :require => nil
|
15
|
+
gem "flog", :require => nil
|
16
|
+
gem "rake", :require => nil
|
17
|
+
gem "Saikuro", :require => nil
|
18
|
+
gem "jeweler", :require => nil
|
data/README.md
CHANGED
data/Rakefile
CHANGED
@@ -1,4 +1,8 @@
|
|
1
1
|
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.setup
|
4
|
+
Bundler.require
|
5
|
+
|
2
6
|
require 'rake'
|
3
7
|
|
4
8
|
require File.expand_path('../lib/pseudocephalopod/version', __FILE__)
|
@@ -33,27 +37,49 @@ begin
|
|
33
37
|
require 'rcov/rcovtask'
|
34
38
|
Rcov::RcovTask.new do |test|
|
35
39
|
test.libs << 'test'
|
36
|
-
test.pattern = 'test
|
40
|
+
test.pattern = 'test/**/*_test.rb'
|
37
41
|
test.verbose = true
|
42
|
+
test.rcov_opts << '--exclude \.bundle-cache --exclude gems-switcher'
|
43
|
+
test.output_dir = "metrics/coverage"
|
38
44
|
end
|
39
45
|
rescue LoadError
|
40
46
|
task :rcov do
|
41
|
-
abort "
|
47
|
+
abort "Rcov isn't installed, please run via bundle exec after bundle installing"
|
42
48
|
end
|
43
49
|
end
|
44
50
|
|
51
|
+
task :metrics => [:rcov, :saikuro, :reek, :flay, :flog, :roodi]
|
52
|
+
|
45
53
|
task :test => :check_dependencies
|
46
54
|
|
55
|
+
task :flog do
|
56
|
+
system "flog lib"
|
57
|
+
end
|
58
|
+
|
59
|
+
task :saikuro do
|
60
|
+
system "rm -rf metrics/saikuro && mkdir -p metrics/saikuro && saikuro -c -t -i lib/ -y 0 -w 11 -e 16 -o metrics/saikuro/"
|
61
|
+
end
|
62
|
+
|
63
|
+
begin
|
64
|
+
require 'flay'
|
65
|
+
require 'flay_task'
|
66
|
+
FlayTask.new
|
67
|
+
rescue LoadError
|
68
|
+
task :flay do
|
69
|
+
abort "Flay isn't installed, please run via bundle exec after bundle installing"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
47
73
|
begin
|
48
|
-
require 'reek/
|
49
|
-
Reek::
|
74
|
+
require 'reek/rake/task'
|
75
|
+
Reek::Rake::Task.new do |t|
|
50
76
|
t.fail_on_error = true
|
51
77
|
t.verbose = false
|
52
78
|
t.source_files = 'lib/**/*.rb'
|
53
79
|
end
|
54
80
|
rescue LoadError
|
55
81
|
task :reek do
|
56
|
-
abort "Reek
|
82
|
+
abort "Reek isn't installed, please run via bundle exec after bundle installing"
|
57
83
|
end
|
58
84
|
end
|
59
85
|
|
@@ -65,7 +91,7 @@ begin
|
|
65
91
|
end
|
66
92
|
rescue LoadError
|
67
93
|
task :roodi do
|
68
|
-
abort "Roodi
|
94
|
+
abort "Roodi isn't installed, please run via bundle exec after bundle installing"
|
69
95
|
end
|
70
96
|
end
|
71
97
|
|
data/lib/generators/pseudocephalopod/slug_migration/templates/{migration.rb → migration.erb}
RENAMED
File without changes
|
File without changes
|
@@ -1,27 +1,21 @@
|
|
1
1
|
module Pseudocephalopod
|
2
2
|
module ActiveRecordMethods
|
3
|
+
AR_CLASS_ATTRIBUTE_NAMES = %w(cached_slug_column slug_source slug_convertor_proc default_uuid_slug use_slug_history sync_slugs slug_scope use_slug_cache use_slug_to_param).map(&:to_sym)
|
3
4
|
|
4
5
|
def is_sluggable(source = :name, options = {})
|
6
|
+
options.symbolize_keys!
|
7
|
+
class_attribute *AR_CLASS_ATTRIBUTE_NAMES
|
8
|
+
attr_accessor :found_via_slug
|
9
|
+
# Load extensions
|
5
10
|
extend ClassMethods
|
6
11
|
include InstanceMethods
|
7
12
|
extend Pseudocephalopod::Scopes
|
8
13
|
extend Pseudocephalopod::Finders
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# Set attribute values
|
15
|
-
set_slug_convertor options[:convertor]
|
16
|
-
self.slug_source = source.to_sym
|
17
|
-
self.cached_slug_column = (options[:slug_column] || :cached_slug).to_sym
|
18
|
-
self.default_uuid_slug = !!options.fetch(:uuid, true)
|
19
|
-
self.store_slug_history = !!options.fetch(:history, true)
|
20
|
-
self.sync_slugs = !!options.fetch(:sync, true)
|
21
|
-
self.slug_scope = options[:slug_scope]
|
22
|
-
include Pseudocephalopod::Caching if !!options.fetch(:use_cache, true)
|
23
|
-
alias_method :to_param, :to_slug if !!options.fetch(:to_param, true)
|
24
|
-
include Pseudocephalopod::SlugHistory if self.store_slug_history
|
14
|
+
self.slug_source = source.to_sym
|
15
|
+
set_slug_options options
|
16
|
+
alias_method :to_param, :to_slug if use_slug_to_param
|
17
|
+
include Pseudocephalopod::SlugHistory if use_slug_history
|
18
|
+
include Pseudocephalopod::Caching if use_slug_cache
|
25
19
|
before_validation :autogenerate_slug
|
26
20
|
end
|
27
21
|
|
@@ -33,7 +27,7 @@ module Pseudocephalopod
|
|
33
27
|
|
34
28
|
def generate_slug
|
35
29
|
slug_value = send(self.slug_source)
|
36
|
-
slug_value = self.
|
30
|
+
slug_value = self.slug_convertor_proc.call(slug_value) if slug_value.present?
|
37
31
|
if slug_value.present?
|
38
32
|
scope = self.class.other_than(self).slug_scope_relation(self)
|
39
33
|
slug_value = Pseudocephalopod.next_value(scope, slug_value)
|
@@ -88,18 +82,27 @@ module Pseudocephalopod
|
|
88
82
|
self.slug_scope.present?
|
89
83
|
end
|
90
84
|
|
85
|
+
def set_slug_options(options)
|
86
|
+
set_slug_convertor options[:convertor]
|
87
|
+
self.cached_slug_column = (options[:slug_column] || :cached_slug).to_sym
|
88
|
+
self.slug_scope = options[:scope]
|
89
|
+
self.default_uuid_slug = !!options.fetch(:uuid, true)
|
90
|
+
self.sync_slugs = !!options.fetch(:sync, true)
|
91
|
+
self.use_slug_cache = !!options.fetch(:use_cache, true)
|
92
|
+
self.use_slug_to_param = !!options.fetch(:to_param, true)
|
93
|
+
self.use_slug_history = !!options.fetch(:history, Pseudocephalopod::Slug.usable?)
|
94
|
+
end
|
95
|
+
|
91
96
|
def set_slug_convertor(convertor)
|
92
97
|
if convertor.present?
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
self.slug_source_convertor = convertor
|
98
|
+
unless convertor.respond_to?(:call)
|
99
|
+
convertor_key = convertor.to_sym
|
100
|
+
convertor = proc { |r| r.try(convertor_key) }
|
97
101
|
end
|
102
|
+
self.slug_convertor_proc = convertor
|
98
103
|
else
|
99
|
-
|
100
|
-
|
101
|
-
else
|
102
|
-
self.slug_source_convertor = proc { |r| ActiveSupport::Multibyte::Chars.new(r.to_s).parameterize }
|
104
|
+
self.slug_convertor_proc = proc do |slug|
|
105
|
+
slug.respond_to?(:to_url) ? slug.to_url : ActiveSupport::Multibyte::Chars.new(slug.to_s).parameterize
|
103
106
|
end
|
104
107
|
end
|
105
108
|
end
|
@@ -1,7 +1,17 @@
|
|
1
1
|
require 'digest/sha2'
|
2
2
|
|
3
3
|
module Pseudocephalopod
|
4
|
+
# Mixin for adding simple caching support to models using pseudocephalod.
|
5
|
+
# Usually included by passing the :cache option as true (by default it is
|
6
|
+
# true, you can disable by passing :cache as false or nil).
|
4
7
|
module Caching
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :cache_expires_in
|
11
|
+
end
|
12
|
+
|
13
|
+
# Cache for 10 minutes by default.
|
14
|
+
self.cache_expires_in = 600
|
5
15
|
|
6
16
|
def self.included(parent)
|
7
17
|
parent.extend ClassMethods
|
@@ -14,19 +24,30 @@ module Pseudocephalopod
|
|
14
24
|
end
|
15
25
|
|
16
26
|
module InstanceMethods
|
27
|
+
# Automatically called in after_save, will cache this records id
|
28
|
+
# with to match the current records slug / scope
|
17
29
|
def globally_cache_slug
|
18
30
|
return unless send(:"#{self.cached_slug_column}_changed?")
|
19
31
|
value = self.to_slug
|
20
32
|
self.class.cache_slug_lookup!(value, self) if value.present?
|
21
|
-
unless
|
33
|
+
unless use_slug_history
|
22
34
|
value = send(:"#{self.cached_slug_column}_was")
|
23
35
|
self.class.cache_slug_lookup!(value, nil)
|
24
36
|
end
|
25
37
|
end
|
38
|
+
|
39
|
+
# Wraps remove_slug_history! to remove each of the slugs
|
40
|
+
# recording in this models slug history.
|
41
|
+
def remove_slug_history!
|
42
|
+
previous_slugs.each { |s| self.class.cache_slug_lookup!(s, nil) }
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
26
46
|
end
|
27
47
|
|
28
48
|
module ClassMethods
|
29
49
|
|
50
|
+
# Wraps find_using_slug to look in the cache.
|
30
51
|
def find_using_slug(slug, options = {})
|
31
52
|
# First, attempt to load an id and then record from the cache.
|
32
53
|
if (cached_id = lookup_cached_id_from_slug(slug)).present?
|
@@ -38,6 +59,7 @@ module Pseudocephalopod
|
|
38
59
|
end
|
39
60
|
end
|
40
61
|
|
62
|
+
# Returns a slug cache key for a given slug.
|
41
63
|
def slug_cache_key(slug)
|
42
64
|
[Pseudocephalopod.cache_key_prefix, slug_scope_key(Digest::SHA256.hexdigest(slug.to_s.strip))].compact.join("/")
|
43
65
|
end
|
@@ -46,8 +68,19 @@ module Pseudocephalopod
|
|
46
68
|
lookup_cached_id_from_slug(slug).present?
|
47
69
|
end
|
48
70
|
|
71
|
+
# Modify the cache for a given slug. If record is nil, it will
|
72
|
+
# delete the item from the slug cache, otherwise it will store
|
73
|
+
# the records id.
|
49
74
|
def cache_slug_lookup!(slug, record)
|
50
|
-
|
75
|
+
return if Pseudocephalopod.cache.blank?
|
76
|
+
cache = Pseudocephalopod.cache
|
77
|
+
key = slug_cache_key(slug)
|
78
|
+
# Set an expires in option for caching.
|
79
|
+
caching_options = Hash.new.tap do |hash|
|
80
|
+
expiry = Pseudocephalopod::Caching.cache_expires_in
|
81
|
+
hash[:expires_in] = expiry.to_i if expiry.present?
|
82
|
+
end
|
83
|
+
record.nil? ? cache.delete(key) : cache.write(key, record.id, hash)
|
51
84
|
end
|
52
85
|
|
53
86
|
protected
|
@@ -6,7 +6,7 @@ module Pseudocephalopod
|
|
6
6
|
value = nil
|
7
7
|
value ||= find_by_id(slug.to_i, options) if slug =~ /\A\d+\Z/
|
8
8
|
value ||= with_cached_slug(slug).first(options)
|
9
|
-
value ||= find_using_slug_history(slug, options) if
|
9
|
+
value ||= find_using_slug_history(slug, options) if use_slug_history
|
10
10
|
value.found_via_slug = slug if value.present?
|
11
11
|
value
|
12
12
|
end
|
@@ -1,18 +1,24 @@
|
|
1
1
|
module Pseudocephalopod
|
2
2
|
class MemoryCache
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
def self.write(key, value)
|
7
|
-
@@cache[key.to_s] = value
|
4
|
+
def self.write(key, value, options = {})
|
5
|
+
cache[key.to_s] = value
|
8
6
|
end
|
9
7
|
|
10
8
|
def self.read(key)
|
11
|
-
|
9
|
+
cache[key.to_s]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.delete(key)
|
13
|
+
cache.delete key.to_s
|
12
14
|
end
|
13
15
|
|
14
16
|
def self.reset!
|
15
|
-
|
17
|
+
@cache = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.cache
|
21
|
+
@cache ||= {}
|
16
22
|
end
|
17
23
|
|
18
24
|
end
|
@@ -16,18 +16,19 @@ module Pseudocephalopod
|
|
16
16
|
Pseudocephalopod.previous_slugs_for(self)
|
17
17
|
end
|
18
18
|
|
19
|
+
def remove_slug_history!
|
20
|
+
Pseudocephalopod.remove_slug_history_for(self)
|
21
|
+
end
|
22
|
+
|
19
23
|
protected
|
20
24
|
|
21
25
|
def record_slug_changes
|
22
|
-
|
23
|
-
|
26
|
+
slug_column = self.cached_slug_column
|
27
|
+
return unless send(:"#{slug_column}_changed?")
|
28
|
+
value = send(:"#{slug_column}_was")
|
24
29
|
Pseudocephalopod.record_slug(self, value) if value.present?
|
25
30
|
end
|
26
31
|
|
27
|
-
def remove_slug_history
|
28
|
-
Pseudocephalopod.remove_slug_history_for(self)
|
29
|
-
end
|
30
|
-
|
31
32
|
end
|
32
33
|
|
33
34
|
module ClassMethods
|
data/lib/pseudocephalopod.rb
CHANGED
data/metrics/.gitignore
ADDED
File without changes
|
data/test/caching_test.rb
CHANGED
@@ -1,60 +1,77 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
class CachingTest < Test::Unit::TestCase
|
4
|
-
with_tables :slugs, :users
|
4
|
+
with_tables :slugs, :users do
|
5
5
|
|
6
6
|
setup { Pseudocephalopod::MemoryCache.reset! }
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
8
|
+
context 'with the default slug setup' do
|
9
|
+
|
10
|
+
setup { setup_slugs! }
|
11
|
+
|
12
|
+
should 'store a cache automatically after finding it' do
|
13
|
+
assert_has_no_cache_for "bob"
|
14
|
+
u = User.create :name => "Bob"
|
15
|
+
assert_has_cache_for "bob"
|
16
|
+
Pseudocephalopod::MemoryCache.reset!
|
17
|
+
assert_has_no_cache_for "bob"
|
18
|
+
assert_same_as_slug u, "bob"
|
19
|
+
assert_has_cache_for "bob"
|
20
|
+
assert_same_as_slug u, "bob"
|
21
|
+
end
|
22
|
+
|
23
|
+
should 'automatically keep cache entries for history by default' do
|
24
|
+
setup_slugs!
|
25
|
+
assert_has_no_cache_for "bob"
|
26
|
+
assert_has_no_cache_for "red"
|
27
|
+
assert_has_no_cache_for "sam"
|
28
|
+
user = User.create :name => "bob"
|
29
|
+
assert_has_cache_for "bob"
|
30
|
+
assert_has_no_cache_for "red"
|
31
|
+
assert_has_no_cache_for "sam"
|
32
|
+
user.update_attributes :name => "Red"
|
33
|
+
assert_has_cache_for "bob"
|
34
|
+
assert_has_cache_for "red"
|
35
|
+
assert_has_no_cache_for "sam"
|
36
|
+
user.update_attributes :name => "Sam"
|
37
|
+
assert_has_cache_for "bob"
|
38
|
+
assert_has_cache_for "red"
|
39
|
+
assert_has_cache_for "sam"
|
40
|
+
end
|
41
|
+
|
18
42
|
end
|
19
43
|
|
20
44
|
should 'remove cache on models without history' do
|
21
|
-
|
22
|
-
u =
|
23
|
-
|
24
|
-
|
25
|
-
|
45
|
+
setup_slugs! :history => false
|
46
|
+
u = User.create :name => "Bob"
|
47
|
+
assert_has_cache_for "bob"
|
48
|
+
assert_has_no_cache_for "red"
|
49
|
+
assert_has_no_cache_for "sam"
|
26
50
|
u.update_attributes :name => "Red"
|
27
51
|
u.update_attributes :name => "Sam"
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
should 'automatically keep cache entries for history by default' do
|
34
|
-
assert !User.has_cache_for_slug?("bob")
|
35
|
-
assert !User.has_cache_for_slug?("red")
|
36
|
-
assert !User.has_cache_for_slug?("sam")
|
37
|
-
user = User.create :name => "bob"
|
38
|
-
assert User.has_cache_for_slug?("bob")
|
39
|
-
assert !User.has_cache_for_slug?("red")
|
40
|
-
assert !User.has_cache_for_slug?("sam")
|
41
|
-
user.update_attributes :name => "Red"
|
42
|
-
assert User.has_cache_for_slug?("bob")
|
43
|
-
assert User.has_cache_for_slug?("red")
|
44
|
-
assert !User.has_cache_for_slug?("sam")
|
45
|
-
user.update_attributes :name => "Sam"
|
46
|
-
assert User.has_cache_for_slug?("bob")
|
47
|
-
assert User.has_cache_for_slug?("red")
|
48
|
-
assert User.has_cache_for_slug?("sam")
|
52
|
+
assert_has_no_cache_for "bob"
|
53
|
+
assert_has_no_cache_for "red"
|
54
|
+
assert_has_cache_for "sam"
|
49
55
|
end
|
50
56
|
|
51
57
|
should 'allow you to disable caching' do
|
52
|
-
|
53
|
-
assert !
|
58
|
+
setup_slugs! :use_cache => false
|
59
|
+
assert !User.respond_to?(:has_cache_for_slug?)
|
54
60
|
end
|
55
61
|
|
56
62
|
# Ensure we destroy the contents of the cache after each test.
|
57
63
|
teardown { Pseudocephalopod::MemoryCache.reset! }
|
58
64
|
|
59
65
|
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
def assert_has_cache_for(key)
|
70
|
+
assert User.has_cache_for_slug?(key), "User should have a cache entry for #{key.inspect}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def assert_has_no_cache_for(key)
|
74
|
+
assert !User.has_cache_for_slug?(key), "User should not have a cache entry for #{key.inspect}"
|
75
|
+
end
|
76
|
+
|
60
77
|
end
|
data/test/helper.rb
CHANGED
@@ -19,4 +19,25 @@ Pseudocephalopod.cache = Pseudocephalopod::MemoryCache
|
|
19
19
|
|
20
20
|
class Test::Unit::TestCase
|
21
21
|
extend ReversibleData::ShouldaMacros
|
22
|
+
|
23
|
+
def setup_slugs!(*args)
|
24
|
+
options = args.extract_options!
|
25
|
+
field = args.pop || :name
|
26
|
+
User.is_sluggable field, options
|
27
|
+
end
|
28
|
+
|
29
|
+
def assert_same_as_slug(user, slug, options = {})
|
30
|
+
found_user = User.find_using_slug(slug, options)
|
31
|
+
assert_equal user, found_user, "#{slug.inspect} should return #{user.inspect}, got #{found_user.inspect}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def assert_different_to_slug(user, slug, options = {})
|
35
|
+
found_user = User.find_using_slug(slug, options)
|
36
|
+
assert_not_equal user, found_user, "#{slug.inspect} should not return #{user.inspect}, got same record."
|
37
|
+
end
|
38
|
+
|
39
|
+
def assert_none_for_slug(slug)
|
40
|
+
assert User.find_using_slug(slug).blank?, "slug #{slug.inspect} should not return any records."
|
41
|
+
end
|
42
|
+
|
22
43
|
end
|
data/test/is_sluggable_test.rb
CHANGED
@@ -2,87 +2,145 @@ require 'helper'
|
|
2
2
|
require 'digest/md5'
|
3
3
|
|
4
4
|
class IsSluggableTest < Test::Unit::TestCase
|
5
|
-
with_tables :slugs, :users
|
5
|
+
with_tables :slugs, :users do
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
assert_equal "bob", user.to_param
|
10
|
-
assert_equal "bob", user.cached_slug
|
7
|
+
class StringWrapper < String
|
8
|
+
def to_url; "my-demo-slug"; end
|
11
9
|
end
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
11
|
+
context 'with the default slug options' do
|
12
|
+
|
13
|
+
setup { setup_slugs! }
|
14
|
+
|
15
|
+
should 'correctly sluggify a value' do
|
16
|
+
user = User.create(:name => "Bob")
|
17
|
+
assert_equal "bob", user.to_param
|
18
|
+
assert_equal "bob", user.cached_slug
|
19
|
+
end
|
20
|
+
|
21
|
+
should 'generate a uuid in place of a slug' do
|
22
|
+
user = User.create(:name => '')
|
23
|
+
assert user.cached_slug.present?
|
24
|
+
end
|
25
|
+
|
26
|
+
should 'return need to generate a slug when the cahced slug is blank' do
|
27
|
+
user = User.new(:name => "Ninja Stuff")
|
28
|
+
assert user.cached_slug.blank?
|
29
|
+
assert user.should_generate_slug?
|
30
|
+
user.save
|
31
|
+
assert user.cached_slug.present?
|
32
|
+
assert !user.should_generate_slug?
|
33
|
+
user.name = 'Awesome'
|
34
|
+
assert user.should_generate_slug?
|
35
|
+
end
|
36
|
+
|
37
|
+
should "let you find a record by it's id as needed" do
|
38
|
+
user = User.create :name => "Bob"
|
39
|
+
assert_equal user, User.find_using_slug(user.id)
|
40
|
+
assert_equal user, User.find_using_slug(user.id.to_i)
|
41
|
+
end
|
42
|
+
|
43
|
+
should 'return nil for unfound slugs by default' do
|
44
|
+
assert_nil User.find_using_slug("awesome")
|
45
|
+
end
|
46
|
+
|
47
|
+
should 'let you find slugs and raise an exception' do
|
48
|
+
assert_raises(ActiveRecord::RecordNotFound) do
|
49
|
+
User.find_using_slug!("awesome")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
should 'default to generate a uuid' do
|
54
|
+
user = User.create :name => ""
|
55
|
+
assert_match /\A[a-zA-Z0-9]{32}\Z/, user.cached_slug.gsub("-", "")
|
56
|
+
user = User.create
|
57
|
+
assert_match /\A[a-zA-Z0-9]{32}\Z/, user.cached_slug.gsub("-", "")
|
58
|
+
end
|
59
|
+
|
60
|
+
should 'automatically append a sequence to the end of conflicting slugs' do
|
61
|
+
u1 = User.create :name => "ninjas Are awesome"
|
62
|
+
u2 = User.create :name => "Ninjas are awesome"
|
63
|
+
assert_equal "ninjas-are-awesome", u1.to_slug
|
64
|
+
assert_equal "ninjas-are-awesome--1", u2.to_slug
|
65
|
+
end
|
66
|
+
|
67
|
+
should 'let you find out if there is a better way of finding a slug' do
|
68
|
+
user = User.create :name => "Bob"
|
69
|
+
user.update_attributes! :name => "Ralph"
|
70
|
+
assert !User.find_using_slug("ralph").has_better_slug?
|
71
|
+
assert User.find_using_slug("bob").has_better_slug?
|
72
|
+
assert User.find_using_slug(user.id).has_better_slug?
|
73
|
+
end
|
74
|
+
|
64
75
|
end
|
65
76
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
77
|
+
context 'with slug syncing disabled' do
|
78
|
+
setup { setup_slugs! :sync => false }
|
79
|
+
|
80
|
+
should 'let you disable syncing a slug' do
|
81
|
+
user = User.create(:name => "Ninja User")
|
82
|
+
assert !user.should_generate_slug?
|
83
|
+
user.name = "Another User Name"
|
84
|
+
assert !user.should_generate_slug?
|
85
|
+
end
|
86
|
+
|
87
|
+
should 'let you force slug generation' do
|
88
|
+
user = User.create(:name => "Ninja User")
|
89
|
+
assert_equal "ninja-user", user.to_slug
|
90
|
+
user.update_attributes :name => "Test User"
|
91
|
+
assert_equal "ninja-user", user.to_slug
|
92
|
+
user.generate_slug!
|
93
|
+
assert_equal "test-user", user.to_slug
|
94
|
+
user.reload
|
95
|
+
assert_equal "test-user", user.to_slug
|
96
|
+
end
|
97
|
+
|
98
|
+
should 'let you force the update of all slugs' do
|
99
|
+
user_a = User.create(:name => "User A")
|
100
|
+
user_b = User.create(:name => "User B")
|
101
|
+
user_c = User.create(:name => "User C")
|
102
|
+
user_a.update_attributes :name => "User A-1"
|
103
|
+
user_b.update_attributes :name => "User B-1"
|
104
|
+
user_c.update_attributes :name => "User C-1"
|
105
|
+
assert_equal "user-a", user_a.to_slug
|
106
|
+
assert_equal "user-b", user_b.to_slug
|
107
|
+
assert_equal "user-c", user_c.to_slug
|
108
|
+
User.update_all_slugs!
|
109
|
+
user_a.reload
|
110
|
+
user_b.reload
|
111
|
+
user_c.reload
|
112
|
+
assert_equal "user-a-1", user_a.to_slug
|
113
|
+
assert_equal "user-b-1", user_b.to_slug
|
114
|
+
assert_equal "user-c-1", user_c.to_slug
|
115
|
+
end
|
116
|
+
|
71
117
|
end
|
72
118
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
119
|
+
context 'setting slug convertors' do
|
120
|
+
|
121
|
+
should 'let you specify a symbol' do
|
122
|
+
setup_slugs! :convertor => :upcase
|
123
|
+
assert_equal "AWESOME USER", User.create(:name => "Awesome User").to_slug
|
124
|
+
end
|
125
|
+
|
126
|
+
should 'let you specify a proc' do
|
127
|
+
setup_slugs! :convertor => proc { |r| r.reverse.upcase }
|
128
|
+
assert_equal "RESU EMOSEWA", User.create(:name => "Awesome User").to_slug
|
129
|
+
end
|
130
|
+
|
131
|
+
should 'call to_url if available by default' do
|
132
|
+
setup_slugs!
|
133
|
+
original_value = StringWrapper.new("Awesome User")
|
134
|
+
assert_equal "my-demo-slug", User.create(:name => original_value).to_slug
|
135
|
+
end
|
136
|
+
|
78
137
|
end
|
79
138
|
|
80
|
-
should '
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
assert User.find_using_slug(user.id).has_better_slug?
|
139
|
+
should 'set the cached slug to nil if uuid is nil and the source value is blank' do
|
140
|
+
setup_slugs! :uuid => nil
|
141
|
+
record = User.create(:name => "")
|
142
|
+
assert_nil record.cached_slug
|
143
|
+
assert_equal record.id.to_s, record.to_slug
|
86
144
|
end
|
87
145
|
|
88
146
|
end
|
data/test/model_definitions.rb
CHANGED
@@ -11,18 +11,9 @@ ReversibleData.add :slugs do |t|
|
|
11
11
|
t.datetime :created_at
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
u.string :name
|
16
|
-
u.string :cached_slug
|
17
|
-
u.timestamps
|
18
|
-
end
|
19
|
-
|
20
|
-
user.define_model do
|
21
|
-
is_sluggable :name
|
22
|
-
end
|
23
|
-
|
24
|
-
ReversibleData.add :unslugged_users do |u|
|
14
|
+
ReversibleData.add :users do |u|
|
25
15
|
u.string :name
|
16
|
+
u.string :address
|
26
17
|
u.string :cached_slug
|
27
18
|
u.timestamps
|
28
19
|
end
|
@@ -2,6 +2,11 @@ require 'helper'
|
|
2
2
|
|
3
3
|
class PseudocephalopodTest < Test::Unit::TestCase
|
4
4
|
|
5
|
+
class SlugScopeTest
|
6
|
+
cattr_accessor :slug_scope_key
|
7
|
+
self.slug_scope_key = "my-test-scope"
|
8
|
+
end
|
9
|
+
|
5
10
|
should 'return the correct counter versions' do
|
6
11
|
assert_equal 'awesome', Pseudocephalopod.with_counter('awesome')
|
7
12
|
assert_equal 'awesome', Pseudocephalopod.with_counter('awesome', 0)
|
@@ -9,5 +14,14 @@ class PseudocephalopodTest < Test::Unit::TestCase
|
|
9
14
|
assert_equal 'awesome--2', Pseudocephalopod.with_counter('awesome', 2)
|
10
15
|
assert_equal 'awesome--100', Pseudocephalopod.with_counter('awesome', 100)
|
11
16
|
end
|
12
|
-
|
17
|
+
|
18
|
+
should 'correct allow you to slug scope keys' do
|
19
|
+
assert_equal "my-test-scope", Pseudocephalopod.key_for_scope(SlugScopeTest)
|
20
|
+
assert_equal "my-test-scope", Pseudocephalopod.key_for_scope(SlugScopeTest.new)
|
21
|
+
assert_equal "my-test-scope", Pseudocephalopod.key_for_scope("my-test-scope")
|
22
|
+
assert_equal "", Pseudocephalopod.key_for_scope(nil)
|
23
|
+
assert_equal "1", Pseudocephalopod.key_for_scope(1)
|
24
|
+
assert_equal "awesome", Pseudocephalopod.key_for_scope(:awesome)
|
25
|
+
end
|
26
|
+
|
13
27
|
end
|
data/test/slug_history_test.rb
CHANGED
@@ -7,22 +7,69 @@ class FakedModel
|
|
7
7
|
end
|
8
8
|
|
9
9
|
class SlugHistoryTest < Test::Unit::TestCase
|
10
|
-
with_tables :slugs do
|
10
|
+
with_tables :slugs, :users do
|
11
|
+
|
12
|
+
context 'for arbitrary models' do
|
13
|
+
|
14
|
+
setup do
|
15
|
+
@record_a = FakedModel.new(12)
|
16
|
+
@record_b = FakedModel.new(4)
|
17
|
+
Pseudocephalopod.record_slug(@record_a, "awesome")
|
18
|
+
Pseudocephalopod.record_slug(@record_b, "awesome-1")
|
19
|
+
Pseudocephalopod.record_slug(@record_a, "ninjas")
|
20
|
+
end
|
21
|
+
|
22
|
+
should 'let you lookup a given record id easily' do
|
23
|
+
assert Pseudocephalopod.last_known_slug_id(FakedModel, "felafel").blank?
|
24
|
+
assert Pseudocephalopod.last_known_slug_id(FakedModel, "ninjas-2").blank?
|
25
|
+
assert_equal 12, Pseudocephalopod.last_known_slug_id(FakedModel, "awesome")
|
26
|
+
assert_equal 4, Pseudocephalopod.last_known_slug_id(FakedModel, "awesome-1")
|
27
|
+
assert_equal 12, Pseudocephalopod.last_known_slug_id(FakedModel, "ninjas")
|
28
|
+
end
|
29
|
+
|
30
|
+
should 'let you return slug history for a given record'
|
11
31
|
|
12
|
-
setup do
|
13
|
-
@record_a = FakedModel.new(12)
|
14
|
-
@record_b = FakedModel.new(4)
|
15
|
-
Pseudocephalopod.record_slug(@record_a, "awesome")
|
16
|
-
Pseudocephalopod.record_slug(@record_b, "awesome-1")
|
17
|
-
Pseudocephalopod.record_slug(@record_a, "ninjas")
|
18
32
|
end
|
19
33
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
34
|
+
context 'on a specific record' do
|
35
|
+
|
36
|
+
should 'by default record slug history' do
|
37
|
+
setup_slugs!
|
38
|
+
user = User.create :name => "Bob"
|
39
|
+
assert_equal [], user.previous_slugs
|
40
|
+
user.update_attributes! :name => "Sal"
|
41
|
+
user.update_attributes! :name => "Red"
|
42
|
+
user.update_attributes! :name => "Jim"
|
43
|
+
assert_same_as_slug user, "red"
|
44
|
+
assert_same_as_slug user, "sal"
|
45
|
+
assert_same_as_slug user, "bob"
|
46
|
+
assert_same_as_slug user, "jim"
|
47
|
+
end
|
48
|
+
|
49
|
+
should 'let you reset history for a slug' do
|
50
|
+
setup_slugs!
|
51
|
+
user = User.create :name => "Bob"
|
52
|
+
user.update_attributes! :name => "Sal"
|
53
|
+
user.update_attributes! :name => "Red"
|
54
|
+
user.update_attributes! :name => "Jim"
|
55
|
+
assert_equal ["red", "sal", "bob"], user.previous_slugs
|
56
|
+
user.remove_slug_history!
|
57
|
+
assert_equal [], user.previous_slugs
|
58
|
+
assert_none_for_slug "red"
|
59
|
+
assert_none_for_slug "sal"
|
60
|
+
assert_none_for_slug "bob"
|
61
|
+
end
|
62
|
+
|
63
|
+
should 'let you disable recording of slug history' do
|
64
|
+
setup_slugs! :history => false
|
65
|
+
user = User.create(:name => "Bob")
|
66
|
+
assert !user.respond_to?(:previous_slugs)
|
67
|
+
user.update_attributes! :name => "Red"
|
68
|
+
assert_same_as_slug user, "red"
|
69
|
+
assert_different_to_slug user, "bob"
|
70
|
+
assert_none_for_slug "bob"
|
71
|
+
end
|
72
|
+
|
26
73
|
end
|
27
74
|
|
28
75
|
end
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
7
|
+
- 2
|
8
8
|
- 0
|
9
|
-
version: 0.
|
9
|
+
version: 0.2.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Darcy Laycock
|
@@ -14,13 +14,13 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-04-
|
17
|
+
date: 2010-04-24 00:00:00 +08:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: activerecord
|
22
|
-
|
23
|
-
|
22
|
+
type: :runtime
|
23
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
@@ -30,32 +30,32 @@ dependencies:
|
|
30
30
|
- 0
|
31
31
|
- beta2
|
32
32
|
version: 3.0.0.beta2
|
33
|
-
|
34
|
-
|
33
|
+
prerelease: false
|
34
|
+
requirement: *id001
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
36
|
name: shoulda
|
37
|
-
|
38
|
-
|
37
|
+
type: :development
|
38
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
39
39
|
requirements:
|
40
40
|
- - ">="
|
41
41
|
- !ruby/object:Gem::Version
|
42
42
|
segments:
|
43
43
|
- 0
|
44
44
|
version: "0"
|
45
|
-
|
46
|
-
|
45
|
+
prerelease: false
|
46
|
+
requirement: *id002
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: reversible_data
|
49
|
-
|
50
|
-
|
49
|
+
type: :development
|
50
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
segments:
|
55
55
|
- 0
|
56
56
|
version: "0"
|
57
|
-
|
58
|
-
|
57
|
+
prerelease: false
|
58
|
+
requirement: *id003
|
59
59
|
description: Super simple slugs for ActiveRecord 3.0 and higher, with support for slug history
|
60
60
|
email: sutto@sutto.net
|
61
61
|
executables: []
|
@@ -73,9 +73,9 @@ files:
|
|
73
73
|
- README.md
|
74
74
|
- Rakefile
|
75
75
|
- lib/generators/pseudocephalopod/slug_migration/slug_migration_generator.rb
|
76
|
-
- lib/generators/pseudocephalopod/slug_migration/templates/migration.
|
76
|
+
- lib/generators/pseudocephalopod/slug_migration/templates/migration.erb
|
77
77
|
- lib/generators/pseudocephalopod/slugs/slugs_generator.rb
|
78
|
-
- lib/generators/pseudocephalopod/slugs/templates/migration.
|
78
|
+
- lib/generators/pseudocephalopod/slugs/templates/migration.erb
|
79
79
|
- lib/pseudocephalopod.rb
|
80
80
|
- lib/pseudocephalopod/active_record_methods.rb
|
81
81
|
- lib/pseudocephalopod/caching.rb
|
@@ -86,6 +86,7 @@ files:
|
|
86
86
|
- lib/pseudocephalopod/slug.rb
|
87
87
|
- lib/pseudocephalopod/slug_history.rb
|
88
88
|
- lib/pseudocephalopod/version.rb
|
89
|
+
- metrics/.gitignore
|
89
90
|
- test/caching_test.rb
|
90
91
|
- test/helper.rb
|
91
92
|
- test/is_sluggable_test.rb
|