kieran-dm-is-slug 0.9.11
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/History.txt +1 -0
- data/LICENSE +20 -0
- data/Manifest.txt +12 -0
- data/README.markdown +150 -0
- data/Rakefile +54 -0
- data/TODO +4 -0
- data/lib/dm-is-slug.rb +36 -0
- data/lib/dm-is-slug/is/slug.rb +207 -0
- data/lib/dm-is-slug/is/version.rb +7 -0
- data/spec/integration/slug_spec.rb +180 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +28 -0
- metadata +87 -0
data/History.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
|
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/Manifest.txt
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
dm-is-slug
|
2
|
+
==========
|
3
|
+
|
4
|
+
|
5
|
+
DataMapper plugin for creating and slugs (and permalinks).
|
6
|
+
|
7
|
+
|
8
|
+
Basics
|
9
|
+
------
|
10
|
+
|
11
|
+
Slugs are unique identifiers in a url that endeavour to be more human-readable.
|
12
|
+
|
13
|
+
Say you have a blog where you like to write about Ruby (it's a long shot, I know). You've just written an insigtful expository on DataMapper, and you're ready to pass that URL around for people to read.
|
14
|
+
|
15
|
+
This gem will turn:
|
16
|
+
|
17
|
+
/category/4/posts/12
|
18
|
+
|
19
|
+
into:
|
20
|
+
|
21
|
+
/category/ruby/posts/datamapper_is_the_bees_knees
|
22
|
+
|
23
|
+
Now the whole world will know exactly what you're sending them. Isn't that nice? The answer is "yes".
|
24
|
+
|
25
|
+
|
26
|
+
Getting started
|
27
|
+
---------------
|
28
|
+
|
29
|
+
Let's say we have a post-class, and we want to generate permalinks or slugs for all posts.
|
30
|
+
|
31
|
+
class Post
|
32
|
+
include DataMapper::Resource
|
33
|
+
|
34
|
+
property :id, Serial
|
35
|
+
property :title, String
|
36
|
+
property :content, Text
|
37
|
+
|
38
|
+
belongs_to :user
|
39
|
+
|
40
|
+
# here we define that it should have a slug that uses title as the slug
|
41
|
+
# it will generate an extra slug property of String type, with the same size as title
|
42
|
+
is :slug, :source => :title
|
43
|
+
end
|
44
|
+
|
45
|
+
Let's Say we need to define a slug based on a method instead of a property.
|
46
|
+
|
47
|
+
class User
|
48
|
+
include DataMapper::Resource
|
49
|
+
|
50
|
+
property :id, Serial
|
51
|
+
property :email, String
|
52
|
+
property :password, String
|
53
|
+
|
54
|
+
has n, :posts
|
55
|
+
|
56
|
+
# we only want to strip out the domain name
|
57
|
+
# and use only the email account name as the permalink
|
58
|
+
def slug_for_email
|
59
|
+
email.split("@").first
|
60
|
+
end
|
61
|
+
|
62
|
+
# here we define that it should have a slug that uses title as the permalink
|
63
|
+
# it will generate an extra slug property of String type, with the same size as title
|
64
|
+
is :slug, :source => :slug_for_email, :size => 255
|
65
|
+
end
|
66
|
+
|
67
|
+
You can now find objects by slug like this:
|
68
|
+
|
69
|
+
post = Post.first(:slug => "your_slug")
|
70
|
+
|
71
|
+
|
72
|
+
Merb routes
|
73
|
+
-----------
|
74
|
+
|
75
|
+
Building pretty routes in Merb is dead simple, especially with all this slugtacular mojo going on.
|
76
|
+
|
77
|
+
Here's a quick example route:
|
78
|
+
|
79
|
+
match("/posts/:slug").to(:controller=>:posts,:action=:index).name(:post)
|
80
|
+
|
81
|
+
Let's make a quick post to our fictional blog:
|
82
|
+
|
83
|
+
Post.create(:title => "Pretty URLs in Merb are easy", :content => "I feel like a good person for making the Internets pretty.")
|
84
|
+
|
85
|
+
Now the URL generation is as easy as:
|
86
|
+
|
87
|
+
url(:post,@post) #=> /posts/pretty_urls_in_merb_are_easy
|
88
|
+
|
89
|
+
Pretty slick, no?
|
90
|
+
|
91
|
+
|
92
|
+
Plays nice with nested routes and resources
|
93
|
+
-------------------------------------------
|
94
|
+
|
95
|
+
Say we want to have pretty URLs for a forum (and who wouldn't?)
|
96
|
+
|
97
|
+
First, let's set up a couple of models:
|
98
|
+
|
99
|
+
class Forum
|
100
|
+
include DataMapper::Resource
|
101
|
+
|
102
|
+
property :id, Serial
|
103
|
+
property :name, String
|
104
|
+
property :description, Text
|
105
|
+
|
106
|
+
is :slug, :source => :name
|
107
|
+
end
|
108
|
+
|
109
|
+
class Topic
|
110
|
+
include DataMapper::Resource
|
111
|
+
|
112
|
+
property :id, Serial
|
113
|
+
property :subject, String
|
114
|
+
property :description, Text
|
115
|
+
|
116
|
+
is :slug, :source => :subject
|
117
|
+
end
|
118
|
+
|
119
|
+
OK, now we'll make some nested resources in Merb in router.rb:
|
120
|
+
|
121
|
+
resources :forums, :identify => :forum_slug do
|
122
|
+
resources :topics, :identify => :topic_slug
|
123
|
+
end
|
124
|
+
|
125
|
+
This will generate CRUD routes that match the following pattern:
|
126
|
+
|
127
|
+
/forums/:forum_slug/topics/:topic_slug
|
128
|
+
|
129
|
+
Since \#forum\_slug and \#topic\_slug are aliases for \#slug (both getters & setters), in your resource controller you can get the models via:
|
130
|
+
|
131
|
+
@forum = Forum.get(params[:forum_slug])
|
132
|
+
|
133
|
+
@topic = Topic.get(params[:topic_slug])
|
134
|
+
|
135
|
+
and generating the URL is as easy as:
|
136
|
+
|
137
|
+
resource(@forum,@topic)
|
138
|
+
|
139
|
+
|
140
|
+
Mutability
|
141
|
+
----------
|
142
|
+
|
143
|
+
By default, all slugs are mutable. That is, they can (and will) be updated when the source property changes.
|
144
|
+
|
145
|
+
If you want to make a slug immutable (a permalink), you can pass :mutable => false as an option:
|
146
|
+
|
147
|
+
is :slug, :source => :name, :mutable => false
|
148
|
+
|
149
|
+
Now your slug will never change once created.
|
150
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
ROOT = Pathname(__FILE__).dirname.expand_path
|
9
|
+
require ROOT + 'lib/dm-is-slug/is/version'
|
10
|
+
|
11
|
+
AUTHOR = "Aaron Qian, Nik Radford"
|
12
|
+
EMAIL = "aaron [a] ekohe [d] com; nik [a] terminaldischarge [d] net"
|
13
|
+
GEM_NAME = "dm-is-slug"
|
14
|
+
GEM_VERSION = DataMapper::Is::Slug::VERSION
|
15
|
+
GEM_DEPENDENCIES = [["dm-core", "~>0.9"]]
|
16
|
+
GEM_CLEAN = ["log", "pkg"]
|
17
|
+
GEM_EXTRAS = { :has_rdoc => false }
|
18
|
+
|
19
|
+
PROJECT_NAME = "dm-is-slug"
|
20
|
+
PROJECT_URL = "http://github.com/aq1018/dm-is-slug"
|
21
|
+
PROJECT_DESCRIPTION = PROJECT_SUMMARY = "DataMapper plugin that generates unique slugs"
|
22
|
+
|
23
|
+
require 'tasks/hoe'
|
24
|
+
|
25
|
+
task :default => [ :spec ]
|
26
|
+
|
27
|
+
WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
|
28
|
+
SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
|
29
|
+
|
30
|
+
desc "Install #{GEM_NAME} #{GEM_VERSION}"
|
31
|
+
task :install => [ :package ] do
|
32
|
+
sh "#{SUDO} gem install --local pkg/#{GEM_NAME}-#{GEM_VERSION} --no-update-sources", :verbose => false
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Uninstall #{GEM_NAME} #{GEM_VERSION} (default ruby)"
|
36
|
+
task :uninstall => [ :clobber ] do
|
37
|
+
sh "#{SUDO} gem uninstall #{GEM_NAME} -v#{GEM_VERSION} -I -x", :verbose => false
|
38
|
+
end
|
39
|
+
|
40
|
+
desc 'Run specifications'
|
41
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
42
|
+
t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
|
43
|
+
t.spec_files = Pathname.glob(Pathname.new(__FILE__).dirname + 'spec/**/*_spec.rb')
|
44
|
+
|
45
|
+
begin
|
46
|
+
t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
|
47
|
+
t.rcov_opts << '--exclude' << 'spec'
|
48
|
+
t.rcov_opts << '--text-summary'
|
49
|
+
t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
|
50
|
+
rescue Exception
|
51
|
+
puts 'rcov is not installed. Please install before continuing'
|
52
|
+
exit
|
53
|
+
end
|
54
|
+
end
|
data/lib/dm-is-slug.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# Needed to import datamapper and other gems
|
2
|
+
require 'rubygems'
|
3
|
+
require 'pathname'
|
4
|
+
require 'iconv'
|
5
|
+
|
6
|
+
# Add all external dependencies for the plugin here
|
7
|
+
gem 'dm-core', '~>0.9.9'
|
8
|
+
require 'dm-core'
|
9
|
+
|
10
|
+
require Pathname(__FILE__).dirname.expand_path / 'dm-is-slug' / 'is' / 'version.rb'
|
11
|
+
|
12
|
+
# Require plugin-files
|
13
|
+
require Pathname(__FILE__).dirname.expand_path / 'dm-is-slug' / 'is' / 'slug.rb'
|
14
|
+
|
15
|
+
# Include the plugin in Resource
|
16
|
+
module DataMapper
|
17
|
+
module Resource
|
18
|
+
module ClassMethods
|
19
|
+
include DataMapper::Is::Slug
|
20
|
+
end # module ClassMethods
|
21
|
+
end # module Resource
|
22
|
+
end # module DataMapper
|
23
|
+
|
24
|
+
# Include DataMapper::Model#get and DataMapper::Collection#get override
|
25
|
+
# So we do user.posts.get("my-shinny-new-post")
|
26
|
+
|
27
|
+
module DataMapper
|
28
|
+
module Model
|
29
|
+
include DataMapper::Is::Slug::AliasMethods
|
30
|
+
end
|
31
|
+
|
32
|
+
class Collection
|
33
|
+
include DataMapper::Is::Slug::AliasMethods
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,207 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Is
|
3
|
+
module Slug
|
4
|
+
class InvalidSlugSource < Exception
|
5
|
+
end
|
6
|
+
|
7
|
+
DEFAULT_SLUG_SIZE = 50
|
8
|
+
|
9
|
+
DEFAULT_SLUG_OPTIONS = {
|
10
|
+
:mutable => true,
|
11
|
+
:separator => '_'
|
12
|
+
}
|
13
|
+
|
14
|
+
##
|
15
|
+
# Overriding the default Slug separator "-" with "_"
|
16
|
+
# I find it makes URIs much more readable
|
17
|
+
# method from dm-types/slug
|
18
|
+
##
|
19
|
+
|
20
|
+
# @param [String] str A string to escape for use as a slug
|
21
|
+
# @return [String] an URL-safe string
|
22
|
+
def self.escape(string)
|
23
|
+
separator = DEFAULT_SLUG_OPTIONS[:separator]
|
24
|
+
|
25
|
+
# swap accented characters with their counterparts
|
26
|
+
string.gsub!(/[èÈééÉêÊëË]/,'e')
|
27
|
+
string.gsub!(/[àÀáÁâÂãÃäÄåÅ]/,'a')
|
28
|
+
string.gsub!(/[ìÌíÍîÎïÏ]/,'i')
|
29
|
+
string.gsub!(/[òÒóÓöÖôÔõÕøØ]/,'o')
|
30
|
+
string.gsub!(/[ùÙúÚûÛüÜ]/,'u')
|
31
|
+
string.gsub!(/[ýÝÿ]/,'y')
|
32
|
+
string.gsub!(/[çÇ]/,'c')
|
33
|
+
string.gsub!(/[ñÑ]/,'n')
|
34
|
+
string.gsub!(/[Ð]/,'d')
|
35
|
+
|
36
|
+
result = Iconv.iconv('ascii//translit//IGNORE', 'utf-8', string).to_s
|
37
|
+
result.gsub!(/[^\x00-\x7F]+/, '') # Remove anything non-ASCII entirely (e.g. diacritics).
|
38
|
+
result.strip!
|
39
|
+
result.downcase!
|
40
|
+
result.gsub!(/\b&\b/, 'and') # Change & to a more slug-friendly character.
|
41
|
+
result.gsub!(/[^\w_ \-]+/i, '') # Remove unwanted chars.
|
42
|
+
result.gsub!(/\ +/, separator) # contract multiple spaces.
|
43
|
+
result.gsub!(Regexp.new("#{separator}+"), separator) # No more than one separator in a row. # result.gsub!(/_+/i, separator)
|
44
|
+
result.gsub!(Regexp.new("^#{separator}|#{separator}$"), separator) # Remove leading/trailing separators. # result.gsub!(/^_|_$/i, '')
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Methods that should be included in DataMapper::Model.
|
50
|
+
# Normally this should just be your generator, so that the namespace
|
51
|
+
# does not get cluttered. ClassMethods and InstanceMethods gets added
|
52
|
+
# in the specific resources when you fire is :slug
|
53
|
+
##
|
54
|
+
|
55
|
+
# Defines a +slug+ property on your model with the same size as your
|
56
|
+
# source property. This property is Unicode escaped, and treated so as
|
57
|
+
# to be fit for use in URLs.
|
58
|
+
#
|
59
|
+
# ==== Example
|
60
|
+
# Suppose your source attribute was the following string: "Hot deals on
|
61
|
+
# Boxing Day". This string would be escaped to "hot_deals_on_boxing_day".
|
62
|
+
#
|
63
|
+
# Non-ASCII characters are attempted to be converted to their nearest
|
64
|
+
# approximate.
|
65
|
+
#
|
66
|
+
# ==== Parameters
|
67
|
+
# +mutable+::
|
68
|
+
# If a slug is mutable it will be updated as the source field changes.
|
69
|
+
# Setting this to false will make the slug immutable (permanent)
|
70
|
+
# +source+::
|
71
|
+
# The property on the model to use as the source of the generated slug,
|
72
|
+
# or an instance method defined in the model, the method must return
|
73
|
+
# a string or nil.
|
74
|
+
# +size+::
|
75
|
+
# The length of the +slug+ property
|
76
|
+
#
|
77
|
+
# @param [Hash] provide options in a Hash. See *Parameters* for details
|
78
|
+
def is_slug(options)
|
79
|
+
extend DataMapper::Is::Slug::ClassMethods
|
80
|
+
include DataMapper::Is::Slug::InstanceMethods
|
81
|
+
|
82
|
+
@slug_options = DEFAULT_SLUG_OPTIONS.merge(options)
|
83
|
+
raise InvalidSlugSource('You must specify a :source to generate slug.') unless slug_source
|
84
|
+
|
85
|
+
slug_options[:size] ||= get_slug_size
|
86
|
+
property(:slug, String, :size => slug_options[:size], :unique => true) unless slug_property
|
87
|
+
before :save, :generate_slug
|
88
|
+
|
89
|
+
# add alternate slug names for nested resources
|
90
|
+
# e.g. /forums/:forum_slug/topics/:topic_slug/
|
91
|
+
class_eval <<-SLUG
|
92
|
+
def #{self.new.class.to_s.snake_case}_slug
|
93
|
+
slug
|
94
|
+
end
|
95
|
+
def #{self.new.class.to_s.snake_case}_slug=(str)
|
96
|
+
self.slug = str
|
97
|
+
end
|
98
|
+
SLUG
|
99
|
+
end
|
100
|
+
|
101
|
+
module ClassMethods
|
102
|
+
attr_reader :slug_options
|
103
|
+
|
104
|
+
def slug_mutable?
|
105
|
+
slug_options[:mutable]
|
106
|
+
end
|
107
|
+
|
108
|
+
def slug_source
|
109
|
+
slug_options[:source] ? slug_options[:source].to_sym : nil
|
110
|
+
end
|
111
|
+
|
112
|
+
def slug_source_property
|
113
|
+
detect_slug_property_by_name(slug_source)
|
114
|
+
end
|
115
|
+
|
116
|
+
def slug_property
|
117
|
+
detect_slug_property_by_name(:slug)
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def detect_slug_property_by_name(name)
|
123
|
+
properties.detect do |p|
|
124
|
+
p.name == name && p.type == String
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_slug_size
|
129
|
+
slug_source_property && slug_source_property.size || DataMapper::Is::Slug::DEFAULT_SLUG_SIZE
|
130
|
+
end
|
131
|
+
end # ClassMethods
|
132
|
+
|
133
|
+
module InstanceMethods
|
134
|
+
def to_param
|
135
|
+
[slug]
|
136
|
+
end
|
137
|
+
|
138
|
+
def slug_mutable?
|
139
|
+
self.class.slug_mutable?
|
140
|
+
end
|
141
|
+
|
142
|
+
def slug_source
|
143
|
+
self.class.slug_source
|
144
|
+
end
|
145
|
+
|
146
|
+
def slug_source_property
|
147
|
+
self.class.slug_source_property
|
148
|
+
end
|
149
|
+
|
150
|
+
def slug_property
|
151
|
+
self.class.slug_property
|
152
|
+
end
|
153
|
+
|
154
|
+
def slug_source_value
|
155
|
+
self.send(slug_source)
|
156
|
+
end
|
157
|
+
|
158
|
+
# The slug is not stale if
|
159
|
+
# 1. the slug is permanent, and slug column has something valid in it
|
160
|
+
# 2. the slug source value is nil or empty
|
161
|
+
def stale_slug?
|
162
|
+
!((!slug_mutable? && slug && !slug.empty?) || (slug_source_value.nil? || slug_source_value.empty?))
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def make_unique_slug!
|
168
|
+
unique_slug = DataMapper::Is::Slug.escape(slug_source_value)
|
169
|
+
|
170
|
+
# see if there are other records with the same slug (enables #save!)
|
171
|
+
unique_slug = "#{unique_slug}-2" if self.class.first(:slug => unique_slug, :id.not => self.id)
|
172
|
+
|
173
|
+
while(self.class.first(:slug => unique_slug, :id.not => self.id))
|
174
|
+
i = unique_slug[-1..-1].to_i + 1
|
175
|
+
unique_slug = unique_slug[0..-2] + i.to_s
|
176
|
+
end
|
177
|
+
unique_slug
|
178
|
+
end
|
179
|
+
|
180
|
+
def generate_slug
|
181
|
+
raise InvalidSlugSource('Invalid slug source.') unless slug_source_property || self.respond_to?(slug_source)
|
182
|
+
return unless stale_slug?
|
183
|
+
self.slug = make_unique_slug!
|
184
|
+
end
|
185
|
+
end # InstanceMethods
|
186
|
+
|
187
|
+
module AliasMethods
|
188
|
+
# override the old get method so that it looks for slugs first
|
189
|
+
# and call the old get if slug is not found
|
190
|
+
def get_with_slug(*key)
|
191
|
+
if respond_to?(:slug_options) && slug_options && key[0].to_s.to_i.to_s != key[0].to_s
|
192
|
+
first(:slug => key[0])
|
193
|
+
else
|
194
|
+
get_without_slug(*key)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
##
|
199
|
+
# fired when your plugin gets included into Resource
|
200
|
+
def self.included(base)
|
201
|
+
base.send :alias_method, :get_without_slug, :get
|
202
|
+
base.send :alias_method, :get, :get_with_slug
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end # Slug
|
206
|
+
end # Is
|
207
|
+
end # DataMapper
|
@@ -0,0 +1,180 @@
|
|
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, :size => 80
|
20
|
+
end
|
21
|
+
|
22
|
+
class Post
|
23
|
+
include DataMapper::Resource
|
24
|
+
|
25
|
+
property :id, Serial
|
26
|
+
property :title, String, :size => 2000
|
27
|
+
property :content, Text
|
28
|
+
|
29
|
+
belongs_to :user
|
30
|
+
|
31
|
+
is :slug, :source => :title, :mutable => false
|
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
|
+
before :all do
|
43
|
+
User.auto_migrate!(:default)
|
44
|
+
Post.auto_migrate!(:default)
|
45
|
+
Todo.auto_migrate!(:default)
|
46
|
+
|
47
|
+
@u1 = User.create(:email => "john@ekohe.com")
|
48
|
+
@p1 = Post.create(:user => @u1, :title => "My first shiny blog post")
|
49
|
+
@p2 = Post.create(:user => @u1, :title => "My second shiny blog post")
|
50
|
+
@p3 = Post.create(:user => @u1, :title => "My third shiny blog post")
|
51
|
+
|
52
|
+
@u2 = User.create(:email => "john@someotherplace.com")
|
53
|
+
@p4 = Post.create(:user => @u2, :title => "My first Shiny blog post")
|
54
|
+
@p5 = Post.create(:user => @u2, :title => "i heart merb and dm")
|
55
|
+
@p6 = Post.create(:user => @u2, :title => "another productive day!!")
|
56
|
+
@p7 = Post.create(:user => @u2, :title => "another productive day!!")
|
57
|
+
@p8 = Post.create(:user => @u2, :title => "another productive day!!")
|
58
|
+
@p9 = Post.create(:user => @u2, :title => "another productive day!!")
|
59
|
+
@p10 = Post.create(:user => @u2, :title => "another productive day!!")
|
60
|
+
@p11 = Post.create(:user => @u2, :title => "another productive day!!")
|
61
|
+
@p12 = Post.create(:user => @u2, :title => "another productive day!!")
|
62
|
+
@p13 = Post.create(:user => @u2, :title => "another productive day!!")
|
63
|
+
@p14 = Post.create(:user => @u2, :title => "another productive day!!")
|
64
|
+
@p15 = Post.create(:user => @u2, :title => "another productive day!!")
|
65
|
+
@p16 = Post.create(:user => @u2, :title => "another productive day!!")
|
66
|
+
@p17 = Post.create(:user => @u2, :title => "A fancy café")
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should generate slugs" do
|
70
|
+
User.all.each do |u|
|
71
|
+
u.slug.should_not be_nil
|
72
|
+
end
|
73
|
+
|
74
|
+
Post.all.each do |p|
|
75
|
+
p.slug.should_not be_nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should generate unique slugs" do
|
80
|
+
@u1.slug.should_not == @u2.slug
|
81
|
+
@p1.slug.should_not == @p4.slug
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should generate correct slug for user" do
|
85
|
+
@u1.slug.should == "john"
|
86
|
+
@u2.slug.should == "john-2"
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should generate correct slug for post" do
|
90
|
+
@p1.slug.should == "my_first_shiny_blog_post"
|
91
|
+
@p2.slug.should == "my_second_shiny_blog_post"
|
92
|
+
@p3.slug.should == "my_third_shiny_blog_post"
|
93
|
+
@p4.slug.should == "my_first_shiny_blog_post-2"
|
94
|
+
@p5.slug.should == "i_heart_merb_and_dm"
|
95
|
+
@p6.slug.should == "another_productive_day"
|
96
|
+
@p7.slug.should == "another_productive_day-2"
|
97
|
+
@p8.slug.should == "another_productive_day-3"
|
98
|
+
@p9.slug.should == "another_productive_day-4"
|
99
|
+
@p10.slug.should == "another_productive_day-5"
|
100
|
+
@p11.slug.should == "another_productive_day-6"
|
101
|
+
@p12.slug.should == "another_productive_day-7"
|
102
|
+
@p13.slug.should == "another_productive_day-8"
|
103
|
+
@p14.slug.should == "another_productive_day-9"
|
104
|
+
@p15.slug.should == "another_productive_day-10"
|
105
|
+
@p16.slug.should == "another_productive_day-11"
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should update slug if :mutable => true or not specified" do
|
109
|
+
user = User.create(:email => "a_person@ekohe.com")
|
110
|
+
user.slug.should == "a_person"
|
111
|
+
|
112
|
+
user.should be_slug_mutable
|
113
|
+
|
114
|
+
user.email = "changed@ekohe.com"
|
115
|
+
user.should be_dirty
|
116
|
+
|
117
|
+
user.save.should be_true
|
118
|
+
user.slug.should == "changed"
|
119
|
+
user.destroy
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should not update slug if :mutable => false" do
|
123
|
+
post = Post.create(:user => @u1, :title => "hello world!")
|
124
|
+
post.slug.should == "hello_world"
|
125
|
+
post.should_not be_slug_mutable
|
126
|
+
post.title = "hello universe!"
|
127
|
+
post.should be_dirty
|
128
|
+
post.save.should be_true
|
129
|
+
post.slug.should == "hello_world"
|
130
|
+
post.destroy
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should have the right size for properties" do
|
134
|
+
user_slug_property = User.properties.detect{|p| p.name == :slug && p.type == String}
|
135
|
+
user_slug_property.should_not be_nil
|
136
|
+
user_slug_property.size.should == 80
|
137
|
+
|
138
|
+
Post.properties.detect{|p| p.name == :title && p.type == String}.size.should == 2000
|
139
|
+
post_slug_property = Post.properties.detect{|p| p.name == :slug && p.type == String}
|
140
|
+
post_slug_property.should_not be_nil
|
141
|
+
post_slug_property.size.should == 2000
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should find model using get method with slug" do
|
145
|
+
u = User.get("john")
|
146
|
+
u.should_not be_nil
|
147
|
+
u.should == @u1
|
148
|
+
|
149
|
+
Post.get("my_first_shiny_blog_post").should == @p1
|
150
|
+
@u1.posts.get("my_first_shiny_blog_post").should == @p1
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should output slug with to_param method" do
|
154
|
+
@u1.to_param.should == ["john"]
|
155
|
+
@p1.to_param.should == ["my_first_shiny_blog_post"]
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should find model using get method using id" do
|
159
|
+
u = User.get(@u1.id)
|
160
|
+
u.should_not be_nil
|
161
|
+
u.should == @u1
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should find model using get method using id with non-slug models" do
|
165
|
+
todo = Todo.create(:user => @u1, :title => "blabla")
|
166
|
+
todo.should_not be_nil
|
167
|
+
|
168
|
+
Todo.get(todo.id).should == todo
|
169
|
+
@u1.todos.get(todo.id).should == todo
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'should strip unicode characters from the slug' do
|
173
|
+
@p17.slug.should == 'a_fancy_cafe'
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'should have slug_property on instance' do
|
177
|
+
@p1.slug_property.should == @p1.class.properties.detect{|p| p.name == :slug}
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'rspec'#, '~>1.1.3'
|
3
|
+
require 'spec'
|
4
|
+
require 'pathname'
|
5
|
+
require Pathname(__FILE__).dirname.expand_path.parent + 'lib/dm-is-slug'
|
6
|
+
|
7
|
+
def load_driver(name, default_uri)
|
8
|
+
return false if ENV['ADAPTER'] != name.to_s
|
9
|
+
|
10
|
+
lib = "do_#{name}"
|
11
|
+
|
12
|
+
begin
|
13
|
+
gem lib, '~>0.9.7'
|
14
|
+
require lib
|
15
|
+
DataMapper.setup(name, ENV["#{name.to_s.upcase}_SPEC_URI"] || default_uri)
|
16
|
+
DataMapper::Repository.adapters[:default] = DataMapper::Repository.adapters[name]
|
17
|
+
true
|
18
|
+
rescue Gem::LoadError => e
|
19
|
+
warn "Could not load #{lib}: #{e}"
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ENV['ADAPTER'] ||= 'sqlite3'
|
25
|
+
|
26
|
+
HAS_SQLITE3 = load_driver(:sqlite3, 'sqlite3::memory:')
|
27
|
+
HAS_MYSQL = load_driver(:mysql, 'mysql://localhost/dm_core_test')
|
28
|
+
HAS_POSTGRES = load_driver(:postgres, 'postgres://postgres@localhost/dm_core_test')
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kieran-dm-is-slug
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.11
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aaron Qian, Nik Radford
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-07-12 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: dm-core
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0.9"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hoe
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.8.2
|
34
|
+
version:
|
35
|
+
description: DataMapper plugin that generates unique permalinks / slugs
|
36
|
+
email:
|
37
|
+
- aaron [a] ekohe [d] com; nik [a] terminaldischarge [d] net
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- History.txt
|
44
|
+
- Manifest.txt
|
45
|
+
- README.markdown
|
46
|
+
files:
|
47
|
+
- History.txt
|
48
|
+
- LICENSE
|
49
|
+
- Manifest.txt
|
50
|
+
- README.markdown
|
51
|
+
- Rakefile
|
52
|
+
- TODO
|
53
|
+
- lib/dm-is-slug.rb
|
54
|
+
- lib/dm-is-slug/is/slug.rb
|
55
|
+
- lib/dm-is-slug/is/version.rb
|
56
|
+
- spec/integration/slug_spec.rb
|
57
|
+
- spec/spec.opts
|
58
|
+
- spec/spec_helper.rb
|
59
|
+
has_rdoc: false
|
60
|
+
homepage: http://github.com/kieran/dm-is-slug
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options:
|
63
|
+
- --main
|
64
|
+
- README.markdown
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: "0"
|
72
|
+
version:
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: "0"
|
78
|
+
version:
|
79
|
+
requirements: []
|
80
|
+
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 1.2.0
|
83
|
+
signing_key:
|
84
|
+
specification_version: 2
|
85
|
+
summary: DataMapper plugin that generates unique permalinks / slugs
|
86
|
+
test_files: []
|
87
|
+
|