friendly_id_globalize3 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. data/Changelog.md +354 -0
  2. data/Contributors.md +43 -0
  3. data/Guide.md +686 -0
  4. data/MIT-LICENSE +19 -0
  5. data/README.md +99 -0
  6. data/Rakefile +75 -0
  7. data/extras/README.txt +3 -0
  8. data/extras/bench.rb +40 -0
  9. data/extras/extras.rb +38 -0
  10. data/extras/prof.rb +19 -0
  11. data/extras/template-gem.rb +26 -0
  12. data/extras/template-plugin.rb +28 -0
  13. data/generators/friendly_id/friendly_id_generator.rb +30 -0
  14. data/generators/friendly_id/templates/create_slugs.rb +18 -0
  15. data/lib/friendly_id.rb +93 -0
  16. data/lib/friendly_id/active_record.rb +74 -0
  17. data/lib/friendly_id/active_record_adapter/configuration.rb +68 -0
  18. data/lib/friendly_id/active_record_adapter/finders.rb +148 -0
  19. data/lib/friendly_id/active_record_adapter/relation.rb +165 -0
  20. data/lib/friendly_id/active_record_adapter/simple_model.rb +63 -0
  21. data/lib/friendly_id/active_record_adapter/slug.rb +77 -0
  22. data/lib/friendly_id/active_record_adapter/slugged_model.rb +122 -0
  23. data/lib/friendly_id/active_record_adapter/tasks.rb +72 -0
  24. data/lib/friendly_id/configuration.rb +178 -0
  25. data/lib/friendly_id/datamapper.rb +5 -0
  26. data/lib/friendly_id/railtie.rb +22 -0
  27. data/lib/friendly_id/sequel.rb +5 -0
  28. data/lib/friendly_id/slug_string.rb +25 -0
  29. data/lib/friendly_id/slugged.rb +105 -0
  30. data/lib/friendly_id/status.rb +35 -0
  31. data/lib/friendly_id/test.rb +350 -0
  32. data/lib/friendly_id/version.rb +9 -0
  33. data/lib/generators/friendly_id_generator.rb +25 -0
  34. data/lib/tasks/friendly_id.rake +19 -0
  35. data/rails/init.rb +2 -0
  36. data/test/active_record_adapter/ar_test_helper.rb +150 -0
  37. data/test/active_record_adapter/basic_slugged_model_test.rb +14 -0
  38. data/test/active_record_adapter/cached_slug_test.rb +76 -0
  39. data/test/active_record_adapter/core.rb +138 -0
  40. data/test/active_record_adapter/custom_normalizer_test.rb +20 -0
  41. data/test/active_record_adapter/custom_table_name_test.rb +22 -0
  42. data/test/active_record_adapter/default_scope_test.rb +30 -0
  43. data/test/active_record_adapter/optimistic_locking_test.rb +18 -0
  44. data/test/active_record_adapter/scoped_model_test.rb +119 -0
  45. data/test/active_record_adapter/simple_test.rb +76 -0
  46. data/test/active_record_adapter/slug_test.rb +34 -0
  47. data/test/active_record_adapter/slugged.rb +33 -0
  48. data/test/active_record_adapter/slugged_status_test.rb +28 -0
  49. data/test/active_record_adapter/sti_test.rb +22 -0
  50. data/test/active_record_adapter/support/database.jdbcsqlite3.yml +2 -0
  51. data/test/active_record_adapter/support/database.mysql.yml +4 -0
  52. data/test/active_record_adapter/support/database.postgres.yml +6 -0
  53. data/test/active_record_adapter/support/database.sqlite3.yml +2 -0
  54. data/test/active_record_adapter/support/models.rb +104 -0
  55. data/test/active_record_adapter/tasks_test.rb +82 -0
  56. data/test/friendly_id_test.rb +96 -0
  57. data/test/test_helper.rb +13 -0
  58. metadata +193 -0
