slug 0.5.7 → 4.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 65d6b738a1e306b5803f1b92e8328ed20632eb0d7706eb2417a198bd1acf8bfc
4
+ data.tar.gz: b1833f858513babd75d2f94ef776831fab3cc2e105ed9df33bf2912c9d7feb5c
5
+ SHA512:
6
+ metadata.gz: 9b46a5b3226a1b4adbdf027fe9c7692d2a33431a5aa0b67ef99edd6297974008789407eee80cd614b8d65a5bfdcbadd6c2112c554e7d79e7198571ccee967a52
7
+ data.tar.gz: d9e07b0b4b55810ab4eaff7f828755bf0bdf0d5e6744cc8ba866e50e96642db707ee459d0c8b31288ef9978cf82677a386163a646e7eff570662587395996b02
data/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # slug
2
+
3
+ Slug provides simple, straightforward slugging for your ActiveRecord models.
4
+
5
+ Slug is based on code from Norman Clarke's fantastic [friendly_id](https://github.com/norman/friendly_id) project and Nick Zadrozny's [friendly_identifier](http://code.google.com/p/friendly-identifier/).
6
+
7
+ What's different:
8
+
9
+ * Unlike friendly_id's more advanced modes, slugs are stored directly in your model's table. friendly_id stores its data in a separate sluggable table, which enables cool things like slug versioning—but forces yet another join when trying to do complex find_by_slugs.
10
+ * Like friendly_id, diacritics (accented characters) are stripped from slug strings.
11
+ * The number of options is manageable.
12
+
13
+ ## Installation
14
+
15
+ Add the gem to your Gemfile
16
+
17
+ ```
18
+ gem 'slug'
19
+ ```
20
+
21
+ of your rails project.
22
+
23
+ This is tested with Rails 5.1.4, MRI Ruby 2.4.1
24
+
25
+ ## Usage
26
+
27
+ ### Creating the database column
28
+
29
+ It's up to you to set up the appropriate column in your model. By default, slug saves the slug to a column called 'slug', so in most cases you'll just want to add
30
+
31
+ ```ruby
32
+ add_column :my_table, :slug, :string
33
+ ```
34
+
35
+ in a migration. You should also add a unque index on the slug field in your migration
36
+
37
+ ```ruby
38
+ add_index :model_name, :slug, unique: true
39
+ ```
40
+
41
+ Though Slug uses `validates_uniqueness_of` to ensue the uniqueness of your slug, two concurrent INSERTs could try to set the same slug.
42
+
43
+ ### Model setup
44
+
45
+ Once your table is migrated, just add
46
+
47
+ ```ruby
48
+ slug :source_field
49
+ ```
50
+
51
+ to your ActiveRecord model. `:source_field` is the column you'd like to base the slug on. For example, it might be `:headline`.
52
+
53
+ #### Using an instance method as the source column
54
+
55
+ The source column isn't limited to just database attributes—you can specify any instance method. This is handy if you need special formatting on your source column before it's slugged, or if you want to base the slug on several attributes.
56
+
57
+ For example:
58
+
59
+ ```ruby
60
+ class Article < ActiveRecord::Base
61
+ slug :title_for_slug
62
+
63
+ def title_for_slug
64
+ "#{headline}-#{publication_date.year}-#{publication_date.month}"
65
+ end
66
+ end
67
+ ```
68
+
69
+ would use `headline-pub year-pub month` as the slug source.
70
+
71
+ From here, you can work with your slug as if it were a normal column. `find_by_slug` and named scopes will work as they do for any other column.
72
+
73
+ ### Options
74
+
75
+ There are two options:
76
+
77
+ #### Column
78
+
79
+ If you want to save the slug in a database column that isn't called
80
+ `slug`, just pass the `:column` option. For example:
81
+
82
+ ```
83
+ slug :headline, column: :web_slug
84
+ ```
85
+
86
+ would generate a slug based on `headline` and save it to `web_slug`.
87
+
88
+ #### Generic Default
89
+
90
+ If the source column is empty, blank, or only contains filtered
91
+ characters, you can avoid `ActiveRecord::ValidationError` exceptions
92
+ by setting `generic_default: true`. For example:
93
+
94
+ ```ruby
95
+ slug :headline, generic_default: true
96
+ ```
97
+
98
+ will generate a slug based on your model name if the headline is blank.
99
+
100
+ This is useful if the source column (e.g. headline) is based on user-generated
101
+ input or can be blank (nil or empty).
102
+
103
+ Some prefer to get the exception in this case. Others want to get a good
104
+ slug and move on.
105
+
106
+ ## Notes
107
+
108
+ * Slug validates presence and uniqueness of the slug column. If you pass something that isn't sluggable as the source (for example, say you set the headline to '---'), a validation error will be set. To avoid this, use the `:generic_default` option.
109
+ * Slug doesn't update the slug if the source column changes. If you really need to regenerate the slug, call `@model.set_slug` before save.
110
+ * If a slug already exists, Slug will automatically append a '-n' suffix to your slug to make it unique. The second instance of a slug is '-1'.
111
+ * If you don't like the slug formatting or the accented character stripping doesn't work for you, it's easy to override Slug's formatting functions. Check the source for details.
112
+
113
+ ## Development
114
+
115
+ `rake test` will run the included unit tests.
116
+
117
+ ## Authors
118
+
119
+ Ben Koski, ben.koski@gmail.com
120
+
121
+ With generous contributions from:
122
+ * [Derek Willis](http://thescoop.org/)
123
+ * [Douglas Lovell](https://github.com/wbreeze)
124
+ * [Paul Battley](https://github.com/threedaymonk)
125
+ * [Yura Omelchuk](https://github.com/jurgens)
126
+ * others listed in the
127
+ [GitHub contributor list](https://github.com/bkoski/slug/graphs/contributors).
data/Rakefile CHANGED
@@ -1,23 +1,6 @@
1
1
  require 'rake'
2
2
 
3
- begin
4
- require 'jeweler'
5
- Jeweler::Tasks.new do |s|
6
- s.name = "slug"
7
- s.summary = %Q{Simple, straightforward slugs for your ActiveRecord models.}
8
- s.email = "ben.koski@gmail.com"
9
- s.homepage = "http://github.com/bkoski/slug"
10
- s.description = "Simple, straightforward slugs for your ActiveRecord models."
11
- s.add_dependency 'activerecord'
12
- s.add_dependency 'activesupport'
13
- s.authors = ["Ben Koski"]
14
- end
15
- Jeweler::GemcutterTasks.new
16
- rescue LoadError
17
- puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
18
- end
19
-
20
- require 'rake/rdoctask'
3
+ require 'rdoc/task'
21
4
  Rake::RDocTask.new do |rdoc|
22
5
  rdoc.rdoc_dir = 'rdoc'
23
6
  rdoc.title = 'slug'
@@ -33,15 +16,4 @@ Rake::TestTask.new(:test) do |t|
33
16
  t.verbose = false
34
17
  end
35
18
 
36
- begin
37
- require 'rcov/rcovtask'
38
- Rcov::RcovTask.new do |t|
39
- t.libs << 'test'
40
- t.test_files = FileList['test/**/*_test.rb']
41
- t.verbose = true
42
- end
43
- rescue LoadError
44
- puts "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
45
- end
46
-
47
19
  task :default => :test
data/lib/slug/slug.rb CHANGED
@@ -1,6 +1,10 @@
1
+ require 'active_support/concern'
2
+
1
3
  module Slug
2
- module ClassMethods
3
-
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+
4
8
  # Call this to set up slug handling on an ActiveRecord model.
5
9
  #
6
10
  # Params:
@@ -8,61 +12,62 @@ module Slug
8
12
  #
9
13
  # Options:
10
14
  # * <tt>:column</tt> - the column the slug will be saved to (defaults to <tt>:slug</tt>)
11
- # * <tt>:validates_uniquness_if</tt> - proc to determine whether uniqueness validation runs, same format as validates_uniquness_of :if
15
+ # * <tt>:validate_uniquness_if</tt> - proc to determine whether uniqueness validation runs, same format as validates_uniquness_of :if
12
16
  #
13
17
  # Slug will take care of validating presence and uniqueness of slug.
14
-
18
+
15
19
  # Before create, Slug will generate and assign the slug if it wasn't explicitly set.
16
20
  # Note that subsequent changes to the source column will have no effect on the slug.
17
21
  # If you'd like to update the slug later on, call <tt>@model.set_slug</tt>
18
22
  def slug source, opts={}
19
- class_inheritable_accessor :slug_source, :slug_column
20
- include InstanceMethods
21
-
23
+ class_attribute :slug_source, :slug_column, :generic_default
24
+
22
25
  self.slug_source = source
23
-
24
- self.slug_column = opts.has_key?(:column) ? opts[:column] : :slug
26
+ self.slug_column = opts.fetch(:column, :slug)
27
+ self.generic_default = opts.fetch(:generic_default, false)
25
28
 
26
29
  uniqueness_opts = {}
27
- uniqueness_opts.merge!(:if => opts[:validates_uniqueness_if]) if opts[:validates_uniqueness_if].present?
28
-
29
- validates_presence_of self.slug_column, :message => "cannot be blank. Is #{self.slug_source} sluggable?"
30
- validates_uniqueness_of self.slug_column, uniqueness_opts
31
- validates_format_of self.slug_column, :with => /^[a-z0-9-]+$/, :message => "contains invalid characters. Only downcase letters, numbers, and '-' are allowed."
32
- before_validation_on_create :set_slug
30
+ uniqueness_opts.merge!(:if => opts[:validate_uniqueness_if]) if opts[:validate_uniqueness_if].present?
31
+ validates_uniqueness_of self.slug_column, uniqueness_opts
32
+
33
+ validates_presence_of self.slug_column,
34
+ message: "cannot be blank. Is #{self.slug_source} sluggable?"
35
+ validates_format_of self.slug_column,
36
+ with: /\A[a-z0-9-]+\z/,
37
+ message: "contains invalid characters. Only downcase letters, numbers, and '-' are allowed."
38
+ before_validation :set_slug, :on => :create
39
+
40
+ include SlugInstanceMethods
33
41
  end
34
42
  end
35
-
36
- module InstanceMethods
37
-
43
+
44
+ module SlugInstanceMethods
38
45
  # Sets the slug. Called before create.
39
46
  # By default, set_slug won't change slug if one already exists. Pass :force => true to overwrite.
40
47
  def set_slug(opts={})
41
- validate_slug_columns
42
- return unless self[self.slug_column].blank? || opts[:force] == true
48
+ validate_slug_columns
49
+ return if self[self.slug_column].present? && !opts[:force]
43
50
 
44
- original_slug = self[self.slug_column]
45
- self[self.slug_column] = self.send(self.slug_source)
51
+ self[self.slug_column] = normalize_slug(self.send(self.slug_source))
46
52
 
47
- strip_diacritics_from_slug
48
- normalize_slug
49
- assign_slug_sequence unless self[self.slug_column] == original_slug # don't try to increment seq if slug hasn't changed
53
+ # if normalize_slug returned a blank string, try the generic_default handling
54
+ if generic_default && self[self.slug_column].blank?
55
+ self[self.slug_column] = self.class.to_s.demodulize.underscore.dasherize
56
+ end
57
+
58
+ assign_slug_sequence if self.changed_attributes.include?(self.slug_column)
50
59
  end
51
-
60
+
52
61
  # Overwrite existing slug based on current contents of source column.
53
62
  def reset_slug
54
63
  set_slug(:force => true)
55
64
  end
56
-
65
+
57
66
  # Overrides to_param to return the model's slug.
58
67
  def to_param
59
68
  self[self.slug_column]
60
69
  end
61
-
62
- def self.included(klass)
63
- klass.extend(ClassMethods)
64
- end
65
-
70
+
66
71
  private
67
72
  # Validates that source and destination methods exist. Invoked at runtime to allow definition
68
73
  # of source/slug methods after <tt>slug</tt> setup in class.
@@ -70,58 +75,27 @@ module Slug
70
75
  raise ArgumentError, "Source column '#{self.slug_source}' does not exist!" if !self.respond_to?(self.slug_source)
71
76
  raise ArgumentError, "Slug column '#{self.slug_column}' does not exist!" if !self.respond_to?("#{self.slug_column}=")
72
77
  end
73
-
78
+
74
79
  # Takes the slug, downcases it and replaces non-word characters with a -.
75
80
  # Feel free to override this method if you'd like different slug formatting.
76
- def normalize_slug
77
- return if self[self.slug_column].blank?
78
- s = ActiveSupport::Multibyte.proxy_class.new(self[self.slug_column]).normalize(:kc)
79
- s.downcase!
80
- s.strip!
81
- s.gsub!(/[^a-z0-9\s-]/, '') # Remove non-word characters
82
- s.gsub!(/\s+/, '-') # Convert whitespaces to dashes
83
- s.gsub!(/-\z/, '') # Remove trailing dashes
84
- s.gsub!(/-+/, '-') # get rid of double-dashes
85
- self[self.slug_column] = s.to_s
81
+ def normalize_slug(str)
82
+ return if str.blank?
83
+ str.gsub!(/[\p{Pc}\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]/, '') # Remove punctuation
84
+ str.parameterize
86
85
  end
87
-
88
- # Converts accented characters to their ASCII equivalents and removes them if they have no equivalent.
89
- # Override this with a void function if you don't want accented characters to be stripped.
90
- def strip_diacritics_from_slug
91
- return if self[self.slug_column].blank?
92
- s = ActiveSupport::Multibyte.proxy_class.new(self[self.slug_column])
93
- s = s.normalize(:kd).unpack('U*')
94
- s = s.inject([]) do |a,u|
95
- if Slug::ASCII_APPROXIMATIONS[u]
96
- a += Slug::ASCII_APPROXIMATIONS[u].unpack('U*')
97
- elsif (u < 0x300 || u > 0x036F)
98
- a << u
99
- end
100
- a
101
- end
102
- s = s.pack('U*')
103
- self[self.slug_column] = s.to_s
104
- end
105
-
86
+
106
87
  # If a slug of the same name already exists, this will append '-n' to the end of the slug to
107
88
  # make it unique. The second instance gets a '-1' suffix.
108
89
  def assign_slug_sequence
109
90
  return if self[self.slug_column].blank?
110
- idx = next_slug_sequence
111
- self[self.slug_column] = "#{self[self.slug_column]}-#{idx}" if idx > 0
112
- end
113
-
114
- # Returns the next unique index for a slug.
115
- def next_slug_sequence
116
- last_in_sequence = self.class.find(:first, :conditions => ["#{self.slug_column} LIKE ?", self[self.slug_column] + '%'],
117
- :order => "CAST(REPLACE(#{self.slug_column},'#{self[self.slug_column]}','') AS UNSIGNED)")
118
- if last_in_sequence.nil?
119
- return 0
120
- else
121
- sequence_match = last_in_sequence[self.slug_column].match(/^#{self[self.slug_column]}(-(\d+))?/)
122
- current = sequence_match.nil? ? 0 : sequence_match[2].to_i
123
- return current + 1
91
+ assoc = self.class.base_class
92
+ base_slug = self[self.slug_column]
93
+ seq = 0
94
+
95
+ while assoc.where(self.slug_column => self[self.slug_column]).exists? do
96
+ seq += 1
97
+ self[self.slug_column] = "#{base_slug}-#{seq}"
124
98
  end
125
99
  end
126
100
  end
127
- end
101
+ end
data/lib/slug.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require File.join(File.dirname(__FILE__), 'slug', 'slug')
2
- require File.join(File.dirname(__FILE__), 'slug', 'ascii_approximations')
3
2
 
4
- if defined?(ActiveRecord)
5
- ActiveRecord::Base.instance_eval { extend Slug::ClassMethods }
6
- end
3
+ ActiveRecord::Base.instance_eval { include Slug }
4
+ if defined?(Rails) && Rails.version.to_i < 4
5
+ raise "This version of slug requires Rails 4 or higher"
6
+ end
data/slug.gemspec CHANGED
@@ -1,61 +1,58 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
1
  # -*- encoding: utf-8 -*-
2
+ # stub: slug 4.0.0 ruby lib
5
3
 
6
4
  Gem::Specification.new do |s|
7
- s.name = %q{slug}
8
- s.version = "0.5.7"
5
+ s.name = "slug"
6
+ s.version = "4.1.1"
9
7
 
10
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
+ s.require_paths = ["lib"]
11
10
  s.authors = ["Ben Koski"]
12
- s.date = %q{2010-06-25}
13
- s.description = %q{Simple, straightforward slugs for your ActiveRecord models.}
14
- s.email = %q{ben.koski@gmail.com}
11
+ s.date = "2018-11-17"
12
+ s.description = "Simple, straightforward slugs for your ActiveRecord models."
13
+ s.email = "ben.koski@gmail.com"
15
14
  s.extra_rdoc_files = [
16
15
  "LICENSE",
17
- "README.rdoc"
16
+ "README.md"
18
17
  ]
19
18
  s.files = [
20
- ".gitignore",
21
- "LICENSE",
22
- "README.rdoc",
23
- "Rakefile",
24
- "VERSION.yml",
25
- "lib/slug.rb",
26
- "lib/slug/ascii_approximations.rb",
27
- "lib/slug/slug.rb",
28
- "slug.gemspec",
29
- "test/models.rb",
30
- "test/schema.rb",
31
- "test/test_helper.rb",
32
- "test/test_slug.rb"
33
- ]
34
- s.homepage = %q{http://github.com/bkoski/slug}
35
- s.rdoc_options = ["--charset=UTF-8"]
36
- s.require_paths = ["lib"]
37
- s.rubygems_version = %q{1.3.5}
38
- s.summary = %q{Simple, straightforward slugs for your ActiveRecord models.}
39
- s.test_files = [
19
+ "LICENSE",
20
+ "README.md",
21
+ "Rakefile",
22
+ "lib/slug.rb",
23
+ "lib/slug/slug.rb",
24
+ "slug.gemspec",
40
25
  "test/models.rb",
41
- "test/schema.rb",
42
- "test/test_helper.rb",
43
- "test/test_slug.rb"
26
+ "test/schema.rb",
27
+ "test/test_helper.rb",
28
+ "test/slug_test.rb"
44
29
  ]
30
+ s.homepage = "http://github.com/bkoski/slug"
31
+ s.rubygems_version = "2.2.0"
32
+ s.summary = "Simple, straightforward slugs for your ActiveRecord models."
45
33
 
46
34
  if s.respond_to? :specification_version then
47
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
48
- s.specification_version = 3
35
+ s.specification_version = 4
49
36
 
50
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
51
- s.add_runtime_dependency(%q<activerecord>, [">= 0"])
52
- s.add_runtime_dependency(%q<activesupport>, [">= 0"])
37
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
38
+ s.add_runtime_dependency(%q<rake>, [">= 0"])
39
+ s.add_development_dependency(%q<minitest>, [">= 0"])
40
+ s.add_development_dependency(%q<sqlite3>, [">= 0"])
41
+ s.add_runtime_dependency(%q<activerecord>, ["> 3.0.0"])
42
+ s.add_runtime_dependency(%q<activesupport>, ["> 3.0.0"])
53
43
  else
54
- s.add_dependency(%q<activerecord>, [">= 0"])
55
- s.add_dependency(%q<activesupport>, [">= 0"])
44
+ s.add_dependency(%q<rake>, [">= 0"])
45
+ s.add_dependency(%q<minitest>, [">= 0"])
46
+ s.add_dependency(%q<sqlite3>, [">= 0"])
47
+ s.add_dependency(%q<activerecord>, ["> 3.0.0"])
48
+ s.add_dependency(%q<activesupport>, ["> 3.0.0"])
56
49
  end
57
50
  else
58
- s.add_dependency(%q<activerecord>, [">= 0"])
59
- s.add_dependency(%q<activesupport>, [">= 0"])
51
+ s.add_dependency(%q<rake>, [">= 0"])
52
+ s.add_dependency(%q<minitest>, [">= 0"])
53
+ s.add_dependency(%q<sqlite3>, [">= 0"])
54
+ s.add_dependency(%q<activerecord>, ["> 3.0.0"])
55
+ s.add_dependency(%q<activesupport>, ["> 3.0.0"])
60
56
  end
61
57
  end
58
+
data/test/models.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # Used to test slug behavior in general
2
2
  class Article < ActiveRecord::Base
3
- slug :headline
3
+ slug :headline
4
+ end
5
+
6
+ class Storyline < Article
4
7
  end
5
8
 
6
9
  # Used to test alternate slug column
@@ -14,14 +17,23 @@ class Company < ActiveRecord::Base
14
17
  end
15
18
 
16
19
  class Post < ActiveRecord::Base
17
- slug :headline, :validates_uniqueness_if => Proc.new { false }
20
+ slug :headline, :validate_uniqueness_if => Proc.new { false }
18
21
  end
19
22
 
20
23
  # Used to test slugs based on methods rather than database attributes
21
24
  class Event < ActiveRecord::Base
22
25
  slug :title_for_slug
23
-
26
+
24
27
  def title_for_slug
25
28
  "#{title}-#{location}"
26
29
  end
27
- end
30
+ end
31
+
32
+ # Test generation of generic slugs
33
+ class Generation < ActiveRecord::Base
34
+ slug :title, generic_default: true
35
+ end
36
+
37
+ # Test model with no slug column
38
+ class Orphan < ActiveRecord::Base
39
+ end
data/test/schema.rb CHANGED
@@ -1,30 +1,41 @@
1
1
  ActiveRecord::Schema.define(:version => 1) do
2
-
3
2
  create_table "articles", :force => true do |t|
4
- t.column "headline", "string"
3
+ t.column "headline", "string", null: false
5
4
  t.column "section", "string"
6
- t.column "slug", "string"
5
+ t.column "slug", "string", null: false
6
+ t.column "type", "string"
7
+ t.index ["slug"], name: "index_articles_on_slug", unique: true
7
8
  end
8
-
9
+
9
10
  create_table "people", :force => true do |t|
10
11
  t.column "name", "string"
11
12
  t.column "web_slug", "string"
12
13
  end
13
-
14
+
14
15
  create_table "companies", :force => true do |t|
15
16
  t.column "name", "string"
16
17
  t.column "slug", "string"
17
18
  end
18
-
19
+
19
20
  create_table "posts", :force => true do |t|
20
21
  t.column "headline", "string"
21
22
  t.column "slug", "string"
22
23
  end
23
-
24
+
24
25
  create_table "events", :force => true do |t|
25
26
  t.column "title", "string"
26
27
  t.column "location", "string"
27
28
  t.column "slug", "string"
28
29
  end
29
-
30
- end
30
+
31
+ create_table "generations", :force => true do |t|
32
+ t.column "title", "string"
33
+ t.column "slug", "string", null: false
34
+ end
35
+
36
+ # table with no slug column
37
+ create_table "orphans", :force => true do |t|
38
+ t.column "name", "string"
39
+ t.column "location", "string"
40
+ end
41
+ end