revolutionhealth-acts_as_seo_friendly 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 1.0.0 2008-06-13
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/License.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 FIXME full name
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
@@ -0,0 +1,28 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ PostInstall.txt
5
+ README.txt
6
+ Rakefile
7
+ acts_as_seo_friendly.gemspec
8
+ config/hoe.rb
9
+ config/requirements.rb
10
+ lib/acts_as_seo_friendly.rb
11
+ lib/acts_as_seo_friendly/version.rb
12
+ script/console
13
+ script/destroy
14
+ script/generate
15
+ script/txt2html
16
+ setup.rb
17
+ tasks/deployment.rake
18
+ tasks/environment.rake
19
+ tasks/website.rake
20
+ test/seo_test_model.rb
21
+ test/seo_test_model_conditions.rb
22
+ test/test_acts_as_seo_friendly.rb
23
+ test/test_helper.rb
24
+ website/index.html
25
+ website/index.txt
26
+ website/javascripts/rounded_corners_lite.inc.js
27
+ website/stylesheets/screen.css
28
+ website/template.html.erb
data/PostInstall.txt ADDED
@@ -0,0 +1,3 @@
1
+
2
+ For more information on acts_as_seo_friendly, see http://github.com/revolutionhealth/acts_as_seo_friendly/tree/master
3
+
data/README.txt ADDED
@@ -0,0 +1,86 @@
1
+ = acts_as_seo_friendly
2
+
3
+ http://revolutiononrails.blogspot.com/
4
+
5
+ == DESCRIPTION:
6
+
7
+ Create an SEO friendly field for a model automatically based on a given field.
8
+
9
+ So if you have a Blogs model, and you would like create an SEO friendly version
10
+ of the 'title' field, you would just add this to your model and then be able to
11
+ use the SEO friendly id as the unique id to the resource. The plugin will only
12
+ append an integer to the SEO id if there is a collision.
13
+
14
+ == FEATURES/PROBLEMS:
15
+
16
+ * Only tested on mysql and sqlite3
17
+
18
+
19
+ == SYNOPSIS:
20
+
21
+
22
+ Create seo column migration:
23
+
24
+ class CreateSeoTestModels < ActiveRecord::Migration
25
+ def self.up
26
+ create_table :seo_test_models do |t|
27
+ t.string :name
28
+ t.timestamps
29
+ end
30
+ SeoTestModel.create_seo_friendly_column()
31
+ end
32
+
33
+ def self.down
34
+ SeoTestModel.drop_seo_friendly_column()
35
+ drop_table :seo_test_models
36
+ end
37
+ end
38
+
39
+
40
+ Add to model:
41
+
42
+ class SeoTestModel < ActiveRecord::Base
43
+ acts_as_seo_friendly :resource_id => :name,
44
+ :seo_friendly_id_field => :seo_id, # default is :seo_friendly_id
45
+ :seo_friendly_id_limit => 100 # default is 50
46
+ end
47
+
48
+
49
+ To lookup the resource in the controllers use:
50
+
51
+ SeoTestModel.find_by_seo_id(params[:id])
52
+
53
+
54
+
55
+ == REQUIREMENTS:
56
+
57
+
58
+ == INSTALL:
59
+
60
+ * sudo gem install revolutionhealth-acts_as_seo_friendly -s http://gems.github.com
61
+
62
+
63
+ == LICENSE:
64
+
65
+ (The MIT License)
66
+
67
+ Copyright (c) 2008 Revolution Health Group LLC
68
+
69
+ Permission is hereby granted, free of charge, to any person obtaining
70
+ a copy of this software and associated documentation files (the
71
+ 'Software'), to deal in the Software without restriction, including
72
+ without limitation the rights to use, copy, modify, merge, publish,
73
+ distribute, sublicense, and/or sell copies of the Software, and to
74
+ permit persons to whom the Software is furnished to do so, subject to
75
+ the following conditions:
76
+
77
+ The above copyright notice and this permission notice shall be
78
+ included in all copies or substantial portions of the Software.
79
+
80
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
81
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
82
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
83
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
84
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
85
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
86
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
5
+
6
+ task :gemspec do
7
+ gemspec_file = File.join(File.dirname(__FILE__), "#{GEM_NAME}.gemspec")
8
+ `rake debug_gem > #{gemspec_file}`
9
+ gemspec = File.readlines(gemspec_file)
10
+ gemspec.delete_at(0)
11
+ File.open(gemspec_file, "w+") {|f| f << gemspec.to_s() }
12
+ end
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{acts_as_seo_friendly}
3
+ s.version = "1.1.0"
4
+
5
+ s.specification_version = 2 if s.respond_to? :specification_version=
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Revolution Health"]
9
+ s.date = %q{2008-06-16}
10
+ s.description = %q{provides a seo friendly version of field on a table}
11
+ s.email = ["opensource@revolutionhealth.com"]
12
+ s.extra_rdoc_files = ["History.txt", "License.txt", "Manifest.txt", "PostInstall.txt", "README.txt", "website/index.txt"]
13
+ s.files = ["History.txt", "License.txt", "Manifest.txt", "PostInstall.txt", "README.txt", "Rakefile", "acts_as_seo_friendly.gemspec", "config/hoe.rb", "config/requirements.rb", "lib/acts_as_seo_friendly.rb", "lib/acts_as_seo_friendly/version.rb", "script/console", "script/destroy", "script/generate", "script/txt2html", "setup.rb", "tasks/deployment.rake", "tasks/environment.rake", "tasks/website.rake", "test/seo_test_model.rb", "test/seo_test_model_conditions.rb", "test/test_acts_as_seo_friendly.rb", "test/test_helper.rb", "website/index.html", "website/index.txt", "website/javascripts/rounded_corners_lite.inc.js", "website/stylesheets/screen.css", "website/template.html.erb"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://github.com/revolutionhealth/acts_as_seo_friendly/tree/master}
16
+ s.post_install_message = %q{
17
+ For more information on acts_as_seo_friendly, see http://github.com/revolutionhealth/acts_as_seo_friendly/tree/master
18
+
19
+ }
20
+ s.rdoc_options = ["--main", "README.txt"]
21
+ s.require_paths = ["lib"]
22
+ s.rubyforge_project = %q{acts_as_seo_friendly}
23
+ s.rubygems_version = %q{1.1.1}
24
+ s.summary = %q{provides a seo friendly version of field on a table}
25
+ s.test_files = ["test/test_acts_as_seo_friendly.rb", "test/test_helper.rb"]
26
+ end
data/config/hoe.rb ADDED
@@ -0,0 +1,73 @@
1
+ require 'acts_as_seo_friendly/version'
2
+
3
+ AUTHOR = 'Revolution Health' # can also be an array of Authors
4
+ EMAIL = "opensource@revolutionhealth.com"
5
+ DESCRIPTION = "provides a seo friendly version of field on a table"
6
+ GEM_NAME = 'acts_as_seo_friendly' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'acts_as_seo_friendly' # The unix name for your project
8
+ HOMEPATH = "http://github.com/revolutionhealth/acts_as_seo_friendly/tree/master"
9
+ DOWNLOAD_PATH = "http://github.com/revolutionhealth/acts_as_seo_friendly/tree/master"
10
+ EXTRA_DEPENDENCIES = [
11
+ # ['activesupport', '>= 1.3.1']
12
+ ] # An array of rubygem dependencies [name, version]
13
+
14
+ @config_file = "~/.rubyforge/user-config.yml"
15
+ @config = nil
16
+ RUBYFORGE_USERNAME = "unknown"
17
+ def rubyforge_username
18
+ unless @config
19
+ begin
20
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
21
+ rescue
22
+ puts <<-EOS
23
+ ERROR: No rubyforge config file found: #{@config_file}
24
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
25
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
26
+ EOS
27
+ exit
28
+ end
29
+ end
30
+ RUBYFORGE_USERNAME.replace @config["username"]
31
+ end
32
+
33
+
34
+ REV = nil
35
+ # UNCOMMENT IF REQUIRED:
36
+ # REV = YAML.load(`svn info`)['Revision']
37
+ VERS = ActsAsSeoFriendly::VERSION::STRING + (REV ? ".#{REV}" : "")
38
+ RDOC_OPTS = ['--quiet', '--title', 'acts_as_seo_friendly documentation',
39
+ "--opname", "index.html",
40
+ "--line-numbers",
41
+ "--main", "README",
42
+ "--inline-source"]
43
+
44
+ class Hoe
45
+ def extra_deps
46
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
47
+ @extra_deps
48
+ end
49
+ end
50
+
51
+ # Generate all the Rake tasks
52
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
53
+ $hoe = Hoe.new(GEM_NAME, VERS) do |p|
54
+ p.developer(AUTHOR, EMAIL)
55
+ p.description = DESCRIPTION
56
+ p.summary = DESCRIPTION
57
+ p.url = HOMEPATH
58
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
59
+ p.test_globs = ["test/**/test_*.rb"]
60
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
61
+
62
+ # == Optional
63
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
64
+ #p.extra_deps = EXTRA_DEPENDENCIES
65
+
66
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
67
+ end
68
+
69
+ CHANGES = $hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
70
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
71
+ $hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
72
+ $hoe.rsync_args = '-av --delete --ignore-errors'
73
+ $hoe.spec.post_install_message = File.open(File.dirname(__FILE__) + "/../PostInstall.txt").read rescue ""
@@ -0,0 +1,15 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen mocha].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
@@ -0,0 +1,165 @@
1
+
2
+ module ActiveRecord
3
+ module Acts #:nodoc:
4
+ module SeoFriendly #:nodoc:
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module MigrationMethods
10
+ def create_seo_friendly_column()
11
+ seo_column_name = read_inheritable_attribute(:seo_friendly_options)[:seo_friendly_id_field].to_s
12
+ seo_column_limit = read_inheritable_attribute(:seo_friendly_options)[:seo_friendly_id_limit].to_i
13
+ self.connection.add_column table_name(), seo_column_name, :string, :null => true, :limit => seo_column_limit
14
+ self.connection.add_index table_name(), seo_column_name, :unique => true
15
+ end
16
+
17
+ def drop_seo_friendly_column()
18
+ seo_column_name = read_inheritable_attribute(:seo_friendly_options)[:seo_friendly_id_field].to_s
19
+ self.connection.remove_index table_name(), seo_column_name
20
+ self.connection.remove_column table_name(), seo_column_name
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ #
26
+ # Use find_by_seo_friendly_id
27
+ def acts_as_seo_friendly(options = {})
28
+ options = {:seo_friendly_id_field => :seo_friendly_id, :seo_friendly_id_limit => 50}.merge(options)
29
+ write_inheritable_attribute(:seo_friendly_options, options)
30
+
31
+ after_save :create_seo_friendly_id
32
+ to_param_with(read_inheritable_attribute(:seo_friendly_options)[:seo_friendly_id_field])
33
+
34
+ if !self.included_modules.include?(ActiveRecord::Acts::SeoFriendly::InstanceMethods)
35
+ include ActiveRecord::Acts::SeoFriendly::InstanceMethods
36
+ end
37
+ end
38
+
39
+ include ActiveRecord::Acts::SeoFriendly::MigrationMethods
40
+
41
+ private
42
+ def to_param_with(attr_sym)
43
+ return if attr_sym.nil?
44
+ attr_str = attr_sym.to_s
45
+ class_eval <<-EOS
46
+ def to_param
47
+ (#{attr_str} = self.#{attr_str}) ? #{attr_str} : nil
48
+ end
49
+ EOS
50
+ end
51
+ end
52
+
53
+
54
+ module InstanceMethods
55
+ private
56
+
57
+ INITITAL_SEO_UNIQUE_DIGITS = 4 # initially allow for 1000 collisions..
58
+
59
+ def create_seo_friendly_id
60
+ ## return if there are errors
61
+ return if self.errors.length > 0
62
+
63
+ seo_id_field = self.class.read_inheritable_attribute(:seo_friendly_options)[:seo_friendly_id_field].to_s
64
+ count_seo_id_field = "count_#{seo_id_field}"
65
+ count_seo_id_field_N = "#{count_seo_id_field}_N"
66
+
67
+ resource_id_field = self.class.read_inheritable_attribute(:seo_friendly_options)[:resource_id].to_s
68
+ resource_id_value = self[resource_id_field]
69
+
70
+ return if resource_id_value.blank?
71
+
72
+ seo_id_value = create_seo_friendly_str(resource_id_value)
73
+
74
+ return if (self[seo_id_field] =~ /^#{seo_id_value}$/) || (self[seo_id_field] =~ /^#{seo_id_value}\-\d+$/)
75
+
76
+ self.class.transaction do
77
+ unique_id = determine_unique_id(seo_id_field, count_seo_id_field, count_seo_id_field_N, seo_id_value)
78
+ seo_field_value = "#{seo_id_value}" + (unique_id != nil ? "-#{unique_id}" : "")
79
+
80
+ seo_friendly_id_limit = self.class.read_inheritable_attribute(:seo_friendly_options)[:seo_friendly_id_limit].to_i
81
+
82
+ if seo_field_value.size > seo_friendly_id_limit
83
+ seo_id_value = create_seo_friendly_str(resource_id_value, INITITAL_SEO_UNIQUE_DIGITS + (seo_field_value.size - seo_friendly_id_limit))
84
+ unique_id = determine_unique_id(seo_id_field, count_seo_id_field, count_seo_id_field_N, seo_id_value)
85
+
86
+ seo_field_value = "#{seo_id_value}" + (unique_id != nil ? "-#{unique_id}" : "")
87
+ seo_field_value = self['id'] if seo_field_value.size > seo_friendly_id_limit # still doesn't fit..give up, store the id
88
+ end
89
+
90
+ self.class.update_all("#{seo_id_field} = \'#{seo_field_value}\'", ["id = ?", self.id])
91
+ # set it so that it can be used after this..
92
+ self[seo_id_field] = seo_field_value
93
+ end
94
+
95
+ true
96
+ end
97
+
98
+
99
+ def determine_unique_id(seo_id_field, count_seo_id_field, count_seo_id_field_N, seo_id_value)
100
+ conditions_proc = self.class.read_inheritable_attribute(:seo_friendly_options)[:conditions]
101
+ conditions_option = execute_block(conditions_proc)
102
+ conditions = (!conditions_option.blank? ? " AND #{self.class.send(:sanitize_sql, conditions_option)} " : "")
103
+
104
+ counts = self.class.find_by_sql(%Q(
105
+ select count(#{seo_id_field}) as #{count_seo_id_field}, NULL as #{count_seo_id_field_N} from #{self.class.table_name} where #{seo_id_field} = '#{seo_id_value}' #{conditions}
106
+ UNION
107
+ select NULL as #{count_seo_id_field}, count(#{seo_id_field}) as #{count_seo_id_field_N} from #{self.class.table_name} where #{seo_id_field} like '#{seo_id_value}-%' #{conditions};)
108
+ )
109
+
110
+ # can't guarantee order of results
111
+ count_seo_id_value = counts[0][count_seo_id_field].to_i
112
+ if counts[0][count_seo_id_field].nil?
113
+ count_seo_id_value = counts[1][count_seo_id_field].to_i
114
+ end
115
+
116
+ count_seo_id_N_value = counts[0][count_seo_id_field_N].to_i
117
+ if counts[0][count_seo_id_field_N].nil?
118
+ count_seo_id_N_value = counts[1][count_seo_id_field_N].to_i
119
+ end
120
+
121
+ result = nil
122
+ if (count_seo_id_value != 0)
123
+ last = count_seo_id_N_value
124
+ result = last + 1
125
+ end
126
+ return result
127
+ end
128
+
129
+ def create_seo_friendly_str(str, digits = INITITAL_SEO_UNIQUE_DIGITS)
130
+ s = str.dup
131
+ s.gsub!(/\'/, '')
132
+ s.gsub!(/\W+/, ' ')
133
+ s.strip!
134
+ s.downcase!
135
+ s.gsub!(/\ +/, '-')
136
+ s.gsub!(/\-{2}/, '-')
137
+ limit = self.class.read_inheritable_attribute(:seo_friendly_options)[:seo_friendly_id_limit].to_i - digits
138
+ # if we are trimming on a word, attempt to pretty it up and trim on a boundary (if available)
139
+ if s.length > limit && (s[limit, 1] =~ /[^\-]{1}/) != nil && (last_boundary_index = s[0..(limit - 1)].rindex('-')) != nil
140
+ limit = last_boundary_index
141
+ end
142
+ s = s[0..(limit - 1)]
143
+ ## final check to make sure no trailing -'s
144
+ s.gsub!(/\-$/,'')
145
+ return s
146
+ end
147
+
148
+ # Given a piece of code, this method calls send(code) if code is a symbol, code.call(self) if it is callable
149
+ # or simply executes it
150
+ def execute_block(block)
151
+ case
152
+ when block.is_a?(Symbol)
153
+ send(block)
154
+ when block.respond_to?(:call) && (block.arity == 1 || block.arity == -1)
155
+ block.call(self)
156
+ else
157
+ block
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ ActiveRecord::Base.send(:include, ActiveRecord::Acts::SeoFriendly)