@@ -0,0 +1,63 @@
1
+ module FriendlyId
2
+ module ActiveRecordAdapter
3
+
4
+ module SimpleModel
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ column = friendly_id_config.column
9
+ validate :validate_friendly_id, :unless => :skip_friendly_id_validations
10
+ validates_presence_of column, :unless => :skip_friendly_id_validations
11
+ validates_length_of column, :maximum => friendly_id_config.max_length, :unless => :skip_friendly_id_validations
12
+ after_update :update_scopes
13
+ extend FriendlyId::ActiveRecordAdapter::Finders unless FriendlyId.on_ar3?
14
+ end
15
+ end
16
+
17
+ # Get the {FriendlyId::Status} after the find has been performed.
18
+ def friendly_id_status
19
+ @friendly_id_status ||= Status.new :record => self
20
+ end
21
+
22
+ # Returns the friendly_id.
23
+ def friendly_id
24
+ send friendly_id_config.column
25
+ end
26
+ alias best_id friendly_id
27
+
28
+ # Returns the friendly id, or if none is available, the numeric id.
29
+ def to_param
30
+ (friendly_id || id).to_s
31
+ end
32
+
33
+ private
34
+
35
+ # The old and new values for the friendly_id column.
36
+ def friendly_id_changes
37
+ changes[friendly_id_config.column.to_s]
38
+ end
39
+
40
+ # Update the slugs for any model that is using this model as its
41
+ # FriendlyId scope.
42
+ def update_scopes
43
+ if changes = friendly_id_changes
44
+ friendly_id_config.child_scopes.each do |klass|
45
+ Slug.update_all "scope = '#{changes[1]}'", ["sluggable_type = ? AND scope = ?", klass.to_s, changes[0]]
46
+ end
47
+ end
48
+ end
49
+
50
+ def skip_friendly_id_validations
51
+ friendly_id.nil? && friendly_id_config.allow_nil?
52
+ end
53
+
54
+ def validate_friendly_id
55
+ if result = friendly_id_config.reserved_error_message(friendly_id)
56
+ self.errors.add(*result)
57
+ return false
58
+ end
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,77 @@
1
+ # A Slug is a unique, human-friendly identifier for an ActiveRecord.
2
+ class Slug < ::ActiveRecord::Base
3
+ attr_writer :sluggable
4
+ attr_accessible :name, :scope, :sluggable, :sequence, :locale
5
+ def self.named_scope(*args, &block) scope(*args, &block) end if FriendlyId.on_ar3?
6
+ table_name = "slugs"
7
+ before_save :enable_name_reversion, :set_sequence
8
+ validate :validate_name
9
+ named_scope :similar_to, lambda {|slug| {:conditions => {
10
+ :name => slug.name,
11
+ :scope => slug.scope,
12
+ :sluggable_type => slug.sluggable_type
13
+ },
14
+ :order => "sequence ASC"
15
+ }
16
+ }
17
+
18
+ def sluggable
19
+ sluggable_id && !@sluggable and begin
20
+ klass = sluggable_type.constantize
21
+ klass.send(:with_exclusive_scope) do
22
+ @sluggable = klass.find(sluggable_id.to_i)
23
+ end
24
+ end
25
+ @sluggable
26
+ end
27
+
28
+ # Whether this slug is the most recent of its owner's slugs.
29
+ def current?
30
+ sluggable.slug == self
31
+ end
32
+
33
+ def outdated?
34
+ !current?
35
+ end
36
+
37
+ def to_friendly_id
38
+ sequence > 1 ? friendly_id_with_sequence : name
39
+ end
40
+
41
+ # Raise a FriendlyId::SlugGenerationError if the slug name is blank.
42
+ def validate_name
43
+ if name.blank?
44
+ raise FriendlyId::BlankError.new("slug.name can not be blank.")
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # If we're renaming back to a previously used friendly_id, delete the
51
+ # slug so that we can recycle the name without having to use a sequence.
52
+ def enable_name_reversion
53
+ sluggable.slugs.find_all_by_name_and_scope(name, scope).each { |slug| slug.destroy }
54
+ end
55
+
56
+ def friendly_id_with_sequence
57
+ "#{name}#{separator}#{sequence}"
58
+ end
59
+
60
+ def similar_to_other_slugs?
61
+ !similar_slugs.empty?
62
+ end
63
+
64
+ def similar_slugs
65
+ self.class.similar_to(self)
66
+ end
67
+
68
+ def separator
69
+ sluggable.friendly_id_config.sequence_separator
70
+ end
71
+
72
+ def set_sequence
73
+ return unless new_record?
74
+ self.sequence = similar_slugs.last.sequence.succ if similar_to_other_slugs?
75
+ end
76
+
77
+ end
@@ -0,0 +1,122 @@
1
+ module FriendlyId
2
+ module ActiveRecordAdapter
3
+ module SluggedModel
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
8
+ has_one :slug, :order => 'id DESC', :as => :sluggable, :dependent => :destroy,
9
+ :conditions => ({:locale => (Thread.current[:globalize_locale] || ::I18n.locale)} if friendly_id_config.class.locales_used?)
10
+ before_save :build_a_slug
11
+ after_save :set_slug_cache
12
+ after_update :update_scope
13
+ after_update :update_dependent_scopes
14
+ protect_friendly_id_attributes
15
+ extend FriendlyId::ActiveRecordAdapter::Finders unless FriendlyId.on_ar3?
16
+ end
17
+ end
18
+
19
+ include FriendlyId::Slugged::Model
20
+
21
+ def find_slug(name, sequence)
22
+ slugs.find_by_name_and_sequence(name, sequence)
23
+ end
24
+
25
+ # Returns the friendly id, or if none is available, the numeric id. Note that this
26
+ # method will use the cached_slug value if present, unlike {#friendly_id}.
27
+ def to_param
28
+ friendly_id_config.cache_column ? to_param_from_cache : to_param_from_slug
29
+ end
30
+
31
+ private
32
+
33
+ def scope_changed?
34
+ friendly_id_config.scope? && send(friendly_id_config.scope).to_param != slug.scope
35
+ end
36
+
37
+ # Respond with the cached value if available.
38
+ def to_param_from_cache
39
+ read_attribute(friendly_id_config.cache_column) || id.to_s
40
+ end
41
+
42
+ # Respond with the slugged value if available.
43
+ def to_param_from_slug
44
+ unless friendly_id_config.class.locales_used?
45
+ slug? ? slug.to_friendly_id : id.to_s
46
+ else
47
+ locale = (Thread.current[:globalize_locale] || ::I18n.locale).to_s
48
+ if (locale_slug = slugs.detect{|s| s.locale.to_s == locale}).present?
49
+ locale_slug.to_friendly_id
50
+ else
51
+ id.to_s
52
+ end
53
+ end
54
+ end
55
+
56
+ # Build the new slug using the generated friendly id.
57
+ def build_a_slug
58
+ return unless new_slug_needed?
59
+ @slug = slugs.build :name => slug_text.to_s, :scope => friendly_id_config.scope_for(self),
60
+ :sluggable => self
61
+ @slug.locale = Thread.current[:globalize_locale] || ::I18n.locale if friendly_id_config.class.locales_used?
62
+ @new_friendly_id = @slug.to_friendly_id
63
+ end
64
+
65
+ # Reset the cached friendly_id?
66
+ def new_cache_needed?
67
+ uses_slug_cache? && slug? && send(friendly_id_config.cache_column) != slug.to_friendly_id
68
+ end
69
+
70
+ # Reset the cached friendly_id.
71
+ def set_slug_cache
72
+ if new_cache_needed?
73
+ begin
74
+ send "#{friendly_id_config.cache_column}=", slug.to_friendly_id
75
+ update_without_callbacks
76
+ rescue ActiveRecord::StaleObjectError
77
+ reload
78
+ retry
79
+ end
80
+ end
81
+ end
82
+
83
+ def update_scope
84
+ return unless slug && scope_changed?
85
+ self.class.transaction do
86
+ slug.scope = send(friendly_id_config.scope).to_param
87
+ similar = Slug.similar_to(slug)
88
+ if !similar.empty?
89
+ slug.sequence = similar.first.sequence.succ
90
+ end
91
+ slug.save!
92
+ end
93
+ end
94
+
95
+ # Update the slugs for any model that is using this model as its
96
+ # FriendlyId scope.
97
+ def update_dependent_scopes
98
+ return unless friendly_id_config.class.scopes_used?
99
+ if slugs(true).size > 1 && @new_friendly_id
100
+ friendly_id_config.child_scopes.each do |klass|
101
+ Slug.update_all "scope = '#{@new_friendly_id}'", ["sluggable_type = ? AND scope = ?",
102
+ klass.to_s, slugs.second.to_friendly_id]
103
+ end
104
+ end
105
+ end
106
+
107
+ # Does the model use slug caching?
108
+ def uses_slug_cache?
109
+ friendly_id_config.cache_column?
110
+ end
111
+
112
+ # This method was removed in ActiveRecord 3.0.
113
+ if !ActiveRecord::Base.private_method_defined? :update_without_callbacks
114
+ def update_without_callbacks
115
+ attributes_with_values = arel_attributes_values(false, false, attribute_names)
116
+ return false if attributes_with_values.empty?
117
+ self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,72 @@
1
+ module FriendlyId
2
+ class TaskRunner
3
+
4
+ extend Forwardable
5
+
6
+ attr_accessor :days
7
+ attr_accessor :klass
8
+ attr_accessor :task_options
9
+
10
+ def_delegators :klass, :find, :friendly_id_config, :update_all
11
+
12
+ OLD_SLUG_DAYS = 45
13
+
14
+ def initialize(&block)
15
+ self.klass = ENV["MODEL"]
16
+ self.days = ENV["DAYS"]
17
+ end
18
+
19
+ def days=(days)
20
+ @days ||= days.blank? ? OLD_SLUG_DAYS : days.to_i
21
+ end
22
+
23
+ def klass=(klass)
24
+ @klass ||= klass.to_s.classify.constantize unless klass.blank?
25
+ end
26
+
27
+ def make_slugs
28
+ validate_uses_slugs
29
+ options = {
30
+ :include => :slug,
31
+ :limit => (ENV["LIMIT"] || 100).to_i,
32
+ :offset => 0,
33
+ :order => ENV["ORDER"] || "#{klass.table_name}.id ASC",
34
+ }.merge(task_options || {})
35
+
36
+ while records = find(:all, options) do
37
+ break if records.size == 0
38
+ records.each do |record|
39
+ record.save(:validate => false) unless record.slug?
40
+ yield(record) if block_given?
41
+ end
42
+ options[:offset] += options[:limit]
43
+ end
44
+ end
45
+
46
+ def delete_slugs
47
+ validate_uses_slugs
48
+ Slug.destroy_all(["sluggable_type = ?", klass.to_s])
49
+ if column = friendly_id_config.cache_column
50
+ update_all("#{column} = NULL")
51
+ end
52
+ end
53
+
54
+ def delete_old_slugs
55
+ conditions = ["created_at < ?", DateTime.now - days]
56
+ if klass
57
+ conditions[0] << " AND sluggable_type = ?"
58
+ conditions << klass.to_s
59
+ end
60
+ Slug.all(:conditions => conditions).select(&:outdated?).map(&:destroy)
61
+ end
62
+
63
+ def validate_uses_slugs
64
+ (raise "You need to pass a MODEL=<model name> argument to rake") if klass.blank?
65
+ unless friendly_id_config.use_slug?
66
+ raise "Class '%s' doesn't use slugs" % klass.to_s
67
+ end
68
+ rescue NoMethodError
69
+ raise "Class '%s' doesn't use FriendlyId" % klass.to_s
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,178 @@
1
+ module FriendlyId
2
+
3
+ # This class is not intended to be used on its own, it is used internally
4
+ # by `has_friendly_id` to store a model's configuration and
5
+ # configuration-related methods.
6
+ #
7
+ # The arguments accepted by +has_friendly_id+ correspond to the writeable
8
+ # instance attributes of this class; please see the description of the
9
+ # attributes below for information on the possible options.
10
+ #
11
+ # @example
12
+ # has_friendly_id :name,
13
+ # :use_slug => true,
14
+ # :max_length => 150,
15
+ # :approximate_ascii => true,
16
+ # :ascii_approximation_options => :german,
17
+ # :sequence_separator => ":",
18
+ # :reserved_words => ["reserved", "words"],
19
+ # :scope => :country,
20
+ # :cache_column => :my_cache_column_name,
21
+ # :default_locale => :en
22
+ # # etc.
23
+ class Configuration
24
+
25
+ DEFAULTS = {
26
+ :allow_nil => false,
27
+ :ascii_approximation_options => [],
28
+ :max_length => 255,
29
+ :reserved_words => ["index", "new"],
30
+ :reserved_message => 'can not be "%s"',
31
+ :sequence_separator => "--",
32
+ :default_locale => :en
33
+ }
34
+
35
+ # Whether to allow friendly_id and/or slugs to be nil. This is not
36
+ # generally useful on its own, but may allow you greater flexibility to
37
+ # customize your application.
38
+ attr_accessor :allow_nil
39
+ alias :allow_nil? :allow_nil
40
+
41
+ # Strip diacritics from Western characters.
42
+ attr_accessor :approximate_ascii
43
+
44
+ # Locale-type options for ASCII approximations.
45
+ attr_accessor :ascii_approximation_options
46
+
47
+ # The class that's using the configuration.
48
+ attr_reader :configured_class
49
+
50
+ # The maximum allowed byte length for a friendly_id string. This is checked *after* a
51
+ # string is processed by FriendlyId to remove spaces, special characters, etc.
52
+ attr_accessor :max_length
53
+
54
+ # The method or column that will be used as the basis of the friendly_id string.
55
+ attr_reader :method
56
+ alias :column :method
57
+
58
+ # The message shown when a reserved word is used.
59
+ # @see #reserved_words
60
+ attr_accessor :reserved_message
61
+
62
+ # Array of words that are reserved and can't be used as friendly_id strings.
63
+ # If a listed word is used in a sluggable model, it will raise a
64
+ # FriendlyId::SlugGenerationError. For Rails applications, you are recommended
65
+ # to include "index" and "new", which used as the defaults unless overridden.
66
+ attr_accessor :reserved_words
67
+
68
+ # The method or relation to use as the friendly_id's scope.
69
+ attr_reader :scope
70
+
71
+ # The string that separates slug names from slug sequences. Defaults to "--".
72
+ attr_accessor :sequence_separator
73
+
74
+ # Strip non-ASCII characters from the friendly_id string.
75
+ attr_accessor :strip_non_ascii
76
+
77
+ # Use slugs for storing the friendly_id string.
78
+ attr_accessor :use_slug
79
+ alias :use_slugs= :use_slug
80
+
81
+ # Allows setting the default locale when locale column is present.
82
+ attr_accessor :locale, :default_locale
83
+
84
+ def initialize(configured_class, method, options = nil, &block)
85
+ @configured_class = configured_class
86
+ @method = method.to_sym
87
+ DEFAULTS.merge(options || {}).each do |key, value|
88
+ self.send "#{key}=".to_sym, value
89
+ end
90
+ yield self if block_given?
91
+ end
92
+
93
+ def cache_column=(value)
94
+ @cache_column = value.to_s.strip.to_sym
95
+ if value =~ /\s/ || [:slug, :slugs].include?(@cache_column)
96
+ raise ArgumentError, "FriendlyId cache column can not be named '#{value}'"
97
+ end
98
+ @cache_column
99
+ end
100
+
101
+ # This should be overridden by adapters that implement caching.
102
+ def cache_column?
103
+ false
104
+ end
105
+
106
+ def reserved_words=(*args)
107
+ if args.first.kind_of?(Regexp)
108
+ @reserved_words = args.first
109
+ else
110
+ @reserved_words = args.flatten.uniq
111
+ end
112
+ end
113
+
114
+ def reserved?(word)
115
+ word = word.to_s
116
+ if reserved_words.kind_of?(Regexp)
117
+ reserved_words =~ word
118
+ else
119
+ reserved_words.include?(word)
120
+ end
121
+ end
122
+
123
+ def reserved_error_message(word)
124
+ [method, reserved_message % word] if reserved? word
125
+ end
126
+
127
+ def scope=(scope)
128
+ self.class.scopes_used = true
129
+ @scope = scope
130
+ end
131
+
132
+ def sequence_separator=(string)
133
+ if string == "-" || string =~ /\s/
134
+ raise ArgumentError, "FriendlyId sequence_separator can not be '#{string}'"
135
+ end
136
+ @sequence_separator = string
137
+ end
138
+
139
+ class << self
140
+ # This will be set if FriendlyId's scope feature is used in any model. It is here
141
+ # to provide a way to avoid invoking costly scope lookup methods when the scoped
142
+ # slug feature is not being used by any models.
143
+ def scopes_used=(val)
144
+ @scopes_used = !!val
145
+ end
146
+
147
+ # Are scoped slugs being used by any model?
148
+ # @see Configuration.scoped_used=
149
+ def scopes_used?
150
+ @scopes_used
151
+ end
152
+
153
+ # Are localed used by the slugs model?
154
+ def locales_used?
155
+ ::Slug.table_exists? && ::Slug.column_names.include?('locale')
156
+ end
157
+ end
158
+
159
+ %w[approximate_ascii scope strip_non_ascii use_slug locale].each do |method|
160
+ class_eval(<<-EOM, __FILE__, __LINE__ + 1)
161
+ def #{method}?
162
+ !! #{method}
163
+ end
164
+ EOM
165
+ end
166
+
167
+ alias :use_slugs? :use_slug?
168
+
169
+ def babosa_options
170
+ {
171
+ :to_ascii => strip_non_ascii?,
172
+ :transliterate => approximate_ascii?,
173
+ :transliterations => ascii_approximation_options,
174
+ :max_length => max_length
175
+ }
176
+ end
177
+ end
178
+ end