pseudocephalopod 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +13 -0
- data/Gemfile +9 -0
- data/LICENSE +20 -0
- data/README.md +117 -0
- data/Rakefile +82 -0
- data/lib/generators/pseudocephalopod/slug_migration/slug_migration_generator.rb +24 -0
- data/lib/generators/pseudocephalopod/slug_migration/templates/migration.rb +12 -0
- data/lib/generators/pseudocephalopod/slugs/slugs_generator.rb +24 -0
- data/lib/generators/pseudocephalopod/slugs/templates/migration.rb +20 -0
- data/lib/pseudocephalopod/active_record_methods.rb +109 -0
- data/lib/pseudocephalopod/caching.rb +62 -0
- data/lib/pseudocephalopod/finders.rb +19 -0
- data/lib/pseudocephalopod/memory_cache.rb +19 -0
- data/lib/pseudocephalopod/railtie.rb +9 -0
- data/lib/pseudocephalopod/scopes.rb +13 -0
- data/lib/pseudocephalopod/slug.rb +32 -0
- data/lib/pseudocephalopod/slug_history.rb +43 -0
- data/lib/pseudocephalopod/version.rb +8 -0
- data/lib/pseudocephalopod.rb +69 -0
- data/test/caching_test.rb +60 -0
- data/test/helper.rb +22 -0
- data/test/is_sluggable_test.rb +89 -0
- data/test/model_definitions.rb +28 -0
- data/test/pseudocephalopod_test.rb +13 -0
- data/test/slug_history_test.rb +30 -0
- metadata +131 -0
data/.document
ADDED
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Darcy Laycock
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# Pseudocephalopod #
|
2
|
+
|
3
|
+
## About ###
|
4
|
+
|
5
|
+
Pseudocephalopod is a simple slug library for ActiveRecord 3.0 plus.
|
6
|
+
|
7
|
+
It's main features are:
|
8
|
+
|
9
|
+
1. A very simple and tested codebase
|
10
|
+
2. Support for slug history (e.g. if a users slug changes, it will record the old slug)
|
11
|
+
3. Simple defaulting for slugs to UUID's (to avoid showing ID's.)
|
12
|
+
4. Built on ActiveRecord 3.0
|
13
|
+
5. If stringex is installed, uses stringex's transliteration stuff
|
14
|
+
|
15
|
+
Also, it's name is inspired by the Jason Wander series of books which I just happened to be
|
16
|
+
reading when I had the need for this.
|
17
|
+
|
18
|
+
### Why? ###
|
19
|
+
|
20
|
+
I love the idea of friendly\_id, and most of the implementation but it felt bloated
|
21
|
+
to me and my experiences on getting it to work correctly with Rails 3 left a base taste
|
22
|
+
in my mouth / was altogether hacky.
|
23
|
+
|
24
|
+
Pseudocephalopod is very much inspired by friendly id but with a much simpler codebase
|
25
|
+
and built to work on Rails 3 from the start.
|
26
|
+
|
27
|
+
## Usage ##
|
28
|
+
|
29
|
+
Using Pseudocephalopod is simple. In Rails, simply drop this in your gemfile:
|
30
|
+
|
31
|
+
gem 'pseudocephalopod'
|
32
|
+
|
33
|
+
Optionally restricting the version.
|
34
|
+
|
35
|
+
Next, if you wish to use slug history run:
|
36
|
+
|
37
|
+
$ rails generate pseudocephalopod:slugs
|
38
|
+
|
39
|
+
Otherwise, when calling is\_sluggable make sure to include :history => false
|
40
|
+
|
41
|
+
Next, you need to add a cached slug column to your model and add an index. In your migration,
|
42
|
+
you'd usually want something like:
|
43
|
+
|
44
|
+
add_column :users, :cached_slug, :string
|
45
|
+
add_index :users, :cached_slug
|
46
|
+
|
47
|
+
Or, using our build in generator:
|
48
|
+
|
49
|
+
$ rails generate pseudocephalopod:slug_migration Model
|
50
|
+
|
51
|
+
Lastly, in your model, call is\_sluggable:
|
52
|
+
|
53
|
+
class User
|
54
|
+
is_sluggable :name
|
55
|
+
end
|
56
|
+
|
57
|
+
is\_sluggable accepts the source method name as a symbol, and an optional has of options including:
|
58
|
+
|
59
|
+
* _:sync_ - when source column changes, save the result. Defaults to true.
|
60
|
+
* _:convertor_ - a symbol (for a method) or block for how to generate the base slug. Defaults to :to\_url if available, parameterize otherwise.
|
61
|
+
* _:history_ - use slug history (e.g. if the name changes, it records the previous version in a slugs table). Defaults to true
|
62
|
+
* _:uuid_ - If the slug is blank, uses a generated uuid instead. Defaults to true
|
63
|
+
* _:slug\_column_ - the column in which to store the slug. Defaults to _:cached\_slug_
|
64
|
+
* _:to\_param_ - if true (by default), overrides to_param to use the slug
|
65
|
+
* _:use\_cache_ - uses Pseudocephalopod.cache if available to cache any lookups e.g. in memcache.
|
66
|
+
|
67
|
+
Once installed, it provides the following methods:
|
68
|
+
|
69
|
+
### User.find\_using\_slug "some-slug" ###
|
70
|
+
|
71
|
+
Finds a user from a slug (which can be the record's id, it's cached slug or, if enabled, slug history)
|
72
|
+
|
73
|
+
### User.other\_than(record) ###
|
74
|
+
|
75
|
+
Returns a relationship which returns records other than the given.
|
76
|
+
|
77
|
+
### User.with\_cached\_slug(record) ###
|
78
|
+
|
79
|
+
Returns a relationship which returns records with the given cached slug.
|
80
|
+
|
81
|
+
### User#generate\_slug ###
|
82
|
+
|
83
|
+
Forces the generation of a current slug
|
84
|
+
|
85
|
+
### User#generate\_slug! ###
|
86
|
+
|
87
|
+
Forces the generation of a current slug and saves it
|
88
|
+
|
89
|
+
### User#autogenerate\_slug ###
|
90
|
+
|
91
|
+
Generates a slug if not already present.
|
92
|
+
|
93
|
+
### User#has\_better\_slug? ###
|
94
|
+
|
95
|
+
When found via Model.find\_using\_slug, it will return try
|
96
|
+
if there is a better slug available. Intended for use in redirects etc.
|
97
|
+
|
98
|
+
## Working on Pseudocephalopod ##
|
99
|
+
|
100
|
+
To run tests, simply do the following:
|
101
|
+
|
102
|
+
bundle install
|
103
|
+
rake
|
104
|
+
|
105
|
+
And it's ready!
|
106
|
+
|
107
|
+
## Note on Patches/Pull Requests ##
|
108
|
+
|
109
|
+
* Fork the project.
|
110
|
+
* Make your feature addition or bug fix.
|
111
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
112
|
+
* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
113
|
+
* Send me a pull request. Bonus points for topic branches.
|
114
|
+
|
115
|
+
## Copyright ##
|
116
|
+
|
117
|
+
Copyright (c) 2010 Darcy Laycock. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
require File.expand_path('../lib/pseudocephalopod/version', __FILE__)
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.version = Pseudocephalopod::Version::STRING
|
10
|
+
gem.name = "pseudocephalopod"
|
11
|
+
gem.summary = %Q{Super simple slugs for ActiveRecord 3.0 and higher, with support for slug history}
|
12
|
+
gem.description = %Q{Super simple slugs for ActiveRecord 3.0 and higher, with support for slug history}
|
13
|
+
gem.email = "sutto@sutto.net"
|
14
|
+
gem.homepage = "http://github.com/Sutto/pseudocephalopod"
|
15
|
+
gem.authors = ["Darcy Laycock"]
|
16
|
+
gem.add_dependency "activerecord", ">= 3.0.0.beta2"
|
17
|
+
gem.add_development_dependency "shoulda", ">= 0"
|
18
|
+
gem.add_development_dependency "reversible_data"
|
19
|
+
end
|
20
|
+
Jeweler::GemcutterTasks.new
|
21
|
+
rescue LoadError
|
22
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'rake/testtask'
|
26
|
+
Rake::TestTask.new(:test) do |test|
|
27
|
+
test.libs << 'lib' << 'test'
|
28
|
+
test.pattern = 'test/**/*_test.rb'
|
29
|
+
test.verbose = true
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
require 'rcov/rcovtask'
|
34
|
+
Rcov::RcovTask.new do |test|
|
35
|
+
test.libs << 'test'
|
36
|
+
test.pattern = 'test/**/test_*.rb'
|
37
|
+
test.verbose = true
|
38
|
+
end
|
39
|
+
rescue LoadError
|
40
|
+
task :rcov do
|
41
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
task :test => :check_dependencies
|
46
|
+
|
47
|
+
begin
|
48
|
+
require 'reek/adapters/rake_task'
|
49
|
+
Reek::RakeTask.new do |t|
|
50
|
+
t.fail_on_error = true
|
51
|
+
t.verbose = false
|
52
|
+
t.source_files = 'lib/**/*.rb'
|
53
|
+
end
|
54
|
+
rescue LoadError
|
55
|
+
task :reek do
|
56
|
+
abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
begin
|
61
|
+
require 'roodi'
|
62
|
+
require 'roodi_task'
|
63
|
+
RoodiTask.new do |t|
|
64
|
+
t.verbose = false
|
65
|
+
end
|
66
|
+
rescue LoadError
|
67
|
+
task :roodi do
|
68
|
+
abort "Roodi is not available. In order to run roodi, you must: sudo gem install roodi"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
task :default => :test
|
73
|
+
|
74
|
+
require 'rake/rdoctask'
|
75
|
+
Rake::RDocTask.new do |rdoc|
|
76
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
77
|
+
|
78
|
+
rdoc.rdoc_dir = 'rdoc'
|
79
|
+
rdoc.title = "pseudocephalopod #{version}"
|
80
|
+
rdoc.rdoc_files.include('README*')
|
81
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
82
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Pseudocephalopod
|
2
|
+
module Generators
|
3
|
+
class SlugMigrationGenerator < Rails::Generators::NamedBase
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
def self.source_root
|
7
|
+
@_ps_source_root ||= File.expand_path("templates", File.dirname(__FILE__))
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.next_migration_number(dirname) #:nodoc:
|
11
|
+
if ActiveRecord::Base.timestamped_migrations
|
12
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
13
|
+
else
|
14
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_migration_file
|
19
|
+
migration_template "migration.rb", "db/migrate/add_cached_slug_to_#{table_name}.rb"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
|
3
|
+
def self.up
|
4
|
+
add_column <%= table_name.to_sym.inspect %>, :cached_slug, :string
|
5
|
+
add_index <%= table_name.to_sym.inspect %>, :cached_slug
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.down
|
9
|
+
remove_column <%= table_name.to_sym.inspect %>, :cached_slug
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Pseudocephalopod
|
2
|
+
module Generators
|
3
|
+
class SlugsGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
def self.source_root
|
7
|
+
@_ps_source_root ||= File.expand_path("templates", File.dirname(__FILE__))
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.next_migration_number(dirname) #:nodoc:
|
11
|
+
if ActiveRecord::Base.timestamped_migrations
|
12
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
13
|
+
else
|
14
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_migration_file
|
19
|
+
migration_template "migration.rb", "db/migrate/create_pseudocephalopod_slugs.rb"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class CreatePseudocephalopodSlugs < ActiveRecord::Migration
|
2
|
+
|
3
|
+
def self.up
|
4
|
+
create_table :slugs do |t|
|
5
|
+
t.string :scope
|
6
|
+
t.string :slug
|
7
|
+
t.integer :record_id
|
8
|
+
t.datetime :created_at
|
9
|
+
end
|
10
|
+
add_index :slugs, [:scope, :slug]
|
11
|
+
add_index :slugs, [:scope, :record_id]
|
12
|
+
add_index :slugs, [:scope, :slug, :created_at]
|
13
|
+
add_index :slugs, [:scope, :record_id, :created_at]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.down
|
17
|
+
drop_table :slugs
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Pseudocephalopod
|
2
|
+
module ActiveRecordMethods
|
3
|
+
|
4
|
+
def is_sluggable(source = :name, options = {})
|
5
|
+
extend ClassMethods
|
6
|
+
include InstanceMethods
|
7
|
+
extend Pseudocephalopod::Scopes
|
8
|
+
extend Pseudocephalopod::Finders
|
9
|
+
options.symbolize_keys!
|
10
|
+
# Define the attributes
|
11
|
+
class_attribute :cached_slug_column, :slug_source, :slug_source_convertor,
|
12
|
+
:default_uuid_slug, :store_slug_history, :sync_slugs, :slug_scope
|
13
|
+
attr_accessor :found_via_slug
|
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
|
25
|
+
before_validation :autogenerate_slug
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
|
30
|
+
def to_slug
|
31
|
+
cached_slug.present? ? cached_slug : id.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def generate_slug
|
35
|
+
slug_value = send(self.slug_source)
|
36
|
+
slug_value = self.slug_source_convertor.call(slug_value) if slug_value.present?
|
37
|
+
if slug_value.present?
|
38
|
+
scope = self.class.other_than(self).slug_scope_relation(self)
|
39
|
+
slug_value = Pseudocephalopod.next_value(scope, slug_value)
|
40
|
+
write_attribute self.cached_slug_column, slug_value
|
41
|
+
elsif self.default_uuid_slug
|
42
|
+
write_attribute self.cached_slug_column, Pseudocephalopod.generate_uuid_slug
|
43
|
+
else
|
44
|
+
write_attribute self.cached_slug_column, nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def generate_slug!
|
49
|
+
generate_slug
|
50
|
+
save :validate => false
|
51
|
+
end
|
52
|
+
|
53
|
+
def autogenerate_slug
|
54
|
+
generate_slug if should_generate_slug?
|
55
|
+
end
|
56
|
+
|
57
|
+
def should_generate_slug?
|
58
|
+
send(self.cached_slug_column).blank? || (self.sync_slugs && send(:"#{self.slug_source}_changed?"))
|
59
|
+
end
|
60
|
+
|
61
|
+
def has_better_slug?
|
62
|
+
found_via_slug.present? && found_via_slug != to_slug
|
63
|
+
end
|
64
|
+
|
65
|
+
def slug_scope_key(nested_scope = [])
|
66
|
+
self.class.slug_scope_key(nested_scope)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
module ClassMethods
|
72
|
+
|
73
|
+
def update_all_slugs!
|
74
|
+
find_each { |r| r.generate_slug! }
|
75
|
+
end
|
76
|
+
|
77
|
+
def slug_scope_key(nested_scope = [])
|
78
|
+
([table_name, slug_scope] + Array(nested_scope)).flatten.compact.join("|")
|
79
|
+
end
|
80
|
+
|
81
|
+
def slug_scope_relation(record)
|
82
|
+
has_slug_scope? ? where(slug_scope => record.send(slug_scope)) : unscoped
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
def has_slug_scope?
|
88
|
+
self.slug_scope.present?
|
89
|
+
end
|
90
|
+
|
91
|
+
def set_slug_convertor(convertor)
|
92
|
+
if convertor.present?
|
93
|
+
if convertor.is_a?(Symbol)
|
94
|
+
self.slug_source_convertor = proc { |r| r.try(convertor) }
|
95
|
+
elsif convertor.respond_to?(:call)
|
96
|
+
self.slug_source_convertor = convertor
|
97
|
+
end
|
98
|
+
else
|
99
|
+
if "".respond_to?(:to_url)
|
100
|
+
self.slug_source_convertor = proc { |r| r.to_s.to_url }
|
101
|
+
else
|
102
|
+
self.slug_source_convertor = proc { |r| ActiveSupport::Multibyte::Chars.new(r.to_s).parameterize }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
|
3
|
+
module Pseudocephalopod
|
4
|
+
module Caching
|
5
|
+
|
6
|
+
def self.included(parent)
|
7
|
+
parent.extend ClassMethods
|
8
|
+
|
9
|
+
parent.class_eval do
|
10
|
+
include InstanceMethods
|
11
|
+
extend ClassMethods
|
12
|
+
after_save :globally_cache_slug
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module InstanceMethods
|
17
|
+
def globally_cache_slug
|
18
|
+
return unless send(:"#{self.cached_slug_column}_changed?")
|
19
|
+
value = self.to_slug
|
20
|
+
self.class.cache_slug_lookup!(value, self) if value.present?
|
21
|
+
unless store_slug_history
|
22
|
+
value = send(:"#{self.cached_slug_column}_was")
|
23
|
+
self.class.cache_slug_lookup!(value, nil)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
|
30
|
+
def find_using_slug(slug, options = {})
|
31
|
+
# First, attempt to load an id and then record from the cache.
|
32
|
+
if (cached_id = lookup_cached_id_from_slug(slug)).present?
|
33
|
+
return find(cached_id, options).tap { |r| r.found_via_slug = slug }
|
34
|
+
end
|
35
|
+
# Otherwise, fallback to the normal approach.
|
36
|
+
super.tap do |record|
|
37
|
+
cache_slug_lookup!(slug, record) if record.present?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def slug_cache_key(slug)
|
42
|
+
[Pseudocephalopod.cache_key_prefix, slug_scope_key(Digest::SHA256.hexdigest(slug.to_s.strip))].compact.join("/")
|
43
|
+
end
|
44
|
+
|
45
|
+
def has_cache_for_slug?(slug)
|
46
|
+
lookup_cached_id_from_slug(slug).present?
|
47
|
+
end
|
48
|
+
|
49
|
+
def cache_slug_lookup!(slug, record)
|
50
|
+
Pseudocephalopod.cache && Pseudocephalopod.cache.write(slug_cache_key(slug), record && record.id)
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def lookup_cached_id_from_slug(slug)
|
56
|
+
Pseudocephalopod.cache && Pseudocephalopod.cache.read(slug_cache_key(slug))
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Pseudocephalopod
|
2
|
+
module Finders
|
3
|
+
|
4
|
+
def find_using_slug(slug, options = {})
|
5
|
+
slug = slug.to_s
|
6
|
+
value = nil
|
7
|
+
value ||= find_by_id(slug.to_i, options) if slug =~ /\A\d+\Z/
|
8
|
+
value ||= with_cached_slug(slug).first(options)
|
9
|
+
value ||= find_using_slug_history(slug, options) if store_slug_history
|
10
|
+
value.found_via_slug = slug if value.present?
|
11
|
+
value
|
12
|
+
end
|
13
|
+
|
14
|
+
def find_using_slug!(slug, options = {})
|
15
|
+
find_using_slug(slug, options) or raise ActiveRecord::RecordNotFound
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Pseudocephalopod
|
2
|
+
module Scopes
|
3
|
+
|
4
|
+
def with_cached_slug(slug)
|
5
|
+
where(self.cached_slug_column => slug.to_s)
|
6
|
+
end
|
7
|
+
|
8
|
+
def other_than(record)
|
9
|
+
record.new_record? ? unscoped : where("#{quoted_table_name}.id != ?", record.id)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Pseudocephalopod
|
2
|
+
class Slug < ActiveRecord::Base
|
3
|
+
set_table_name "slugs"
|
4
|
+
|
5
|
+
validates_presence_of :record_id, :slug, :scope
|
6
|
+
|
7
|
+
scope :ordered, order('created_at DESC')
|
8
|
+
scope :only_slug, select(:slug)
|
9
|
+
scope :for_record, lambda { |r| where(:record_id => r.id, :scope => Pseudocephalopod.key_for_scope(r)) }
|
10
|
+
scope :for_slug, lambda { |scope, slug| where(:scope=> scope.to_s, :slug => slug.to_s)}
|
11
|
+
|
12
|
+
def self.id_for(scope, slug)
|
13
|
+
ordered.for_slug(scope, slug).first.try(:record_id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.record_slug(record, slug)
|
17
|
+
scope = Pseudocephalopod.key_for_scope(record)
|
18
|
+
# Clear slug history in this scope before recording the new slug
|
19
|
+
for_slug(scope, slug).delete_all
|
20
|
+
create :scope => scope, :record_id => record.id, :slug => slug.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.previous_for(record)
|
24
|
+
ordered.only_slug.for_record(record).all.map(&:slug)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.remove_history_for(record)
|
28
|
+
for_record(record).delete_all
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Pseudocephalopod
|
2
|
+
module SlugHistory
|
3
|
+
|
4
|
+
def self.included(parent)
|
5
|
+
parent.class_eval do
|
6
|
+
include InstanceMethods
|
7
|
+
extend ClassMethods
|
8
|
+
after_save :record_slug_changes
|
9
|
+
after_destroy :remove_slug_history
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
|
15
|
+
def previous_slugs
|
16
|
+
Pseudocephalopod.previous_slugs_for(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def record_slug_changes
|
22
|
+
return unless send(:"#{self.cached_slug_column}_changed?")
|
23
|
+
value = send(:"#{self.cached_slug_column}_was")
|
24
|
+
Pseudocephalopod.record_slug(self, value) if value.present?
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove_slug_history
|
28
|
+
Pseudocephalopod.remove_slug_history_for(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
|
35
|
+
def find_using_slug_history(slug, options = {})
|
36
|
+
id = Pseudocephalopod.last_known_slug_id(self, slug)
|
37
|
+
id.present? ? find_by_id(id, options) : nil
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'uuid'
|
2
|
+
|
3
|
+
module Pseudocephalopod
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
attr_accessor :cache_key_prefix, :cache
|
8
|
+
|
9
|
+
def with_counter(prefix, counter = 0)
|
10
|
+
counter < 1 ? prefix : "#{prefix}--#{counter}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def next_value(scope, prefix)
|
14
|
+
counter = 0
|
15
|
+
slug = self.with_counter(prefix, counter)
|
16
|
+
while scope.with_cached_slug(slug).exists?
|
17
|
+
counter += 1
|
18
|
+
slug = self.with_counter(prefix, counter)
|
19
|
+
end
|
20
|
+
slug
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate_uuid_slug
|
24
|
+
UUID.new.generate
|
25
|
+
end
|
26
|
+
|
27
|
+
def last_known_slug_id(scope, slug)
|
28
|
+
Pseudocephalopod::Slug.id_for(Pseudocephalopod.key_for_scope(scope), slug)
|
29
|
+
end
|
30
|
+
|
31
|
+
def record_slug(record, slug)
|
32
|
+
Pseudocephalopod::Slug.record_slug(record, slug)
|
33
|
+
end
|
34
|
+
|
35
|
+
def previous_slugs_for(record)
|
36
|
+
Pseudocephalopod::Slug.previous_for(record)
|
37
|
+
end
|
38
|
+
|
39
|
+
def remove_slug_history_for(record)
|
40
|
+
Pseudocephalopod::Slug.remove_history_for(record)
|
41
|
+
end
|
42
|
+
|
43
|
+
def key_for_scope(scope)
|
44
|
+
if scope.respond_to?(:slug_scope_key)
|
45
|
+
scope.slug_scope_key
|
46
|
+
elsif scope.class.respond_to?(:slug_scope_key)
|
47
|
+
scope.class.slug_scope_key
|
48
|
+
else
|
49
|
+
scope.to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
self.cache_key_prefix ||= "cached-slugs"
|
56
|
+
|
57
|
+
autoload :Caching, 'pseudocephalopod/caching'
|
58
|
+
autoload :Scopes, 'pseudocephalopod/scopes'
|
59
|
+
autoload :Finders, 'pseudocephalopod/finders'
|
60
|
+
autoload :SlugHistory, 'pseudocephalopod/slug_history'
|
61
|
+
autoload :Slug, 'pseudocephalopod/slug'
|
62
|
+
autoload :MemoryCache, 'pseudocephalopod/memory_cache'
|
63
|
+
|
64
|
+
require 'pseudocephalopod/active_record_methods'
|
65
|
+
ActiveRecord::Base.extend Pseudocephalopod::ActiveRecordMethods
|
66
|
+
|
67
|
+
require 'pseudocephalopod/railtie' if defined?(Rails::Railtie)
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class CachingTest < Test::Unit::TestCase
|
4
|
+
with_tables :slugs, :users, :unslugged_users do
|
5
|
+
|
6
|
+
setup { Pseudocephalopod::MemoryCache.reset! }
|
7
|
+
|
8
|
+
should 'store a cache automatically after finding it' do
|
9
|
+
assert !User.has_cache_for_slug?("bob")
|
10
|
+
u = User.create :name => "Bob"
|
11
|
+
assert User.has_cache_for_slug?("bob")
|
12
|
+
Pseudocephalopod::MemoryCache.reset!
|
13
|
+
assert !User.has_cache_for_slug?("bob")
|
14
|
+
assert_equal u, User.find_using_slug("bob")
|
15
|
+
assert User.has_cache_for_slug?("bob")
|
16
|
+
# Second find from cache
|
17
|
+
assert_equal u, User.find_using_slug("bob")
|
18
|
+
end
|
19
|
+
|
20
|
+
should 'remove cache on models without history' do
|
21
|
+
UnsluggedUser.is_sluggable :name, :history => false
|
22
|
+
u = UnsluggedUser.create :name => "Bob"
|
23
|
+
assert UnsluggedUser.has_cache_for_slug?("bob")
|
24
|
+
assert !UnsluggedUser.has_cache_for_slug?("red")
|
25
|
+
assert !UnsluggedUser.has_cache_for_slug?("sam")
|
26
|
+
u.update_attributes :name => "Red"
|
27
|
+
u.update_attributes :name => "Sam"
|
28
|
+
assert !UnsluggedUser.has_cache_for_slug?("bob")
|
29
|
+
assert !UnsluggedUser.has_cache_for_slug?("red")
|
30
|
+
assert UnsluggedUser.has_cache_for_slug?("sam")
|
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")
|
49
|
+
end
|
50
|
+
|
51
|
+
should 'allow you to disable caching' do
|
52
|
+
UnsluggedUser.is_sluggable :name, :use_cache => false
|
53
|
+
assert !UnsluggedUser.respond_to?(:has_cache_for_slug?)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Ensure we destroy the contents of the cache after each test.
|
57
|
+
teardown { Pseudocephalopod::MemoryCache.reset! }
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
$KCODE = 'UTF8'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
Bundler.setup
|
6
|
+
Bundler.require
|
7
|
+
|
8
|
+
require 'test/unit'
|
9
|
+
require 'shoulda'
|
10
|
+
require 'redgreen' if RUBY_VERSION < '1.9'
|
11
|
+
|
12
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
13
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
14
|
+
require 'pseudocephalopod'
|
15
|
+
require 'model_definitions'
|
16
|
+
|
17
|
+
# Use a memory cache for testing.
|
18
|
+
Pseudocephalopod.cache = Pseudocephalopod::MemoryCache
|
19
|
+
|
20
|
+
class Test::Unit::TestCase
|
21
|
+
extend ReversibleData::ShouldaMacros
|
22
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
class IsSluggableTest < Test::Unit::TestCase
|
5
|
+
with_tables :slugs, :users, :unslugged_users do
|
6
|
+
|
7
|
+
should 'correctly sluggify a value' do
|
8
|
+
user = User.create(:name => "Bob")
|
9
|
+
assert_equal "bob", user.to_param
|
10
|
+
assert_equal "bob", user.cached_slug
|
11
|
+
end
|
12
|
+
|
13
|
+
should 'generate a uuid in place of a slug' do
|
14
|
+
user = User.create(:name => '')
|
15
|
+
assert user.cached_slug.present?
|
16
|
+
end
|
17
|
+
|
18
|
+
should 'return need to generate a slug when the cahced slug is blank' do
|
19
|
+
user = User.new(:name => "Ninja Stuff")
|
20
|
+
assert user.cached_slug.blank?
|
21
|
+
assert user.should_generate_slug?
|
22
|
+
user.save
|
23
|
+
assert user.cached_slug.present?
|
24
|
+
assert !user.should_generate_slug?
|
25
|
+
user.name = 'Awesome'
|
26
|
+
assert user.should_generate_slug?
|
27
|
+
end
|
28
|
+
|
29
|
+
should 'let you disable syncing a slug' do
|
30
|
+
UnsluggedUser.is_sluggable :name, :sync => false
|
31
|
+
user = UnsluggedUser.create(:name => "Ninja User")
|
32
|
+
assert !user.should_generate_slug?
|
33
|
+
user.name = "Another User Name"
|
34
|
+
assert !user.should_generate_slug?
|
35
|
+
end
|
36
|
+
|
37
|
+
should 'by default record slug history' do
|
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_equal ["red", "sal", "bob"], user.previous_slugs
|
44
|
+
assert_equal user, User.find_using_slug("red")
|
45
|
+
assert_equal user, User.find_using_slug("sal")
|
46
|
+
assert_equal user, User.find_using_slug("bob")
|
47
|
+
assert_equal user, User.find_using_slug("jim")
|
48
|
+
end
|
49
|
+
|
50
|
+
should 'let you disable recording of slug history' do
|
51
|
+
UnsluggedUser.is_sluggable :name, :history => false
|
52
|
+
user = UnsluggedUser.create(:name => "Bob")
|
53
|
+
assert !user.respond_to?(:previous_slugs)
|
54
|
+
user.update_attributes! :name => "Red"
|
55
|
+
assert_equal user, UnsluggedUser.find_using_slug("red")
|
56
|
+
assert_not_equal user, UnsluggedUser.find_using_slug("bob")
|
57
|
+
assert UnsluggedUser.find_using_slug("bob").blank?
|
58
|
+
end
|
59
|
+
|
60
|
+
should "let you find a record by it's id as needed" do
|
61
|
+
user = User.create :name => "Bob"
|
62
|
+
assert_equal user, User.find_using_slug(user.id)
|
63
|
+
assert_equal user, User.find_using_slug(user.id.to_i)
|
64
|
+
end
|
65
|
+
|
66
|
+
should 'default to generate a uuid' do
|
67
|
+
user = User.create :name => ""
|
68
|
+
assert_match /\A[a-zA-Z0-9]{32}\Z/, user.cached_slug.gsub("-", "")
|
69
|
+
user = User.create
|
70
|
+
assert_match /\A[a-zA-Z0-9]{32}\Z/, user.cached_slug.gsub("-", "")
|
71
|
+
end
|
72
|
+
|
73
|
+
should 'automatically append a sequence to the end of conflicting slugs' do
|
74
|
+
u1 = User.create :name => "ninjas Are awesome"
|
75
|
+
u2 = User.create :name => "Ninjas are awesome"
|
76
|
+
assert_equal "ninjas-are-awesome", u1.to_slug
|
77
|
+
assert_equal "ninjas-are-awesome--1", u2.to_slug
|
78
|
+
end
|
79
|
+
|
80
|
+
should 'let you find out if there is a better way of finding a slug' do
|
81
|
+
user = User.create :name => "Bob"
|
82
|
+
user.update_attributes! :name => "Ralph"
|
83
|
+
assert !User.find_using_slug("ralph").has_better_slug?
|
84
|
+
assert User.find_using_slug("bob").has_better_slug?
|
85
|
+
assert User.find_using_slug(user.id).has_better_slug?
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'reversible_data'
|
2
|
+
|
3
|
+
ReversibleData.in_memory!
|
4
|
+
|
5
|
+
# Define models here.
|
6
|
+
|
7
|
+
ReversibleData.add :slugs do |t|
|
8
|
+
t.string :scope
|
9
|
+
t.string :slug
|
10
|
+
t.integer :record_id
|
11
|
+
t.datetime :created_at
|
12
|
+
end
|
13
|
+
|
14
|
+
user = ReversibleData.add :users do |u|
|
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|
|
25
|
+
u.string :name
|
26
|
+
u.string :cached_slug
|
27
|
+
u.timestamps
|
28
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class PseudocephalopodTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
should 'return the correct counter versions' do
|
6
|
+
assert_equal 'awesome', Pseudocephalopod.with_counter('awesome')
|
7
|
+
assert_equal 'awesome', Pseudocephalopod.with_counter('awesome', 0)
|
8
|
+
assert_equal 'awesome', Pseudocephalopod.with_counter('awesome', -1)
|
9
|
+
assert_equal 'awesome--2', Pseudocephalopod.with_counter('awesome', 2)
|
10
|
+
assert_equal 'awesome--100', Pseudocephalopod.with_counter('awesome', 100)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class FakedModel
|
4
|
+
attr_reader :id
|
5
|
+
def self.slug_scope_key; "faked_models"; end
|
6
|
+
def initialize(id); @id = id; end
|
7
|
+
end
|
8
|
+
|
9
|
+
class SlugHistoryTest < Test::Unit::TestCase
|
10
|
+
with_tables :slugs do
|
11
|
+
|
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
|
+
end
|
19
|
+
|
20
|
+
should 'let you lookup a given record id easily' do
|
21
|
+
assert Pseudocephalopod.last_known_slug_id(FakedModel, "felafel").blank?
|
22
|
+
assert Pseudocephalopod.last_known_slug_id(FakedModel, "ninjas-2").blank?
|
23
|
+
assert_equal 12, Pseudocephalopod.last_known_slug_id(FakedModel, "awesome")
|
24
|
+
assert_equal 4, Pseudocephalopod.last_known_slug_id(FakedModel, "awesome-1")
|
25
|
+
assert_equal 12, Pseudocephalopod.last_known_slug_id(FakedModel, "ninjas")
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pseudocephalopod
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Darcy Laycock
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-23 00:00:00 +08:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activerecord
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 3
|
29
|
+
- 0
|
30
|
+
- 0
|
31
|
+
- beta2
|
32
|
+
version: 3.0.0.beta2
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: shoulda
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
segments:
|
43
|
+
- 0
|
44
|
+
version: "0"
|
45
|
+
type: :development
|
46
|
+
version_requirements: *id002
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: reversible_data
|
49
|
+
prerelease: false
|
50
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
type: :development
|
58
|
+
version_requirements: *id003
|
59
|
+
description: Super simple slugs for ActiveRecord 3.0 and higher, with support for slug history
|
60
|
+
email: sutto@sutto.net
|
61
|
+
executables: []
|
62
|
+
|
63
|
+
extensions: []
|
64
|
+
|
65
|
+
extra_rdoc_files:
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
files:
|
69
|
+
- .document
|
70
|
+
- .gitignore
|
71
|
+
- Gemfile
|
72
|
+
- LICENSE
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- lib/generators/pseudocephalopod/slug_migration/slug_migration_generator.rb
|
76
|
+
- lib/generators/pseudocephalopod/slug_migration/templates/migration.rb
|
77
|
+
- lib/generators/pseudocephalopod/slugs/slugs_generator.rb
|
78
|
+
- lib/generators/pseudocephalopod/slugs/templates/migration.rb
|
79
|
+
- lib/pseudocephalopod.rb
|
80
|
+
- lib/pseudocephalopod/active_record_methods.rb
|
81
|
+
- lib/pseudocephalopod/caching.rb
|
82
|
+
- lib/pseudocephalopod/finders.rb
|
83
|
+
- lib/pseudocephalopod/memory_cache.rb
|
84
|
+
- lib/pseudocephalopod/railtie.rb
|
85
|
+
- lib/pseudocephalopod/scopes.rb
|
86
|
+
- lib/pseudocephalopod/slug.rb
|
87
|
+
- lib/pseudocephalopod/slug_history.rb
|
88
|
+
- lib/pseudocephalopod/version.rb
|
89
|
+
- test/caching_test.rb
|
90
|
+
- test/helper.rb
|
91
|
+
- test/is_sluggable_test.rb
|
92
|
+
- test/model_definitions.rb
|
93
|
+
- test/pseudocephalopod_test.rb
|
94
|
+
- test/slug_history_test.rb
|
95
|
+
has_rdoc: true
|
96
|
+
homepage: http://github.com/Sutto/pseudocephalopod
|
97
|
+
licenses: []
|
98
|
+
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options:
|
101
|
+
- --charset=UTF-8
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
version: "0"
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
segments:
|
116
|
+
- 0
|
117
|
+
version: "0"
|
118
|
+
requirements: []
|
119
|
+
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 1.3.6
|
122
|
+
signing_key:
|
123
|
+
specification_version: 3
|
124
|
+
summary: Super simple slugs for ActiveRecord 3.0 and higher, with support for slug history
|
125
|
+
test_files:
|
126
|
+
- test/caching_test.rb
|
127
|
+
- test/helper.rb
|
128
|
+
- test/is_sluggable_test.rb
|
129
|
+
- test/model_definitions.rb
|
130
|
+
- test/pseudocephalopod_test.rb
|
131
|
+
- test/slug_history_test.rb
|