mongoid_slug 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Paper Cavalier
1
+ Copyright (c) 2010-2012 Hakan Ensari
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,22 +1,25 @@
1
1
  Mongoid Slug
2
2
  ============
3
3
 
4
- Mongoid Slug generates a URL slug or permalink based on one or more
5
- fields in a Mongoid model. It sits idly on top of [stringex] [1] and
6
- works with non-Latin characters.
4
+ Mongoid Slug generates a URL slug or permalink based on one or more fields in a
5
+ Mongoid model. It sits idly on top of [stringex] [1], supporting non-Latin
6
+ characters.
7
7
 
8
- [![travis](https://secure.travis-ci.org/hakanensari/mongoid-slug.png)](http://travis-ci.org/hakanensari/mongoid-slug)
8
+ [![travis] [2]] [3]
9
9
 
10
- Quick Start
11
- -----------
10
+ Installation
11
+ ------------
12
12
 
13
- Add mongoid_slug to your Gemfile:
13
+ Add to your Gemfile:
14
14
 
15
15
  ```ruby
16
16
  gem 'mongoid_slug'
17
17
  ```
18
18
 
19
- Set up some slugs:
19
+ Usage
20
+ -----
21
+
22
+ Set up a slug:
20
23
 
21
24
  ```ruby
22
25
  class Book
@@ -24,34 +27,18 @@ class Book
24
27
  include Mongoid::Slug
25
28
 
26
29
  field :title
27
- embeds_many :authors
28
-
29
30
  slug :title
30
31
  end
31
-
32
- class Author
33
- include Mongoid::Document
34
- include Mongoid::Slug
35
-
36
- field :first
37
- field :last
38
- embedded_in :book, :inverse_of => :authors
39
-
40
- slug :first, :last, :as => :name
41
- end
42
32
  ```
43
33
 
44
- In your controller, use available finders:
34
+ Find a record by its slug:
45
35
 
46
36
  ```ruby
47
- # GET /books/a-thousand-plateaus/authors/gilles-deleuze
48
- author = Book.find_by_slug(params[:book_id]).
49
- authors.
50
- find_by_name(params[:id])
37
+ # GET /books/a-thousand-plateaus
38
+ book = Book.find_by_slug params[:book_id]
51
39
  ```
52
40
 
53
- [Read here] [2]
54
- for all available options.
41
+ [Read here] [3] for all available options.
55
42
 
56
43
  Scoping
57
44
  -------
@@ -61,37 +48,89 @@ To scope a slug by a reference association, pass `:scope`:
61
48
  ```ruby
62
49
  class Company
63
50
  include Mongoid::Document
51
+
64
52
  references_many :employees
65
53
  end
66
54
 
67
55
  class Employee
68
- include Mongoid::Document
69
- include Mongoid::Slug
70
- field :name
71
- slug :name, :scope => :company
72
- referenced_in :company
56
+ include Mongoid::Document
57
+ include Mongoid::Slug
58
+
59
+ field :name
60
+ referenced_in :company
61
+
62
+ slug :name, :scope => :company
73
63
  end
74
64
  ```
75
65
 
76
- In this example, if you create an employee without associating it with
77
- any company, the scope will fall back to the root employees collection.
66
+ In this example, if you create an employee without associating it with any
67
+ company, the scope will fall back to the root employees collection.
78
68
 
79
- Currently, if you have an irregular association name, you **must**
80
- specify the `:inverse_of` option on the other side of the assocation.
69
+ Currently, if you have an irregular association name, you **must** specify the
70
+ `:inverse_of` option on the other side of the assocation.
81
71
 
82
72
  Embedded objects are automatically scoped by their parent.
83
73
 
84
- If the value of `:scope` is not an association, it should be the name of a field within the model itself:
74
+ The value of `:scope` can alternatively be a field within the model itself:
85
75
 
86
76
  ```ruby
87
77
  class Employee
88
- include Mongoid::Document
89
- include Mongoid::Slug
90
- field :name
91
- field :company_id
92
- slug :name, :scope => :company_id
78
+ include Mongoid::Document
79
+ include Mongoid::Slug
80
+
81
+ field :name
82
+ field :company_id
83
+
84
+ slug :name, :scope => :company_id
85
+ end
86
+ ```
87
+
88
+ History
89
+ -------
90
+
91
+ To specify that the history of a document should be kept track of, pass
92
+ `:history` with a value of `true`.
93
+
94
+ ```ruby
95
+ class Page
96
+ include Mongoid::Document
97
+ include Mongoid::Slug
98
+
99
+ field :title
100
+
101
+ slug :title, history: true
102
+ end
103
+ ```
104
+
105
+ The document will then be returned for any of the saved slugs:
106
+
107
+ ```ruby
108
+ page = Page.new title: "Home"
109
+ page.save
110
+ page.update_attributes title: "Welcome"
111
+
112
+ Page.find_by_slug("welcome") == Page.find_by_slug("home") #=> true
113
+ ```
114
+
115
+ Reserved Slugs
116
+ --------------
117
+
118
+ Pass words you do not want to be slugged using the `reserve` option:
119
+
120
+ ```ruby
121
+ class Friend
122
+ include Mongoid::Document
123
+
124
+ field :name
125
+ slug :name, reserve: ['admin', 'root']
93
126
  end
127
+
128
+ friend = Friend.create name: 'admin'
129
+ Friend.find_by_slug('admin') # => nil
130
+ friend.slug # => 'admin-1'
94
131
  ```
95
132
 
96
133
  [1]: https://github.com/rsl/stringex/
97
- [2]: https://github.com/hakanensari/mongoid-slug/blob/master/lib/mongoid/slug.rb
134
+ [2]: https://secure.travis-ci.org/hakanensari/mongoid-slug.png
135
+ [3]: http://travis-ci.org/hakanensari/mongoid-slug
136
+ [4]: https://github.com/hakanensari/mongoid-slug/blob/master/lib/mongoid/slug.rb
data/lib/mongoid/slug.rb CHANGED
@@ -1,213 +1,353 @@
1
1
  require 'mongoid'
2
2
  require 'stringex'
3
3
 
4
- module Mongoid #:nodoc:
5
-
6
- # The slug module helps you generate a URL slug or permalink based on
7
- # one or more fields in a Mongoid model.
8
- #
9
- # class Person
10
- # include Mongoid::Document
11
- # include Mongoid::Slug
12
- #
13
- # field :name
14
- # slug :name
15
- # end
16
- #
4
+ module Mongoid
5
+ # The Slug module helps you generate a URL slug or permalink based on one or
6
+ # more fields in a Mongoid model.
17
7
  module Slug
18
8
  extend ActiveSupport::Concern
19
9
 
20
10
  included do
21
11
  cattr_accessor :slug_builder,
22
- :slugged_fields,
23
12
  :slug_name,
24
- :slug_scope
13
+ :slug_history_name,
14
+ :slug_scope,
15
+ :reserved_words_in_slug,
16
+ :slugged_attributes
25
17
  end
26
18
 
27
19
  module ClassMethods
28
-
29
- # Sets one ore more fields as source of slug.
30
- #
31
- # Takes a list of fields to slug and an optional options hash.
32
- #
33
- # The options hash respects the following members:
34
- #
35
- # * `:as`, which specifies name of the field that stores the
36
- # slug. Defaults to `slug`.
37
- #
38
- # * `:scope`, which specifies a reference association to scope
39
- # the slug by. Embedded documents are by default scoped by their
40
- # parent.
41
- #
42
- # * `:permanent`, which specifies whether the slug should be
43
- # immutable once created. Defaults to `false`.
20
+ # @overload slug(*fields)
21
+ # Sets one ore more fields as source of slug.
22
+ # @param [Array] fields One or more fields the slug should be based on.
23
+ # @yield If given, the block is used to build a custom slug.
44
24
  #
45
- # * `:index`, which specifies whether an index should be defined
46
- # for the slug. Defaults to `false` and has no effect if the
47
- # document is embedded. Make sure you have a unique index on the
48
- # slug of root documents to avoid the (very unlikely) race
49
- # condition that would ensue if two documents with identical
50
- # slugs were to be saved simultaneously.
25
+ # @overload slug(*fields, options)
26
+ # Sets one ore more fields as source of slug.
27
+ # @param [Array] fields One or more fields the slug should be based on.
28
+ # @param [Hash] options
29
+ # @param options [String] :as The name of the field that stores the
30
+ # slug. Defaults to `slug`.
31
+ # @param options [Boolean] :history Whether a history of changes to
32
+ # the slug should be retained. When searched by slug, the document now
33
+ # matches both past and present slugs.
34
+ # @param options [Boolean] :index Whether an index should be defined
35
+ # on the slug field. Defaults to `false` and has no effect if the
36
+ # document is embedded.
37
+ # Make sure you have a unique index on the slugs of root documents to
38
+ # avoid race conditions.
39
+ # @param options [Boolean] :permanent Whether the slug should be
40
+ # immutable. Defaults to `false`.
41
+ # @param options [Array] :reserve` A list of reserved slugs
42
+ # @param options :scope [Symbol] a reference association or field to
43
+ # scope the slug by. Embedded documents are, by default, scoped by
44
+ # their parent.
45
+ # @yield If given, a block is used to build a slug.
51
46
  #
52
- # Alternatively, this method can be given a block to build a
53
- # custom slug out of the specified fields.
47
+ # @example A custom builder
48
+ # class Person
49
+ # include Mongoid::Document
50
+ # include Mongoid::Slug
54
51
  #
55
- # The block takes a single argument, the document itself, and
56
- # should return a string that will serve as the base of the slug.
57
- #
58
- # Here, for instance, we slug an array field.
59
- #
60
- # class Person
61
- # include Mongoid::Document
62
- # include Mongoid::Slug
63
- #
64
- # field :names, :type => Array
65
- # slug :names do |doc|
66
- # doc.names.join(' ')
67
- # end
52
+ # field :names, :type => Array
53
+ # slug :names do |doc|
54
+ # doc.names.join(' ')
55
+ # end
56
+ # end
68
57
  #
69
58
  def slug(*fields, &block)
70
- options = fields.extract_options!
71
- self.slug_scope = options[:scope]
72
- self.slug_name = options[:as] || :slug
73
- self.slugged_fields = fields.map(&:to_s)
74
-
75
- self.slug_builder =
76
- if block_given?
77
- block
78
- else
79
- lambda do |doc|
80
- slugged_fields.map { |f| doc.read_attribute(f) }.
81
- join(' ')
82
- end
83
- end
59
+ options = fields.extract_options!
60
+ options[:history] = false if options[:permanent]
61
+
62
+ self.slug_scope = options[:scope]
63
+ self.reserved_words_in_slug = options[:reserve] || []
64
+ self.slug_name = options[:as] || :slug
65
+ self.slugged_attributes = fields.map(&:to_s)
66
+ if options[:history] && !options[:permanent]
67
+ self.slug_history_name = "#{self.slug_name}_history".to_sym
68
+ end
69
+
70
+ default_builder = lambda do |doc|
71
+ slugged_attributes.map { |f| doc.send f }.join ' '
72
+ end
73
+ self.slug_builder = block_given? ? block : default_builder
84
74
 
85
75
  field slug_name
86
76
 
77
+ if slug_history_name
78
+ field slug_history_name, :type => Array, :default => []
79
+ end
80
+
87
81
  if options[:index]
88
- index(slug_name, :unique => !slug_scope)
82
+ index slug_name, :unique => !slug_scope
83
+ index slug_history_name if slug_history_name
89
84
  end
90
85
 
91
- if options[:permanent]
92
- before_create :generate_slug
93
- else
94
- before_save :generate_slug
86
+ set_callback options[:permanent] ? :create : :save, :before do |doc|
87
+ doc.build_slug if doc.slug_should_be_rebuilt?
95
88
  end
96
89
 
97
- # Build a finder based on the slug name.
90
+ # Build a finder for slug.
98
91
  #
99
92
  # Defaults to `find_by_slug`.
100
93
  instance_eval <<-CODE
101
94
  def self.find_by_#{slug_name}(slug)
102
- where(slug_name => slug).first
95
+ if slug_history_name
96
+ any_of({ slug_name => slug }, { slug_history_name => slug })
97
+ else
98
+ where(slug_name => slug)
99
+ end.first
103
100
  end
104
101
 
105
102
  def self.find_by_#{slug_name}!(slug)
106
- where(slug_name => slug).first ||
107
- raise(Mongoid::Errors::DocumentNotFound.new(self, slug))
103
+ self.find_by_#{slug_name}(slug) ||
104
+ raise(Mongoid::Errors::DocumentNotFound.new self, slug)
108
105
  end
109
106
  CODE
110
- end
111
- end
112
107
 
113
- # Returns the slug.
114
- def to_param
115
- read_attribute(slug_name) || begin
116
- generate_slug!
117
- save
118
- read_attribute(slug_name)
108
+ # Build a scope based on the slug name.
109
+ #
110
+ # Defaults to `by_slug`.
111
+ scope "by_#{slug_name}".to_sym, lambda { |slug|
112
+ if slug_history_name
113
+ any_of({ slug_name => slug }, { slug_history_name => slug })
114
+ else
115
+ where(slug_name => slug)
116
+ end
117
+ }
119
118
  end
120
- end
119
+
120
+ # Finds a unique slug, were specified string used to generate a slug.
121
+ #
122
+ # Returned slug will the same as the specified string when there are no
123
+ # duplicates.
124
+ #
125
+ # @param [String] desired_slug
126
+ # @param [Hash] options
127
+ # @param options [Symbol] :scope The scope that should be used to
128
+ # generate the slug, if the class creates scoped slugs. Defaults to
129
+ # `nil`.
130
+ # @param options [Constant] :model The model that the slug should be
131
+ # generated for. This option overrides `:scope`, as the scope can now
132
+ # be extracted from the model. Defaults to `nil`.
133
+ # @return [String] A unique slug
134
+ def find_unique_slug_for(desired_slug, options = {})
135
+ if slug_scope && self.reflect_on_association(slug_scope).nil?
136
+ scope_object = uniqueness_scope(options[:model])
137
+ scope_attribute = options[:scope] || options[:model].try(:read_attribute, slug_scope)
138
+ else
139
+ scope_object = options[:scope] || uniqueness_scope(options[:model])
140
+ scope_attribute = nil
141
+ end
142
+
143
+ excluded_id = options[:model]._id if options[:model]
144
+
145
+ slug = desired_slug.to_url
146
+
147
+ # Regular expression that matches slug, slug-1, ... slug-n
148
+ # If slug_name field was indexed, MongoDB will utilize that
149
+ # index to match /^.../ pattern.
150
+ pattern = /^#{Regexp.escape(slug)}(?:-(\d+))?$/
121
151
 
122
- private
152
+ if slug_scope &&
153
+ self.reflect_on_association(slug_scope).nil?
154
+ # scope is not an association, so it's scoped to a local field
155
+ # (e.g. an association id in a denormalized db design)
123
156
 
124
- def find_unique_slug
125
- # TODO: An epic method which calls for refactoring.
126
- slug = slug_builder.call(self).to_url
127
-
128
- # Regular expression that matches slug, slug-1, ... slug-n
129
- # If slug_name field was indexed, MongoDB will utilize that
130
- # index to match /^.../ pattern.
131
- pattern = /^#{Regexp.escape(slug)}(?:-(\d+))?$/
132
-
133
- if slug_scope &&
134
- self.class.reflect_on_association(slug_scope).nil?
135
- # scope is not an association, so it's scoped to a local field
136
- # (e.g. an association id in a denormalized db design)
137
- existing_slugs =
138
- self.class.
139
- only(slug_name).
140
- where(slug_name => pattern,
141
- :_id.ne => _id,
142
- slug_scope => self[slug_scope])
143
- else
144
- existing_slugs =
145
- uniqueness_scope.
146
- only(slug_name).
147
- where(slug_name => pattern, :_id.ne => _id)
148
- end
157
+ where_hash = {}
158
+ where_hash[slug_name] = pattern
159
+ where_hash[:_id.ne] = excluded_id if excluded_id
160
+ where_hash[slug_scope] = scope_attribute
149
161
 
150
- existing_slugs = existing_slugs.map do |obj|
151
- obj.try(:read_attribute, slug_name)
152
- end
162
+ existing_slugs =
163
+ deepest_document_superclass.
164
+ only(slug_name).
165
+ where(where_hash)
166
+ else
167
+ where_hash = {}
168
+ where_hash[slug_name] = pattern
169
+ where_hash[:_id.ne] = excluded_id if excluded_id
170
+
171
+ existing_slugs =
172
+ scope_object.
173
+ only(slug_name).
174
+ where(where_hash)
175
+ end
176
+
177
+ existing_slugs = existing_slugs.map do |doc|
178
+ doc.slug
179
+ end
180
+
181
+ if slug_history_name
182
+ if slug_scope &&
183
+ self.reflect_on_association(slug_scope).nil?
184
+ # scope is not an association, so it's scoped to a local field
185
+ # (e.g. an association id in a denormalized db design)
186
+
187
+ where_hash = {}
188
+ where_hash[slug_history_name.all] = [pattern]
189
+ where_hash[:_id.ne] = excluded_id if excluded_id
190
+ where_hash[slug_scope] = scope_attribute
191
+
192
+ history_slugged_documents =
193
+ deepest_document_superclass.
194
+ where(where_hash)
195
+ else
196
+ where_hash = {}
197
+ where_hash[slug_history_name.all] = [pattern]
198
+ where_hash[:_id.ne] = excluded_id if excluded_id
199
+
200
+ history_slugged_documents =
201
+ scope_object.
202
+ where(where_hash)
203
+ end
153
204
 
154
- if existing_slugs.count > 0
155
- # Sort the existing_slugs in increasing order by comparing the
156
- # suffix numbers:
157
- # slug, slug-1, slug-2, ..., slug-n
158
- existing_slugs.sort! do |a, b|
159
- (pattern.match(a)[1] || -1).to_i <=>
160
- (pattern.match(b)[1] || -1).to_i
205
+ existing_history_slugs = []
206
+ history_slugged_documents.each do |doc|
207
+ history_slugs = doc.read_attribute(slug_history_name)
208
+ next if history_slugs.nil?
209
+ existing_history_slugs.push(*history_slugs.find_all { |slug| slug =~ pattern })
210
+ end
211
+
212
+ # If the only conflict is in the history of a document in the same scope,
213
+ # transfer the slug
214
+ if slug_scope && existing_slugs.count == 0 && existing_history_slugs.count > 0
215
+ history_slugged_documents.each do |doc|
216
+ doc_history_slugs = doc.read_attribute(slug_history_name)
217
+ next if doc_history_slugs.nil?
218
+ doc_history_slugs -= existing_history_slugs
219
+ doc.write_attribute(slug_history_name, doc_history_slugs)
220
+ doc.save
221
+ end
222
+ existing_history_slugs = []
223
+ end
224
+
225
+ existing_slugs += existing_history_slugs
226
+ end
227
+
228
+ if reserved_words_in_slug.any? { |word| word === slug }
229
+ existing_slugs << slug
161
230
  end
162
- max = existing_slugs.last.match(/-(\d+)$/).try(:[], 1).to_i
163
231
 
164
- slug += "-#{max + 1}"
232
+ if existing_slugs.count > 0
233
+ # Sort the existing_slugs in increasing order by comparing the
234
+ # suffix numbers:
235
+ # slug, slug-1, slug-2, ..., slug-n
236
+ existing_slugs.sort! do |a, b|
237
+ (pattern.match(a)[1] || -1).to_i <=>
238
+ (pattern.match(b)[1] || -1).to_i
239
+ end
240
+ max = existing_slugs.last.match(/-(\d+)$/).try(:[], 1).to_i
241
+
242
+ slug += "-#{max + 1}"
243
+ end
244
+
245
+ slug
165
246
  end
247
+
248
+ private
249
+
250
+ def uniqueness_scope(model = nil)
251
+ if model
252
+ if slug_scope && (metadata = self.reflect_on_association(slug_scope))
253
+ parent = model.send(metadata.name)
166
254
 
167
- slug
255
+ # Make sure doc is actually associated with something, and that
256
+ # some referenced docs have been persisted to the parent
257
+ #
258
+ # TODO: we need better reflection for reference associations,
259
+ # like association_name instead of forcing collection_name here
260
+ # -- maybe in the forthcoming Mongoid refactorings?
261
+ inverse = metadata.inverse_of || collection_name
262
+ return parent.respond_to?(inverse) ? parent.send(inverse) : self
263
+ end
264
+ if embedded?
265
+ parent_metadata = reflect_on_all_associations(:embedded_in)[0]
266
+ return model._parent.send(parent_metadata.inverse_of || model.metadata.name)
267
+ end
268
+ end
269
+ deepest_document_superclass
270
+ end
271
+
272
+ def deepest_document_superclass
273
+ appropriate_class = self
274
+ while appropriate_class.superclass.include?(Mongoid::Document)
275
+ appropriate_class = appropriate_class.superclass
276
+ end
277
+ appropriate_class
278
+ end
168
279
  end
169
280
 
170
- def generate_slug
171
- # Generate a slug for new records only if the slug was not set.
172
- # If we're not a new record generate a slug if our slugged fields
173
- # changed on us.
174
- if (new_record? && !read_attribute(slug_name)) ||
175
- (!new_record? && slugged_fields_changed?)
176
- generate_slug!
281
+ # Builds a new slug.
282
+ #
283
+ # @return [true]
284
+ def build_slug
285
+ old = slug
286
+ write_attribute slug_name, find_unique_slug
287
+
288
+ # @note I find it odd that we can't use `slug_was`, `slug_changed?`, or
289
+ # `read_attribute (slug_history_name)` here.
290
+
291
+ if slug_history_name && old && old != slug
292
+ self.send(slug_history_name).<<(old).uniq!
177
293
  end
294
+
295
+ true
178
296
  end
179
297
 
180
- def generate_slug!
181
- write_attribute(slug_name, find_unique_slug)
298
+ # Finds a unique slug, were specified string used to generate a slug.
299
+ #
300
+ # Returned slug will the same as the specified string when there are no
301
+ # duplicates.
302
+ #
303
+ # @param [String] Desired slug
304
+ # @return [String] A unique slug
305
+ def find_unique_slug_for(desired_slug)
306
+ self.class.find_unique_slug_for desired_slug, :model => self
182
307
  end
183
308
 
184
- def slugged_fields_changed?
185
- slugged_fields.any? { |f| attribute_changed?(f) }
309
+ # @return [Boolean] Whether the slug requires to be rebuilt
310
+ def slug_should_be_rebuilt?
311
+ new_record? or slug_changed? or slugged_attributes_changed?
186
312
  end
187
313
 
188
- def uniqueness_scope
189
- if slug_scope
190
- metadata = self.class.reflect_on_association(slug_scope)
191
- parent = self.send(metadata.name)
314
+ unless self.respond_to? :slug
315
+ def slug
316
+ read_attribute slug_name
317
+ end
192
318
 
193
- # Make sure doc is actually associated with something, and that
194
- # some referenced docs have been persisted to the parent
195
- #
196
- # TODO: we need better reflection for reference associations,
197
- # like association_name instead of forcing collection_name here
198
- # -- maybe in the forthcoming Mongoid refactorings?
199
- inverse = metadata.inverse_of || collection_name
200
- parent.respond_to?(inverse) ? parent.send(inverse) : self.class
201
- elsif embedded?
202
- parent_metadata = reflect_on_all_associations(:embedded_in)[0]
203
- _parent.send(parent_metadata.inverse_of || self.metadata.name)
204
- else
205
- appropriate_class = self.class
206
- while appropriate_class.superclass.include?(Mongoid::Document)
207
- appropriate_class = appropriate_class.superclass
208
- end
209
- appropriate_class
319
+ def slug_changed?
320
+ attribute_changed? slug_name
321
+ end
322
+
323
+ def slug_was
324
+ attribute_was slug_name
325
+ end
326
+ end
327
+
328
+ def slugged_attributes_changed?
329
+ slugged_attributes.any? { |f| attribute_changed? f }
330
+ end
331
+
332
+ # @return [String] A string which Action Pack uses for constructing an URL
333
+ # to this record.
334
+ def to_param
335
+ unless slug
336
+ build_slug
337
+ save
210
338
  end
339
+
340
+ slug
341
+ end
342
+
343
+ private
344
+
345
+ def find_unique_slug
346
+ find_unique_slug_for user_defined_slug || slug_builder.call(self)
347
+ end
348
+
349
+ def user_defined_slug
350
+ slug if new_record? and slug.present? or slug_changed?
211
351
  end
212
352
  end
213
353
  end
@@ -1,5 +1,5 @@
1
1
  module Mongoid #:nodoc:
2
2
  module Slug
3
- VERSION = '0.9.0'
3
+ VERSION = '0.10.0'
4
4
  end
5
5
  end
@@ -0,0 +1,6 @@
1
+ class Alias
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :name, :as => :author_name
5
+ slug :author_name
6
+ end
data/spec/models/book.rb CHANGED
@@ -2,7 +2,7 @@ class Book
2
2
  include Mongoid::Document
3
3
  include Mongoid::Slug
4
4
  field :title
5
- slug :title, :index => true
5
+ slug :title, :index => true, :history => true
6
6
  embeds_many :subjects
7
7
  references_many :authors
8
8
  end
@@ -0,0 +1,6 @@
1
+ class Friend
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+ field :name
5
+ slug :name, :reserve => ['foo', 'bar', /^[a-z]{2}$/i]
6
+ end
@@ -2,6 +2,6 @@ class Subject
2
2
  include Mongoid::Document
3
3
  include Mongoid::Slug
4
4
  field :name
5
- slug :name
5
+ slug :name, :scope => :book, :history => true
6
6
  embedded_in :book
7
7
  end
@@ -58,7 +58,7 @@ module Mongoid
58
58
 
59
59
  it "generates a unique slug by appending a counter to duplicate text" do
60
60
  dup = book.subjects.create(:name => subject.name)
61
- dup.to_param.should eql 'psychoanalysis-1'
61
+ dup.to_param.should eql "psychoanalysis-1"
62
62
  end
63
63
 
64
64
  it "does not update slug if slugged fields have not changed" do
@@ -147,26 +147,25 @@ module Mongoid
147
147
  dup = Author.create(
148
148
  :first_name => author.first_name,
149
149
  :last_name => author.last_name)
150
- dup.to_param.should eql 'gilles-deleuze-1'
150
+ dup.to_param.should eql "gilles-deleuze-1"
151
151
 
152
152
  dup2 = Author.create(
153
153
  :first_name => author.first_name,
154
154
  :last_name => author.last_name)
155
155
 
156
156
  dup.save
157
- dup2.to_param.should eql 'gilles-deleuze-2'
157
+ dup2.to_param.should eql "gilles-deleuze-2"
158
158
  end
159
159
 
160
160
  it "does not update slug if slugged fields have changed but generated slug is identical" do
161
161
  author.last_name = "DELEUZE"
162
162
  author.save
163
- author.to_param.should eql 'gilles-deleuze'
163
+ author.to_param.should eql "gilles-deleuze"
164
164
  end
165
165
 
166
166
  it "finds by slug" do
167
167
  Author.find_by_slug("gilles-deleuze").should eql author
168
168
  end
169
-
170
169
  end
171
170
 
172
171
  context "when :as is passed as an argument" do
@@ -196,6 +195,40 @@ module Mongoid
196
195
  end
197
196
  end
198
197
 
198
+ context "when :history is passed as an argument" do
199
+ let(:book) do
200
+ Book.create(:title => "Book Title")
201
+ end
202
+
203
+ before(:each) do
204
+ book.title = "Other Book Title"
205
+ book.save
206
+ end
207
+
208
+ it "saves the old slug in the owner's history" do
209
+ book.slug_history.should include("book-title")
210
+ end
211
+
212
+ it "returns the document for the old slug" do
213
+ Book.find_by_slug("book-title").should == book
214
+ end
215
+
216
+ it "returns the document for the new slug" do
217
+ Book.find_by_slug("other-book-title").should == book
218
+ end
219
+
220
+ it "generates a unique slug by appending a counter to duplicate text" do
221
+ dup = Book.create(:title => "Book Title")
222
+ dup.to_param.should eql "book-title-1"
223
+ end
224
+
225
+ it "ensures no duplicate values are stored in history" do
226
+ book.update_attributes :title => 'Book Title'
227
+ book.update_attributes :title => 'Foo'
228
+ book.slug_history.find_all { |slug| slug == 'book-title' }.size.should eql 1
229
+ end
230
+ end
231
+
199
232
  context "when slug is scoped by a reference association" do
200
233
  let(:author) do
201
234
  book.authors.create(:first_name => "Gilles", :last_name => "Deleuze")
@@ -214,7 +247,7 @@ module Mongoid
214
247
  dup = book.authors.create(
215
248
  :first_name => author.first_name,
216
249
  :last_name => author.last_name)
217
- dup.to_param.should eql 'gilles-deleuze-1'
250
+ dup.to_param.should eql "gilles-deleuze-1"
218
251
  end
219
252
 
220
253
  context "with an irregular association name" do
@@ -235,46 +268,70 @@ module Mongoid
235
268
  dup.to_param.should eql character.to_param
236
269
  end
237
270
  end
271
+
272
+ context "when using history and reusing a slug within the scope" do
273
+ let!(:subject1) do
274
+ book.subjects.create(:name => "A Subject")
275
+ end
276
+ let!(:subject2) do
277
+ book.subjects.create(:name => "Another Subject")
278
+ end
279
+
280
+ before(:each) do
281
+ subject1.name = "Something Else Entirely"
282
+ subject1.save
283
+ subject2.name = "A Subject"
284
+ subject2.save
285
+ end
286
+
287
+ it "allows using the slug" do
288
+ subject2.slug.should == "a-subject"
289
+ end
290
+
291
+ it "removes the slug from the old owner's history" do
292
+ subject1.slug_history.should_not include("a-subject")
293
+ end
294
+ end
238
295
  end
239
-
296
+
240
297
  context "when slug is scoped by one of the class's own fields" do
241
298
  let!(:magazine) do
242
299
  Magazine.create(:title => "Big Weekly", :publisher_id => "abc123")
243
300
  end
244
301
 
245
302
  it "should scope by local field" do
246
- magazine.to_param.should eql 'big-weekly'
303
+ magazine.to_param.should eql "big-weekly"
247
304
  magazine2 = Magazine.create(:title => "Big Weekly", :publisher_id => "def456")
248
305
  magazine2.to_param.should eql magazine.to_param
249
306
  end
250
307
 
251
308
  it "should generate a unique slug by appending a counter to duplicate text" do
252
309
  dup = Magazine.create(:title => "Big Weekly", :publisher_id => "abc123")
253
- dup.to_param.should eql 'big-weekly-1'
310
+ dup.to_param.should eql "big-weekly-1"
254
311
  end
255
312
  end
256
313
 
257
- context "when :slug is given a block" do
314
+ context "when #slug is given a block" do
258
315
  let(:caption) do
259
- Caption.create(:identity => 'Edward Hopper (American, 1882-1967)',
260
- :title => 'Soir Bleu, 1914',
261
- :medium => 'Oil on Canvas')
316
+ Caption.create(:identity => "Edward Hopper (American, 1882-1967)",
317
+ :title => "Soir Bleu, 1914",
318
+ :medium => "Oil on Canvas")
262
319
  end
263
320
 
264
321
  it "generates a slug" do
265
- caption.to_param.should eql 'edward-hopper-soir-bleu-1914'
322
+ caption.to_param.should eql "edward-hopper-soir-bleu-1914"
266
323
  end
267
324
 
268
325
  it "updates the slug" do
269
- caption.title = 'Road in Maine, 1914'
326
+ caption.title = "Road in Maine, 1914"
270
327
  caption.save
271
328
  caption.to_param.should eql "edward-hopper-road-in-maine-1914"
272
329
  end
273
330
 
274
331
  it "does not change slug if slugged fields have changed but generated slug is identical" do
275
- caption.identity = 'Edward Hopper'
332
+ caption.identity = "Edward Hopper"
276
333
  caption.save
277
- caption.to_param.should eql 'edward-hopper-soir-bleu-1914'
334
+ caption.to_param.should eql "edward-hopper-soir-bleu-1914"
278
335
  end
279
336
 
280
337
  it "finds by slug" do
@@ -298,13 +355,13 @@ module Mongoid
298
355
  it "slugs Chinese characters" do
299
356
  book.title = "中文"
300
357
  book.save
301
- book.to_param.should eql 'zhong-wen'
358
+ book.to_param.should eql "zhong-wen"
302
359
  end
303
360
 
304
361
  it "slugs non-ASCII Latin characters" do
305
- book.title = 'Paul Cézanne'
362
+ book.title = "Paul Cézanne"
306
363
  book.save
307
- book.to_param.should eql 'paul-cezanne'
364
+ book.to_param.should eql "paul-cezanne"
308
365
  end
309
366
  end
310
367
 
@@ -340,6 +397,29 @@ module Mongoid
340
397
  Person.collection.index_information.should_not have_key "permalink_1"
341
398
  end
342
399
  end
400
+
401
+ context "when :reserve is passed" do
402
+ it "does not use the the reserved slugs" do
403
+ friend1 = Friend.create(:name => "foo")
404
+ friend1.slug.should_not eql("foo")
405
+ friend1.slug.should eql("foo-1")
406
+
407
+ friend2 = Friend.create(:name => "bar")
408
+ friend2.slug.should_not eql("bar")
409
+ friend2.slug.should eql("bar-1")
410
+
411
+ friend3 = Friend.create(:name => "en")
412
+ friend3.slug.should_not eql("en")
413
+ friend3.slug.should eql("en-1")
414
+ end
415
+
416
+ it "should start with concatenation -1" do
417
+ friend1 = Friend.create(:name => "foo")
418
+ friend1.slug.should eql("foo-1")
419
+ friend2 = Friend.create(:name => "foo")
420
+ friend2.slug.should eql("foo-2")
421
+ end
422
+ end
343
423
 
344
424
  context "when the object has STI" do
345
425
  it "scopes by the superclass" do
@@ -349,6 +429,25 @@ module Mongoid
349
429
  end
350
430
  end
351
431
 
432
+ context "when slug defined on alias of field" do
433
+ it "should use accessor, not alias" do
434
+ pseudonim = Alias.create(:author_name => "Max Stirner")
435
+ pseudonim.slug.should eql("max-stirner")
436
+ end
437
+ end
438
+
439
+ describe ".by_slug scope" do
440
+ let!(:author) { book.authors.create(:first_name => "Gilles", :last_name => "Deleuze") }
441
+
442
+ it "returns an empty array if no document is found" do
443
+ book.authors.by_slug("never-heard-of").should == []
444
+ end
445
+
446
+ it "returns an array containing the document if it is found" do
447
+ book.authors.by_slug(author.slug).should == [author]
448
+ end
449
+ end
450
+
352
451
  describe ".find_by_slug" do
353
452
  let!(:book) { Book.create(:title => "A Thousand Plateaus") }
354
453
 
@@ -386,11 +485,62 @@ module Mongoid
386
485
  book.reload.slug.should eql "proust-and-signs"
387
486
  end
388
487
  end
389
-
390
- context "when the slugged field is set upon creation" do
391
- it "respects the provided slug and does not generate a new one" do
392
- book = Book.create(:title => "A Thousand Plateaus", :slug => 'not-what-you-expected')
393
- book.to_param.should eql "not-what-you-expected"
488
+
489
+ describe ".for_unique_slug_for" do
490
+ it "returns the unique slug" do
491
+ Book.find_unique_slug_for("A Thousand Plateaus").should eq("a-thousand-plateaus")
492
+ end
493
+
494
+ it "returns the unique slug with a counter if necessary" do
495
+ Book.create(:title => "A Thousand Plateaus")
496
+ Book.find_unique_slug_for("A Thousand Plateaus").should eq("a-thousand-plateaus-1")
497
+ end
498
+
499
+ it "returns the unique slug as if it were the provided object" do
500
+ book = Book.create(:title => "A Thousand Plateaus")
501
+ Book.find_unique_slug_for("A Thousand Plateaus", :model => book).should eq("a-thousand-plateaus")
502
+ end
503
+ end
504
+
505
+ describe "#find_unique_slug_for" do
506
+ let!(:book) { Book.create(:title => "A Thousand Plateaus") }
507
+
508
+ it "returns the unique slug" do
509
+ book.find_unique_slug_for("Anti Oedipus").should eq("anti-oedipus")
510
+ end
511
+
512
+ it "returns the unique slug with a counter if necessary" do
513
+ Book.create(:title => "Anti Oedipus")
514
+ book.find_unique_slug_for("Anti Oedipus").should eq("anti-oedipus-1")
515
+ end
516
+ end
517
+
518
+ context "when the slugged field is set manually" do
519
+ context "when it set to a non-empty string" do
520
+ it "respects the provided slug" do
521
+ book = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
522
+ book.to_param.should eql "not-what-you-expected"
523
+ end
524
+
525
+ it "ensures uniqueness" do
526
+ book1 = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
527
+ book2 = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
528
+ book2.to_param.should eql "not-what-you-expected-1"
529
+ end
530
+
531
+ it "updates the slug when a new one is passed in" do
532
+ book = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
533
+ book.slug = "not-it-either"
534
+ book.save
535
+ book.to_param.should eql "not-it-either"
536
+ end
537
+ end
538
+
539
+ context "when it is set to an empty string" do
540
+ it "generate a new one" do
541
+ book = Book.create(:title => "A Thousand Plateaus", :slug => "")
542
+ book.to_param.should eql "a-thousand-plateaus"
543
+ end
394
544
  end
395
545
  end
396
546
  end
data/spec/spec_helper.rb CHANGED
@@ -1,12 +1,13 @@
1
- require "rubygems"
2
- require "bundler/setup"
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
3
 
4
- require "rspec"
4
+ require 'pry'
5
+ require 'rspec'
5
6
 
6
- require File.expand_path("../../lib/mongoid/slug", __FILE__)
7
+ require File.expand_path('../../lib/mongoid/slug', __FILE__)
7
8
 
8
9
  Mongoid.configure do |config|
9
- name = "mongoid_slug_test"
10
+ name = 'mongoid_slug_test'
10
11
  config.master = Mongo::Connection.new.db(name)
11
12
  end
12
13
 
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid_slug
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
- - Paper Cavalier
8
+ - Hakan Ensari
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-01-10 00:00:00.000000000 Z
12
+ date: 2012-03-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mongoid
16
- requirement: &70354887937940 !ruby/object:Gem::Requirement
16
+ requirement: &70314214792060 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '2.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70354887937940
24
+ version_requirements: *70314214792060
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: stringex
27
- requirement: &70354887953780 !ruby/object:Gem::Requirement
27
+ requirement: &70314214791560 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,32 @@ dependencies:
32
32
  version: '1.3'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70354887953780
35
+ version_requirements: *70314214791560
36
+ - !ruby/object:Gem::Dependency
37
+ name: bson_ext
38
+ requirement: &70314214791100 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '1.6'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70314214791100
47
+ - !ruby/object:Gem::Dependency
48
+ name: pry
49
+ requirement: &70314214790640 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '0.9'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70314214790640
36
58
  - !ruby/object:Gem::Dependency
37
59
  name: rake
38
- requirement: &70354887953320 !ruby/object:Gem::Requirement
60
+ requirement: &70314214790180 !ruby/object:Gem::Requirement
39
61
  none: false
40
62
  requirements:
41
63
  - - ~>
@@ -43,22 +65,21 @@ dependencies:
43
65
  version: '0.9'
44
66
  type: :development
45
67
  prerelease: false
46
- version_requirements: *70354887953320
68
+ version_requirements: *70314214790180
47
69
  - !ruby/object:Gem::Dependency
48
70
  name: rspec
49
- requirement: &70354887952860 !ruby/object:Gem::Requirement
71
+ requirement: &70314214789720 !ruby/object:Gem::Requirement
50
72
  none: false
51
73
  requirements:
52
74
  - - ~>
53
75
  - !ruby/object:Gem::Version
54
- version: '2.6'
76
+ version: '2.8'
55
77
  type: :development
56
78
  prerelease: false
57
- version_requirements: *70354887952860
58
- description: Mongoid Slug generates a URL slug or permalink based on one or more fields
59
- in a Mongoid model.
79
+ version_requirements: *70314214789720
80
+ description: ! " a \n a ."
60
81
  email:
61
- - code@papercavalier.com
82
+ - hakan.ensari@papercavalier.com
62
83
  executables: []
63
84
  extensions: []
64
85
  extra_rdoc_files: []
@@ -68,10 +89,12 @@ files:
68
89
  - lib/mongoid_slug.rb
69
90
  - LICENSE
70
91
  - README.md
92
+ - spec/models/alias.rb
71
93
  - spec/models/article.rb
72
94
  - spec/models/author.rb
73
95
  - spec/models/book.rb
74
96
  - spec/models/caption.rb
97
+ - spec/models/friend.rb
75
98
  - spec/models/magazine.rb
76
99
  - spec/models/page.rb
77
100
  - spec/models/partner.rb
@@ -80,7 +103,7 @@ files:
80
103
  - spec/models/subject.rb
81
104
  - spec/mongoid/slug_spec.rb
82
105
  - spec/spec_helper.rb
83
- homepage: http://github.com/papercavalier/mongoid-slug
106
+ homepage: http://github.com/hakanensari/mongoid-slug
84
107
  licenses: []
85
108
  post_install_message:
86
109
  rdoc_options: []
@@ -103,12 +126,14 @@ rubyforge_project: mongoid_slug
103
126
  rubygems_version: 1.8.11
104
127
  signing_key:
105
128
  specification_version: 3
106
- summary: Generates a URL slug
129
+ summary: Generates a URL slug in a Mongoid model
107
130
  test_files:
131
+ - spec/models/alias.rb
108
132
  - spec/models/article.rb
109
133
  - spec/models/author.rb
110
134
  - spec/models/book.rb
111
135
  - spec/models/caption.rb
136
+ - spec/models/friend.rb
112
137
  - spec/models/magazine.rb
113
138
  - spec/models/page.rb
114
139
  - spec/models/partner.rb