dm-is-slug 0.10.2
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 -0
- data/LICENSE +20 -0
- data/README.textile +56 -0
- data/Rakefile +32 -0
- data/TODO +4 -0
- data/VERSION +1 -0
- data/dm-is-slug.gemspec +69 -0
- data/lib/dm-is-slug.rb +7 -0
- data/lib/dm-is-slug/is/slug.rb +185 -0
- data/spec/integration/slug_spec.rb +269 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +38 -0
- data/tasks/ci.rake +1 -0
- data/tasks/metrics.rake +36 -0
- data/tasks/spec.rake +41 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +115 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Aaron Qian
|
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.textile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
= dm-is-slug
|
2
|
+
|
3
|
+
DataMapper plugin for creating and slugs(permalinks).
|
4
|
+
|
5
|
+
== Installation
|
6
|
+
|
7
|
+
NOTE: You no longer need to download dm-more source code in order to install
|
8
|
+
this.
|
9
|
+
|
10
|
+
All you need to do is:
|
11
|
+
|
12
|
+
$ sudo rake install
|
13
|
+
|
14
|
+
Remember to require it in your app's init.rb
|
15
|
+
|
16
|
+
dependency 'dm-is-slug'
|
17
|
+
|
18
|
+
== Getting started
|
19
|
+
|
20
|
+
Lets say we have a post-class, and we want to generate permalinks or slugs for all posts.
|
21
|
+
|
22
|
+
class Post
|
23
|
+
include DataMapper::Resource
|
24
|
+
|
25
|
+
property :id, Serial
|
26
|
+
property :title, String
|
27
|
+
property :content, String
|
28
|
+
|
29
|
+
# here we define that it should have a slug that uses title as the permalink
|
30
|
+
# it will generate an extra slug property of String type, with the same size as title
|
31
|
+
is :slug, :source => :title
|
32
|
+
end
|
33
|
+
|
34
|
+
Let's Say we need to define a permalink based on a method instead of a property.
|
35
|
+
|
36
|
+
class User
|
37
|
+
include DataMapper::Resource
|
38
|
+
|
39
|
+
property :id, Serial
|
40
|
+
property :email, String
|
41
|
+
property :password, String
|
42
|
+
|
43
|
+
# we only want to strip out the domain name
|
44
|
+
# and use only the email account name as the permalink
|
45
|
+
def slug_for_email
|
46
|
+
email.split("@").first
|
47
|
+
end
|
48
|
+
|
49
|
+
# here we define that it should have a slug that uses title as the permalink
|
50
|
+
# it will generate an extra slug property of String type, with the same size as title
|
51
|
+
is :slug, :source => :slug_for_email, :size => 255
|
52
|
+
end
|
53
|
+
|
54
|
+
You can now find objects by slug like this:
|
55
|
+
|
56
|
+
post = Post.first(:slug => "your_slug")
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
gem 'jeweler', '~> 1.4'
|
6
|
+
require 'jeweler'
|
7
|
+
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.name = "dm-is-slug"
|
10
|
+
gem.summary = "DataMapper plugin that generates unique slugs"
|
11
|
+
gem.description = gem.summary
|
12
|
+
gem.email = [
|
13
|
+
'aq1018@gmail.com',
|
14
|
+
'james.herdman@gmail.com',
|
15
|
+
'nik [a] terminaldischarge [d] net',
|
16
|
+
'maverick.stoklosa@gmail.com',
|
17
|
+
'frawl021@gmail.com',
|
18
|
+
'cheba@pointlessone.org'
|
19
|
+
]
|
20
|
+
gem.homepage = "http://github.com/aq1018/dm-is-slug"
|
21
|
+
gem.authors = ['Aaron Qian', 'James Herdman', 'Nik Radford', 'Paul', 'Mike Frawley', 'Alex Makuta']
|
22
|
+
gem.add_dependency "dm-core", "~>0.10.2"
|
23
|
+
gem.add_dependency "unidecode", "~>1.0.0"
|
24
|
+
gem.add_development_dependency 'rspec', '~> 1.3'
|
25
|
+
end
|
26
|
+
|
27
|
+
Jeweler::GemcutterTasks.new
|
28
|
+
|
29
|
+
FileList['tasks/**/*.rake'].each { |task| import task }
|
30
|
+
rescue LoadError
|
31
|
+
puts 'Jeweler (or a dependency) not available. Install it with: gem install jeweler'
|
32
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.10.2
|
data/dm-is-slug.gemspec
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{dm-is-slug}
|
8
|
+
s.version = "0.10.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Aaron Qian", "James Herdman", "Nik Radford", "Paul", "Mike Frawley", "Alex Makuta"]
|
12
|
+
s.date = %q{2010-02-07}
|
13
|
+
s.description = %q{DataMapper plugin that generates unique slugs}
|
14
|
+
s.email = ["aq1018@gmail.com", "james.herdman@gmail.com", "nik [a] terminaldischarge [d] net", "maverick.stoklosa@gmail.com", "frawl021@gmail.com", "cheba@pointlessone.org"]
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.textile",
|
18
|
+
"TODO"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.textile",
|
24
|
+
"Rakefile",
|
25
|
+
"TODO",
|
26
|
+
"VERSION",
|
27
|
+
"dm-is-slug.gemspec",
|
28
|
+
"lib/dm-is-slug.rb",
|
29
|
+
"lib/dm-is-slug/is/slug.rb",
|
30
|
+
"spec/integration/slug_spec.rb",
|
31
|
+
"spec/rcov.opts",
|
32
|
+
"spec/spec.opts",
|
33
|
+
"spec/spec_helper.rb",
|
34
|
+
"tasks/ci.rake",
|
35
|
+
"tasks/metrics.rake",
|
36
|
+
"tasks/spec.rake",
|
37
|
+
"tasks/yard.rake",
|
38
|
+
"tasks/yardstick.rake"
|
39
|
+
]
|
40
|
+
s.homepage = %q{http://github.com/aq1018/dm-is-slug}
|
41
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
42
|
+
s.require_paths = ["lib"]
|
43
|
+
s.rubygems_version = %q{1.3.5}
|
44
|
+
s.summary = %q{DataMapper plugin that generates unique slugs}
|
45
|
+
s.test_files = [
|
46
|
+
"spec/integration/slug_spec.rb",
|
47
|
+
"spec/spec_helper.rb"
|
48
|
+
]
|
49
|
+
|
50
|
+
if s.respond_to? :specification_version then
|
51
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
52
|
+
s.specification_version = 3
|
53
|
+
|
54
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_runtime_dependency(%q<dm-core>, ["~> 0.10.2"])
|
56
|
+
s.add_runtime_dependency(%q<unidecode>, ["~> 1.0.0"])
|
57
|
+
s.add_development_dependency(%q<rspec>, ["~> 1.3"])
|
58
|
+
else
|
59
|
+
s.add_dependency(%q<dm-core>, ["~> 0.10.2"])
|
60
|
+
s.add_dependency(%q<unidecode>, ["~> 1.0.0"])
|
61
|
+
s.add_dependency(%q<rspec>, ["~> 1.3"])
|
62
|
+
end
|
63
|
+
else
|
64
|
+
s.add_dependency(%q<dm-core>, ["~> 0.10.2"])
|
65
|
+
s.add_dependency(%q<unidecode>, ["~> 1.0.0"])
|
66
|
+
s.add_dependency(%q<rspec>, ["~> 1.3"])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
data/lib/dm-is-slug.rb
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'unidecode'
|
2
|
+
require 'dm-core/support/chainable'
|
3
|
+
|
4
|
+
module DataMapper
|
5
|
+
module Is
|
6
|
+
module Slug
|
7
|
+
class InvalidSlugSourceError < StandardError; end
|
8
|
+
|
9
|
+
# @param [String] str A string to escape for use as a slug
|
10
|
+
# @return [String] an URL-safe string
|
11
|
+
def self.escape(str)
|
12
|
+
s = str.to_ascii
|
13
|
+
s.gsub!(/\W+/, ' ')
|
14
|
+
s.strip!
|
15
|
+
s.downcase!
|
16
|
+
s.gsub!(/\ +/, '-')
|
17
|
+
s
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Methods that should be included in DataMapper::Model.
|
22
|
+
# Normally this should just be your generator, so that the namespace
|
23
|
+
# does not get cluttered. ClassMethods and InstanceMethods gets added
|
24
|
+
# in the specific resources when you fire is :slug
|
25
|
+
##
|
26
|
+
|
27
|
+
# Defines a +slug+ property on your model with the same length as your
|
28
|
+
# source property. This property is Unicode escaped, and treated so as
|
29
|
+
# to be fit for use in URLs.
|
30
|
+
#
|
31
|
+
# ==== Example
|
32
|
+
# Suppose your source attribute was the following string: "Hot deals on
|
33
|
+
# Boxing Day". This string would be escaped to "hot-deals-on-boxing-day".
|
34
|
+
#
|
35
|
+
# Non-ASCII characters are attempted to be converted to their nearest
|
36
|
+
# approximate.
|
37
|
+
#
|
38
|
+
# ==== Parameters
|
39
|
+
# +permanent_slug+::
|
40
|
+
# Permanent slugs are not changed even if the source property has
|
41
|
+
# +source+::
|
42
|
+
# The property on the model to use as the source of the generated slug,
|
43
|
+
# or an instance method defined in the model, the method must return
|
44
|
+
# a string or nil.
|
45
|
+
# +length+::
|
46
|
+
# The length of the +slug+ property
|
47
|
+
#
|
48
|
+
# @param [Hash] provide options in a Hash. See *Parameters* for details
|
49
|
+
def is_slug(options)
|
50
|
+
if options.key?(:size)
|
51
|
+
warn "Slug with :size option is deprecated, use :length instead"
|
52
|
+
options[:length] = options.delete(:size)
|
53
|
+
end
|
54
|
+
|
55
|
+
extend DataMapper::Is::Slug::ClassMethods
|
56
|
+
include DataMapper::Is::Slug::InstanceMethods
|
57
|
+
extend Chainable
|
58
|
+
|
59
|
+
@slug_options = {}
|
60
|
+
|
61
|
+
@slug_options[:permanent_slug] = options.delete(:permanent_slug)
|
62
|
+
@slug_options[:permanent_slug] = true if @slug_options[:permanent_slug].nil?
|
63
|
+
|
64
|
+
@slug_options[:source] = options.delete(:source)
|
65
|
+
raise InvalidSlugSourceError, 'You must specify a :source to generate slug.' unless slug_source
|
66
|
+
|
67
|
+
|
68
|
+
options[:length] ||= get_slug_length
|
69
|
+
property(:slug, String, options.merge(:unique => true)) unless slug_property
|
70
|
+
|
71
|
+
before :valid?, :generate_slug
|
72
|
+
before :save, :generate_slug
|
73
|
+
end
|
74
|
+
|
75
|
+
module ClassMethods
|
76
|
+
attr_reader :slug_options
|
77
|
+
|
78
|
+
def permanent_slug?
|
79
|
+
slug_options[:permanent_slug]
|
80
|
+
end
|
81
|
+
|
82
|
+
def slug_source
|
83
|
+
slug_options[:source] ? slug_options[:source].to_sym : nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def slug_source_property
|
87
|
+
detect_slug_property_by_name(slug_source)
|
88
|
+
end
|
89
|
+
|
90
|
+
def slug_property
|
91
|
+
detect_slug_property_by_name(:slug)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def detect_slug_property_by_name(name)
|
97
|
+
p = properties[name]
|
98
|
+
!p.nil? && p.type == String ? p : nil
|
99
|
+
end
|
100
|
+
|
101
|
+
def get_slug_length
|
102
|
+
slug_property.nil? ? (slug_source_property.nil? ? DataMapper::Property::DEFAULT_LENGTH : slug_source_property.length) : slug_property.length
|
103
|
+
end
|
104
|
+
end # ClassMethods
|
105
|
+
|
106
|
+
module InstanceMethods
|
107
|
+
def to_param
|
108
|
+
[slug]
|
109
|
+
end
|
110
|
+
|
111
|
+
def permanent_slug?
|
112
|
+
self.class.permanent_slug?
|
113
|
+
end
|
114
|
+
|
115
|
+
def slug_source
|
116
|
+
self.class.slug_source
|
117
|
+
end
|
118
|
+
|
119
|
+
def slug_source_property
|
120
|
+
self.class.slug_source_property
|
121
|
+
end
|
122
|
+
|
123
|
+
def slug_property
|
124
|
+
self.class.slug_property
|
125
|
+
end
|
126
|
+
|
127
|
+
def slug_source_value
|
128
|
+
self.send(slug_source)
|
129
|
+
end
|
130
|
+
|
131
|
+
# The slug is not stale if
|
132
|
+
# 1. the slug is permanent, and slug column has something valid in it
|
133
|
+
# 2. the slug source value is nil or empty
|
134
|
+
def stale_slug?
|
135
|
+
!((permanent_slug? && slug && !slug.empty?) || (slug_source_value.nil? || slug_source_value.empty?))
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def generate_slug
|
141
|
+
#puts "\nGenerating slug for #{self.class.name}: #{self.key.inspect}\n"
|
142
|
+
return unless self.class.respond_to?(:slug_options) && self.class.slug_options
|
143
|
+
raise InvalidSlugSourceError, 'Invalid slug source.' unless slug_source_property || self.respond_to?(slug_source)
|
144
|
+
return unless stale_slug?
|
145
|
+
attribute_set :slug, unique_slug
|
146
|
+
end
|
147
|
+
|
148
|
+
def unique_slug
|
149
|
+
old_slug = self.slug
|
150
|
+
max_length = self.class.send(:get_slug_length)
|
151
|
+
base_slug = ::DataMapper::Is::Slug.escape(slug_source_value)[0, max_length]
|
152
|
+
i = 1
|
153
|
+
new_slug = base_slug
|
154
|
+
|
155
|
+
if old_slug != new_slug
|
156
|
+
not_self_conditions = {}
|
157
|
+
unless new?
|
158
|
+
self.model.key.each do |property|
|
159
|
+
not_self_conditions.merge!(property.name.not => self.send(property.name))
|
160
|
+
end
|
161
|
+
#puts "Not self: #{not_self_conditions.inspect}"
|
162
|
+
end
|
163
|
+
|
164
|
+
lambda do
|
165
|
+
#puts "Lambda new slug: #{new_slug}"
|
166
|
+
dupe = self.class.first(not_self_conditions.merge(:slug => new_slug))
|
167
|
+
if dupe
|
168
|
+
#puts "Got dupe: #{dupe.inspect}"
|
169
|
+
i = i + 1
|
170
|
+
slug_length = max_length - i.to_s.length - 1
|
171
|
+
new_slug = "#{base_slug[0, slug_length]}-#{i}"
|
172
|
+
#puts "New slug: #{new_slug}"
|
173
|
+
redo
|
174
|
+
end
|
175
|
+
end.call
|
176
|
+
#puts "Found new slug: #{new_slug}"
|
177
|
+
new_slug
|
178
|
+
else
|
179
|
+
old_slug
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end # InstanceMethods
|
183
|
+
end # Slug
|
184
|
+
end # Is
|
185
|
+
end # DataMapper
|
@@ -0,0 +1,269 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
|
3
|
+
|
4
|
+
if HAS_SQLITE3 || HAS_MYSQL || HAS_POSTGRES
|
5
|
+
describe 'DataMapper::Is::Slug' do
|
6
|
+
|
7
|
+
class User
|
8
|
+
include DataMapper::Resource
|
9
|
+
|
10
|
+
property :id, Serial
|
11
|
+
property :email, String
|
12
|
+
has n, :posts
|
13
|
+
has n, :todos
|
14
|
+
|
15
|
+
def slug_for_email
|
16
|
+
email.split("@").first
|
17
|
+
end
|
18
|
+
|
19
|
+
is :slug, :source => :slug_for_email, :length => 80, :permanent_slug => false
|
20
|
+
end
|
21
|
+
|
22
|
+
class Post
|
23
|
+
include DataMapper::Resource
|
24
|
+
|
25
|
+
property :id, Serial
|
26
|
+
property :title, String, :length => 30
|
27
|
+
property :content, Text
|
28
|
+
|
29
|
+
belongs_to :user
|
30
|
+
|
31
|
+
is :slug, :source => :title
|
32
|
+
end
|
33
|
+
|
34
|
+
class Todo
|
35
|
+
include DataMapper::Resource
|
36
|
+
property :id, Serial
|
37
|
+
property :title, String
|
38
|
+
|
39
|
+
belongs_to :user
|
40
|
+
end
|
41
|
+
|
42
|
+
class SlugKey
|
43
|
+
include DataMapper::Resource
|
44
|
+
property :title, String
|
45
|
+
|
46
|
+
is :slug, :source => :title, :key => true
|
47
|
+
end
|
48
|
+
|
49
|
+
before :all do
|
50
|
+
User.auto_migrate!(:default)
|
51
|
+
Post.auto_migrate!(:default)
|
52
|
+
Todo.auto_migrate!(:default)
|
53
|
+
SlugKey.auto_migrate!(:default)
|
54
|
+
|
55
|
+
@u1 = User.create(:email => "john@ekohe.com")
|
56
|
+
@p1 = Post.create(:user => @u1, :title => "My first shinny blog post")
|
57
|
+
@p2 = Post.create(:user => @u1, :title => "My second shinny blog post")
|
58
|
+
@p3 = Post.create(:user => @u1, :title => "My third shinny blog post")
|
59
|
+
|
60
|
+
@u2 = User.create(:email => "john@someotherplace.com")
|
61
|
+
@p4 = Post.create(:user => @u2, :title => "My first Shinny blog post")
|
62
|
+
@p5 = Post.create(:user => @u2, :title => "i heart merb and dm")
|
63
|
+
@p6 = Post.create(:user => @u2, :title => "A fancy café")
|
64
|
+
@p7 = Post.create(:user => @u2, :title => "你好")
|
65
|
+
|
66
|
+
(1..10).each do |i|
|
67
|
+
instance_variable_set "@p1_#{i}".to_sym, Post.create(:user => @u2, :title => "another productive day!!")
|
68
|
+
end
|
69
|
+
(1..10).each do |i|
|
70
|
+
instance_variable_set "@p2_#{i}".to_sym, Post.create(:user => @u2, :title => "DM tricks")
|
71
|
+
end
|
72
|
+
|
73
|
+
@sk = SlugKey.create(:title => 'slug key')
|
74
|
+
|
75
|
+
@post1 = Post.create :user => @u1, :title => 'a' * Post.slug_property.length
|
76
|
+
@post2 = Post.create :user => @u1, :title => 'a' * Post.slug_property.length
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should raise error if :source option is not specified" do
|
80
|
+
lambda {
|
81
|
+
class BadUsage
|
82
|
+
include DataMapper::Resource
|
83
|
+
|
84
|
+
property :id, Serial
|
85
|
+
|
86
|
+
is :slug, {}
|
87
|
+
end
|
88
|
+
}.should raise_error(DataMapper::Is::Slug::InvalidSlugSourceError)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should display obsolete warning if :size option is used" do
|
92
|
+
class Thingy
|
93
|
+
end
|
94
|
+
Thingy.stub!(:warn)
|
95
|
+
Thingy.should_receive(:warn).with("Slug with :size option is deprecated, use :length instead")
|
96
|
+
|
97
|
+
lambda {
|
98
|
+
class Thingy
|
99
|
+
include DataMapper::Resource
|
100
|
+
property :title, String
|
101
|
+
|
102
|
+
is :slug, :source => :title, :size => 20
|
103
|
+
end
|
104
|
+
}.should_not raise_error
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should generate slugs" do
|
109
|
+
User.all.each do |u|
|
110
|
+
u.slug.should_not be_nil
|
111
|
+
end
|
112
|
+
|
113
|
+
Post.all.each do |p|
|
114
|
+
p.slug.should_not be_nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should generate unique slugs" do
|
119
|
+
@u1.slug.should_not == @u2.slug
|
120
|
+
@p1.slug.should_not == @p4.slug
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should generate correct slug for user" do
|
124
|
+
@u1.slug.should == "john"
|
125
|
+
@u2.slug.should == "john-2"
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should generate correct slug for post" do
|
129
|
+
@p1.slug.should == "my-first-shinny-blog-post"
|
130
|
+
@p2.slug.should == "my-second-shinny-blog-post"
|
131
|
+
@p3.slug.should == "my-third-shinny-blog-post"
|
132
|
+
@p4.slug.should == "my-first-shinny-blog-post-2"
|
133
|
+
@p5.slug.should == "i-heart-merb-and-dm"
|
134
|
+
|
135
|
+
@p1_1.slug.should == "another-productive-day"
|
136
|
+
(2..10).each do |i|
|
137
|
+
instance_variable_get("@p1_#{i}".to_sym).slug.should == "another-productive-day-#{i}"
|
138
|
+
end
|
139
|
+
|
140
|
+
@p2_1.slug.should == 'dm-tricks'
|
141
|
+
(2..10).each do |i|
|
142
|
+
instance_variable_get("@p2_#{i}".to_sym).slug.should == "dm-tricks-#{i}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should update slug if :permanent_slug => :false is specified" do
|
147
|
+
user = User.create(:email => "a_person@ekohe.com")
|
148
|
+
user.slug.should == "a_person"
|
149
|
+
|
150
|
+
user.should_not be_permanent_slug
|
151
|
+
|
152
|
+
user.email = "changed@ekohe.com"
|
153
|
+
user.should be_dirty
|
154
|
+
|
155
|
+
user.save.should be_true
|
156
|
+
user.slug.should == "changed"
|
157
|
+
user.destroy
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should not update slug if :permanent_slug => :true or not specified" do
|
161
|
+
post = Post.create(:user => @u1, :title => "hello world!")
|
162
|
+
post.slug.should == "hello-world"
|
163
|
+
post.should be_permanent_slug
|
164
|
+
post.title = "hello universe!"
|
165
|
+
post.should be_dirty
|
166
|
+
post.save.should be_true
|
167
|
+
post.slug.should == "hello-world"
|
168
|
+
post.destroy
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should have the right size for properties" do
|
172
|
+
user_slug_property = User.properties[:slug]
|
173
|
+
user_slug_property.should_not be_nil
|
174
|
+
user_slug_property.type.should == String
|
175
|
+
user_slug_property.length.should == 80
|
176
|
+
|
177
|
+
post_title_property = Post.properties[:title]
|
178
|
+
post_title_property.type.should == String
|
179
|
+
post_title_property.length.should == 30
|
180
|
+
|
181
|
+
post_slug_property = Post.properties[:slug]
|
182
|
+
post_slug_property.type.should == String
|
183
|
+
post_slug_property.should_not be_nil
|
184
|
+
post_slug_property.length.should == 30
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should output slug with to_param method" do
|
188
|
+
@u1.to_param.should == ["john"]
|
189
|
+
@p1.to_param.should == ["my-first-shinny-blog-post"]
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should find model using get method using id" do
|
193
|
+
u = User.get(@u1.id)
|
194
|
+
u.should_not be_nil
|
195
|
+
u.should == @u1
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should find model using get method using id with non-slug models" do
|
199
|
+
todo = Todo.create(:user => @u1, :title => "blabla")
|
200
|
+
todo.should_not be_nil
|
201
|
+
|
202
|
+
Todo.get(todo.id).should == todo
|
203
|
+
@u1.todos.get(todo.id).should == todo
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'should unidecode latin characters from the slug' do
|
207
|
+
@p6.slug.should == 'a-fancy-cafe'
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'should unidecode chinese characters from the slug' do
|
211
|
+
@p7.slug.should == 'ni-hao'
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'should have slug_property on instance' do
|
215
|
+
@p1.slug_property.should == @p1.class.properties.detect{|p| p.name == :slug}
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'should properly increment slug suffix' do
|
219
|
+
@p2_10.slug.should == 'dm-tricks-10'
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'should work with key on slug and validations' do
|
223
|
+
@sk.title.should == 'slug key'
|
224
|
+
@sk.slug.should == 'slug-key'
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'should have slug no longer than slug_property.length' do
|
228
|
+
@post1.slug.length.should == @post1.slug_property.length
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'should have suffixed slug no longer than slug_property.length' do
|
232
|
+
@post2.slug.length.should == @post2.class.slug_property.length
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'should generate right slug for long sources' do
|
236
|
+
@post1.slug.should == 'a' * @post1.class.slug_property.length
|
237
|
+
@post2.slug.should == ('a' * (@post2.class.slug_property.length - 2) + '-2')
|
238
|
+
end
|
239
|
+
|
240
|
+
describe 'editing' do
|
241
|
+
class Post2
|
242
|
+
include DataMapper::Resource
|
243
|
+
property :id, Serial
|
244
|
+
property :title, String, :length => 30
|
245
|
+
property :content, Text
|
246
|
+
|
247
|
+
is :slug, :source => :title, :permanent_slug => false
|
248
|
+
end
|
249
|
+
|
250
|
+
Post2.auto_migrate!
|
251
|
+
|
252
|
+
before :each do
|
253
|
+
Post2.all.destroy!
|
254
|
+
@post = Post2.create :title => 'The Post', :content => 'The content.'
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'should not change slug if source is not changed' do
|
258
|
+
@post.update :content => 'The other content.'
|
259
|
+
Post2.first.slug.should == 'the-post'
|
260
|
+
end
|
261
|
+
|
262
|
+
it 'should change slug if source is changed' do
|
263
|
+
@post.update :title => 'The Other Post'
|
264
|
+
post = Post2.first
|
265
|
+
post.slug.should == 'the-other-post'
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
data/spec/rcov.opts
ADDED
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
# use local dm-core if running from a typical dev checkout.
|
4
|
+
lib = File.join('..', '..', 'dm-core', 'lib')
|
5
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib)
|
6
|
+
require 'dm-core'
|
7
|
+
|
8
|
+
# use local dm-adjust if running from a typical dev checkout.
|
9
|
+
lib = File.join('..', 'dm-adjust', 'lib')
|
10
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib)
|
11
|
+
require 'dm-adjust'
|
12
|
+
|
13
|
+
# Support running specs with 'rake spec' and 'spec'
|
14
|
+
$LOAD_PATH.unshift('lib') unless $LOAD_PATH.include?('lib')
|
15
|
+
|
16
|
+
require 'dm-validations'
|
17
|
+
require 'dm-is-slug'
|
18
|
+
|
19
|
+
def load_driver(name, default_uri)
|
20
|
+
return false if ENV['ADAPTER'] != name.to_s
|
21
|
+
|
22
|
+
begin
|
23
|
+
DataMapper.setup(name, ENV["#{name.to_s.upcase}_SPEC_URI"] || default_uri)
|
24
|
+
DataMapper::Repository.adapters[:default] = DataMapper::Repository.adapters[name]
|
25
|
+
true
|
26
|
+
rescue LoadError => e
|
27
|
+
warn "Could not load do_#{name}: #{e}"
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
ENV['ADAPTER'] ||= 'sqlite3'
|
33
|
+
|
34
|
+
HAS_SQLITE3 = load_driver(:sqlite3, 'sqlite3::memory:')
|
35
|
+
HAS_MYSQL = load_driver(:mysql, 'mysql://localhost/dm_core_test')
|
36
|
+
HAS_POSTGRES = load_driver(:postgres, 'postgres://postgres@localhost/dm_core_test')
|
37
|
+
|
38
|
+
|
data/tasks/ci.rake
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
task :ci => [ :verify_measurements, 'metrics:all' ]
|
data/tasks/metrics.rake
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
begin
|
2
|
+
require 'metric_fu'
|
3
|
+
rescue LoadError
|
4
|
+
namespace :metrics do
|
5
|
+
task :all do
|
6
|
+
abort 'metric_fu is not available. In order to run metrics:all, you must: gem install metric_fu'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'reek/adapters/rake_task'
|
13
|
+
|
14
|
+
Reek::RakeTask.new do |t|
|
15
|
+
t.fail_on_error = true
|
16
|
+
t.verbose = false
|
17
|
+
t.source_files = 'lib/**/*.rb'
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
task :reek do
|
21
|
+
abort 'Reek is not available. In order to run reek, you must: gem install reek'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
begin
|
26
|
+
require 'roodi'
|
27
|
+
require 'roodi_task'
|
28
|
+
|
29
|
+
RoodiTask.new do |t|
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
rescue LoadError
|
33
|
+
task :roodi do
|
34
|
+
abort 'Roodi is not available. In order to run roodi, you must: gem install roodi'
|
35
|
+
end
|
36
|
+
end
|
data/tasks/spec.rake
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
spec_defaults = lambda do |spec|
|
2
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
3
|
+
spec.libs << 'lib' << 'spec'
|
4
|
+
spec.spec_opts << '--options' << 'spec/spec.opts'
|
5
|
+
end
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'spec/rake/spectask'
|
9
|
+
|
10
|
+
Spec::Rake::SpecTask.new(:spec, &spec_defaults)
|
11
|
+
rescue LoadError
|
12
|
+
task :spec do
|
13
|
+
abort 'rspec is not available. In order to run spec, you must: gem install rspec'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
require 'rcov'
|
19
|
+
require 'spec/rake/verify_rcov'
|
20
|
+
|
21
|
+
Spec::Rake::SpecTask.new(:rcov) do |rcov|
|
22
|
+
spec_defaults.call(rcov)
|
23
|
+
rcov.rcov = true
|
24
|
+
rcov.rcov_opts = File.read('spec/rcov.opts').split(/\s+/)
|
25
|
+
end
|
26
|
+
|
27
|
+
RCov::VerifyTask.new(:verify_rcov => :rcov) do |rcov|
|
28
|
+
rcov.threshold = 100
|
29
|
+
end
|
30
|
+
rescue LoadError
|
31
|
+
%w[ rcov verify_rcov ].each do |name|
|
32
|
+
task name do
|
33
|
+
abort "rcov is not available. In order to run #{name}, you must: gem install rcov"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
task :spec => :check_dependencies
|
39
|
+
task :rcov => :check_dependencies
|
40
|
+
|
41
|
+
task :default => :spec
|
data/tasks/yard.rake
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
begin
|
2
|
+
require 'pathname'
|
3
|
+
require 'yardstick/rake/measurement'
|
4
|
+
require 'yardstick/rake/verify'
|
5
|
+
|
6
|
+
# yardstick_measure task
|
7
|
+
Yardstick::Rake::Measurement.new
|
8
|
+
|
9
|
+
# verify_measurements task
|
10
|
+
Yardstick::Rake::Verify.new do |verify|
|
11
|
+
verify.threshold = 100
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
%w[ yardstick_measure verify_measurements ].each do |name|
|
15
|
+
task name.to_s do
|
16
|
+
abort "Yardstick is not available. In order to run #{name}, you must: gem install yardstick"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dm-is-slug
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.10.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aaron Qian
|
8
|
+
- James Herdman
|
9
|
+
- Nik Radford
|
10
|
+
- Paul
|
11
|
+
- Mike Frawley
|
12
|
+
- Alex Makuta
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-02-07 00:00:00 +08:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: dm-core
|
22
|
+
type: :runtime
|
23
|
+
version_requirement:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: 0.10.2
|
29
|
+
version:
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: unidecode
|
32
|
+
type: :runtime
|
33
|
+
version_requirement:
|
34
|
+
version_requirements: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ~>
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 1.0.0
|
39
|
+
version:
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rspec
|
42
|
+
type: :development
|
43
|
+
version_requirement:
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ~>
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "1.3"
|
49
|
+
version:
|
50
|
+
description: DataMapper plugin that generates unique slugs
|
51
|
+
email:
|
52
|
+
- aq1018@gmail.com
|
53
|
+
- james.herdman@gmail.com
|
54
|
+
- nik [a] terminaldischarge [d] net
|
55
|
+
- maverick.stoklosa@gmail.com
|
56
|
+
- frawl021@gmail.com
|
57
|
+
- cheba@pointlessone.org
|
58
|
+
executables: []
|
59
|
+
|
60
|
+
extensions: []
|
61
|
+
|
62
|
+
extra_rdoc_files:
|
63
|
+
- LICENSE
|
64
|
+
- README.textile
|
65
|
+
- TODO
|
66
|
+
files:
|
67
|
+
- .gitignore
|
68
|
+
- LICENSE
|
69
|
+
- README.textile
|
70
|
+
- Rakefile
|
71
|
+
- TODO
|
72
|
+
- VERSION
|
73
|
+
- dm-is-slug.gemspec
|
74
|
+
- lib/dm-is-slug.rb
|
75
|
+
- lib/dm-is-slug/is/slug.rb
|
76
|
+
- spec/integration/slug_spec.rb
|
77
|
+
- spec/rcov.opts
|
78
|
+
- spec/spec.opts
|
79
|
+
- spec/spec_helper.rb
|
80
|
+
- tasks/ci.rake
|
81
|
+
- tasks/metrics.rake
|
82
|
+
- tasks/spec.rake
|
83
|
+
- tasks/yard.rake
|
84
|
+
- tasks/yardstick.rake
|
85
|
+
has_rdoc: true
|
86
|
+
homepage: http://github.com/aq1018/dm-is-slug
|
87
|
+
licenses: []
|
88
|
+
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options:
|
91
|
+
- --charset=UTF-8
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: "0"
|
99
|
+
version:
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: "0"
|
105
|
+
version:
|
106
|
+
requirements: []
|
107
|
+
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 1.3.5
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: DataMapper plugin that generates unique slugs
|
113
|
+
test_files:
|
114
|
+
- spec/integration/slug_spec.rb
|
115
|
+
- spec/spec_helper.rb
|