vidibus-permalink 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.bundle/config ADDED
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_DISABLE_SHARED_GEMS: "1"
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format nested
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source :rubygems
2
+
3
+ gem "rails", "~> 3.0.0"
4
+ gem "mongoid", "~> 2.0.0.beta.20"
5
+ gem "vidibus-core_extensions"
6
+ gem "vidibus-uuid"
7
+ gem "vidibus-words"
8
+
9
+ # Development dependecies
10
+ gem "jeweler"
11
+ gem "rake"
12
+ gem "rspec", "~> 2.0.0.beta.20"
13
+ gem "rr"
14
+ gem "relevance-rcov"
data/Gemfile.lock ADDED
@@ -0,0 +1,120 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ abstract (1.0.0)
5
+ actionmailer (3.0.1)
6
+ actionpack (= 3.0.1)
7
+ mail (~> 2.2.5)
8
+ actionpack (3.0.1)
9
+ activemodel (= 3.0.1)
10
+ activesupport (= 3.0.1)
11
+ builder (~> 2.1.2)
12
+ erubis (~> 2.6.6)
13
+ i18n (~> 0.4.1)
14
+ rack (~> 1.2.1)
15
+ rack-mount (~> 0.6.12)
16
+ rack-test (~> 0.5.4)
17
+ tzinfo (~> 0.3.23)
18
+ activemodel (3.0.1)
19
+ activesupport (= 3.0.1)
20
+ builder (~> 2.1.2)
21
+ i18n (~> 0.4.1)
22
+ activerecord (3.0.1)
23
+ activemodel (= 3.0.1)
24
+ activesupport (= 3.0.1)
25
+ arel (~> 1.0.0)
26
+ tzinfo (~> 0.3.23)
27
+ activeresource (3.0.1)
28
+ activemodel (= 3.0.1)
29
+ activesupport (= 3.0.1)
30
+ activesupport (3.0.1)
31
+ arel (1.0.1)
32
+ activesupport (~> 3.0.0)
33
+ bson (1.1.2)
34
+ builder (2.1.2)
35
+ diff-lcs (1.1.2)
36
+ erubis (2.6.6)
37
+ abstract (>= 1.0.0)
38
+ gemcutter (0.6.1)
39
+ git (1.2.5)
40
+ i18n (0.4.2)
41
+ jeweler (1.4.0)
42
+ gemcutter (>= 0.1.0)
43
+ git (>= 1.2.5)
44
+ rubyforge (>= 2.0.0)
45
+ json_pure (1.4.6)
46
+ macaddr (1.0.0)
47
+ mail (2.2.9)
48
+ activesupport (>= 2.3.6)
49
+ i18n (~> 0.4.1)
50
+ mime-types (~> 1.16)
51
+ treetop (~> 1.4.8)
52
+ mime-types (1.16)
53
+ mongo (1.1.2)
54
+ bson (>= 1.1.1)
55
+ mongoid (2.0.0.beta.20)
56
+ activemodel (~> 3.0)
57
+ mongo (~> 1.1)
58
+ tzinfo (~> 0.3.22)
59
+ will_paginate (~> 3.0.pre)
60
+ polyglot (0.3.1)
61
+ rack (1.2.1)
62
+ rack-mount (0.6.13)
63
+ rack (>= 1.0.0)
64
+ rack-test (0.5.6)
65
+ rack (>= 1.0)
66
+ rails (3.0.1)
67
+ actionmailer (= 3.0.1)
68
+ actionpack (= 3.0.1)
69
+ activerecord (= 3.0.1)
70
+ activeresource (= 3.0.1)
71
+ activesupport (= 3.0.1)
72
+ bundler (~> 1.0.0)
73
+ railties (= 3.0.1)
74
+ railties (3.0.1)
75
+ actionpack (= 3.0.1)
76
+ activesupport (= 3.0.1)
77
+ rake (>= 0.8.4)
78
+ thor (~> 0.14.0)
79
+ rake (0.8.7)
80
+ relevance-rcov (0.9.2.1)
81
+ rr (1.0.2)
82
+ rspec (2.0.1)
83
+ rspec-core (~> 2.0.1)
84
+ rspec-expectations (~> 2.0.1)
85
+ rspec-mocks (~> 2.0.1)
86
+ rspec-core (2.0.1)
87
+ rspec-expectations (2.0.1)
88
+ diff-lcs (>= 1.1.2)
89
+ rspec-mocks (2.0.1)
90
+ rspec-core (~> 2.0.1)
91
+ rspec-expectations (~> 2.0.1)
92
+ rubyforge (2.0.4)
93
+ json_pure (>= 1.1.7)
94
+ thor (0.14.4)
95
+ treetop (1.4.8)
96
+ polyglot (>= 0.3.1)
97
+ tzinfo (0.3.23)
98
+ uuid (2.3.1)
99
+ macaddr (~> 1.0)
100
+ vidibus-core_extensions (0.3.11)
101
+ vidibus-uuid (0.3.8)
102
+ mongoid (~> 2.0.0.beta.20)
103
+ uuid (~> 2.3.1)
104
+ vidibus-words (0.0.0)
105
+ will_paginate (3.0.pre2)
106
+
107
+ PLATFORMS
108
+ ruby
109
+
110
+ DEPENDENCIES
111
+ jeweler
112
+ mongoid (~> 2.0.0.beta.20)
113
+ rails (~> 3.0.0)
114
+ rake
115
+ relevance-rcov
116
+ rr
117
+ rspec (~> 2.0.0.beta.20)
118
+ vidibus-core_extensions
119
+ vidibus-uuid
120
+ vidibus-words
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Andre Pankratz
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/README.rdoc ADDED
@@ -0,0 +1,37 @@
1
+ = vidibus-permalink
2
+
3
+ This gem allows changeable permalinks. That's an oxymoron, but it's really useful from a SEO perspective.
4
+
5
+ It is part of the open source SOA framework Vidibus: http://vidibus.org
6
+
7
+
8
+ == Installation
9
+
10
+ Add the dependency to the Gemfile of your application:
11
+
12
+ gem "vidibus-permalink"
13
+
14
+ Then call bundle install on your console.
15
+
16
+
17
+ == Usage
18
+
19
+ TODO: describe
20
+
21
+
22
+ == TODO
23
+
24
+ * Add controller extension for automatic dispatching
25
+ * Limit length of permalinks
26
+
27
+
28
+ == Ideas (for a separate gem)
29
+
30
+ * Catch 404s and store invalid routes.
31
+ * Make invalid routes assignable from a web interface.
32
+ * Try to suggest a matching Linkable by valid parts of the request path.
33
+
34
+
35
+ == Copyright
36
+
37
+ Copyright (c) 2010 Andre Pankratz. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ require "rubygems"
2
+ require "rake"
3
+ require "rake/rdoctask"
4
+ require "rspec"
5
+ require "rspec/core/rake_task"
6
+
7
+ begin
8
+ require "jeweler"
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.name = "vidibus-permalink"
11
+ gem.summary = %Q{Permalink handling}
12
+ gem.description = %Q{Allows changeable permalinks (good for SEO).}
13
+ gem.email = "andre@vidibus.com"
14
+ gem.homepage = "http://github.com/vidibus/vidibus-permalink"
15
+ gem.authors = ["Andre Pankratz"]
16
+ gem.add_dependency "rails", "~> 3.0.0"
17
+ gem.add_dependency "mongoid", "~> 2.0.0.beta.20"
18
+ gem.add_dependency "vidibus-core_extensions"
19
+ gem.add_dependency "vidibus-uuid"
20
+ gem.add_dependency "vidibus-words"
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
25
+ end
26
+
27
+ Rspec::Core::RakeTask.new(:rcov) do |t|
28
+ t.pattern = "spec/**/*_spec.rb"
29
+ t.rcov = true
30
+ t.rcov_opts = ["--exclude", "^spec,/gems/"]
31
+ end
32
+
33
+ Rake::RDocTask.new do |rdoc|
34
+ version = File.exist?("VERSION") ? File.read("VERSION") : ""
35
+ rdoc.rdoc_dir = "rdoc"
36
+ rdoc.title = "vidibus-permalink #{version}"
37
+ rdoc.rdoc_files.include("README*")
38
+ rdoc.rdoc_files.include("lib/**/*.rb")
39
+ rdoc.options << "--charset=utf-8"
40
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,161 @@
1
+ class Permalink
2
+ include Mongoid::Document
3
+ include Mongoid::Timestamps
4
+
5
+ class UuidRequiredError < StandardError; end
6
+
7
+ field :value
8
+ field :linkable_class
9
+ field :linkable_uuid
10
+ field :_current, :type => Boolean, :default => true
11
+
12
+ before_save :set_current
13
+ after_destroy :set_last_current, :if => :current?
14
+
15
+ validates :linkable_uuid, :uuid => true
16
+ validates :value, :linkable_class, :presence => true
17
+
18
+ index :value
19
+
20
+ # Sets object as linkable.
21
+ def linkable=(obj)
22
+ @linkable = nil
23
+ self.linkable_class = obj.class.to_s
24
+ if uuid = obj.try!(:uuid)
25
+ self.linkable_uuid = uuid
26
+ else
27
+ raise UuidRequiredError.new("The linkable object must respond to #uuid. The gem vidibus-uuid will help you.")
28
+ end
29
+ end
30
+
31
+ # Returns the linkable object.
32
+ def linkable
33
+ @linkable ||= begin
34
+ return unless linkable_class and linkable_uuid
35
+ linkable_class.constantize.where(:uuid => linkable_uuid).first
36
+ end
37
+ end
38
+
39
+ # Assigns given string as value.
40
+ # Sanitizes and increments string, if necessary.
41
+ def value=(string)
42
+ unless string == value
43
+ string = sanitize(string)
44
+ unless string == value
45
+ string = increment(string)
46
+ self.write_attribute(:value, string)
47
+ end
48
+ end
49
+ string
50
+ end
51
+
52
+ # Returns true if this permalink is the current one
53
+ # of the assigned linkable.
54
+ def current?
55
+ @is_current ||= !!_current
56
+ end
57
+
58
+ # Returns the current permalink of the assigned linkable.
59
+ def current
60
+ @current ||= begin
61
+ if current?
62
+ self
63
+ else
64
+ Permalink.where(:linkable_uuid => linkable_uuid, :_current => true).first
65
+ end
66
+ end
67
+ end
68
+
69
+ class << self
70
+
71
+ # Scope method for finding Permalinks for given object.
72
+ def for_linkable(object)
73
+ where(:linkable_uuid => object.uuid)
74
+ end
75
+
76
+ # Scope method for finding Permalinks for given value.
77
+ # The value will be sanitized.
78
+ def for_value(value)
79
+ where(:value => sanitize(value))
80
+ end
81
+
82
+ # Returns a dispatcher object for given path.
83
+ def dispatch(path)
84
+ Vidibus::Permalink::Dispatcher.new(path)
85
+ end
86
+ end
87
+
88
+ protected
89
+
90
+ # Removes stopwords and turns string into a permalink-formatted one.
91
+ # However, if the stopwords-free value is blank or it already exists
92
+ # in the database, the full value will be used.
93
+ def sanitize(string)
94
+ return if string.blank?
95
+ clean = Permalink.remove_stopwords(string)
96
+ unless clean.blank? or clean == string
97
+ clean = clean.permalink
98
+ sanitized = clean unless existing(clean).any?
99
+ end
100
+ sanitized || string.permalink
101
+ end
102
+
103
+ # Sanitize string: Remove stopwords and format as permalink.
104
+ # See Vidibus::CoreExtensions::String for details.
105
+ def self.sanitize(string)
106
+ return if string.blank?
107
+ remove_stopwords(string).permalink
108
+ end
109
+
110
+ # Tries to remove stopwords from string.
111
+ # If the resulting string is blank, the original one will be returned.
112
+ # See Vidibus::Words for details.
113
+ def self.remove_stopwords(string)
114
+ words = Vidibus::Words.new(string)
115
+ clean = words.keywords(10).join(" ")
116
+ clean.blank? ? string : clean
117
+ end
118
+
119
+ # Appends next available number to string if it's already in use.
120
+ def increment(string)
121
+ _existing = existing(string)
122
+ return string unless _existing.any?
123
+ number = 1
124
+ while true
125
+ number += 1
126
+ desired = "#{string}-#{number}"
127
+ unless _existing.detect {|e| e.value == desired}
128
+ return desired
129
+ end
130
+ end
131
+ end
132
+
133
+ # Finds existing permalinks with current value.
134
+ def existing(string)
135
+ @existing ||= {}
136
+ @existing[string] ||= Permalink.where(:value => /^#{string}(-\d+)?$/).excludes(:_id => id).to_a
137
+ end
138
+
139
+ # Sets this permalink as the current one and unsets all others.
140
+ def set_current
141
+ unset_other_current
142
+ self._current = true
143
+ end
144
+
145
+ # Sets _current to false on all permalinks of the assigned linkable.
146
+ def unset_other_current
147
+ return unless linkable
148
+ collection.update(
149
+ {:linkable_uuid => linkable_uuid, :_id => {"$ne" => _id}},
150
+ {"$set" => {:_current => false}},
151
+ {:multi => true}
152
+ )
153
+ end
154
+
155
+ # Sets the lastly updated permalink of the assigned linkable as current one.
156
+ def set_last_current
157
+ if last = Permalink.where(:linkable_uuid => linkable_uuid).order_by(:updated_at.desc).limit(1).first
158
+ last.update_attributes!(:_current => true)
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,83 @@
1
+ module Vidibus
2
+ module Permalink
3
+ class Dispatcher
4
+
5
+ class PathError < StandardError; end
6
+
7
+ # Initialize a new Dispatcher instance.
8
+ # Provide an absolute +path+ to be dispatched.
9
+ def initialize(path)
10
+ unless path.match(/^\//)
11
+ raise PathError, "Path must be absolute."
12
+ end
13
+ @path = path
14
+ end
15
+
16
+ # Returns the path to dispatch.
17
+ def path
18
+ @path
19
+ end
20
+
21
+ # Returns parts of the path.
22
+ def parts
23
+ @parts ||= path.split("/").reject{|p| p == ""}
24
+ end
25
+
26
+ # Returns permalink objects matching the parts.
27
+ def objects
28
+ @objects ||= resolve_path
29
+ end
30
+
31
+ # Returns true if all parts could be resolved.
32
+ def found?
33
+ @is_found ||= begin
34
+ !objects.include?(nil)
35
+ end
36
+ end
37
+
38
+ # Returns true if any part does not reflect
39
+ # the current permalink of the underlying linkable.
40
+ def redirect?
41
+ @is_redirect ||= begin
42
+ return unless found?
43
+ redirectables.any?
44
+ end
45
+ end
46
+
47
+ # Returns the path to redirect to, if any.
48
+ def redirect_path
49
+ @redirect_path ||= begin
50
+ return unless redirect?
51
+ "/" << current_parts.join("/")
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # TODO: Allow scopes
58
+ def resolve_path
59
+ results = ::Permalink.any_in(:value => parts)
60
+ links = Array.new(parts.length)
61
+ done = {}
62
+ for result in results
63
+ if i = parts.index(result.value)
64
+ key = "#{result.linkable_class}##{result.linkable_uuid}"
65
+ next if done[key]
66
+ links[i] = result
67
+ done[key] = true
68
+ end
69
+ end
70
+ links
71
+ end
72
+
73
+ # Returns an array containing the current permalinks of all objects.
74
+ def current_parts
75
+ objects.map {|o| o.current.value}
76
+ end
77
+
78
+ def redirectables
79
+ objects.select {|o| !o.current?}
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,71 @@
1
+ module Vidibus
2
+ module Permalink
3
+ module Mongoid
4
+ extend ActiveSupport::Concern
5
+
6
+ class PermalinkConfigurationError < StandardError; end
7
+
8
+ included do
9
+ field :permalink
10
+ before_validation :set_permalink
11
+ validates :permalink, :presence => true
12
+ after_save :store_permalink_object
13
+ after_destroy :destroy_permalink_objects
14
+ end
15
+
16
+ module ClassMethods
17
+
18
+ # Sets permalink attributes.
19
+ # Usage:
20
+ # permalink :some, :fields
21
+ def permalink(*args)
22
+ class_eval <<-EOS
23
+ def permalink_attributes
24
+ #{args.inspect}
25
+ end
26
+ EOS
27
+ end
28
+ end
29
+
30
+ # Returns the current permalink object.
31
+ def permalink_object
32
+ @permalink_object || ::Permalink.for_linkable(self).where(:_current => true).first
33
+ end
34
+
35
+ # Returns all permalink objects ordered by time of update.
36
+ def permalink_objects
37
+ ::Permalink.for_linkable(self).asc(:updated_at)
38
+ end
39
+
40
+ protected
41
+
42
+ # Initializes a new permalink object and sets permalink attribute.
43
+ def set_permalink
44
+ if attributes = try!(:permalink_attributes)
45
+ changed = false
46
+ values = []
47
+ for a in attributes
48
+ changed = send("#{a}_changed?") unless changed == true
49
+ values << send(a)
50
+ end
51
+ return unless permalink.blank? or changed
52
+ value = values.join(" ")
53
+ @permalink_object = ::Permalink.for_linkable(self).for_value(value).first
54
+ @permalink_object ||= ::Permalink.new(:value => value, :linkable => self)
55
+ self.permalink = @permalink_object.value
56
+ else
57
+ raise PermalinkConfigurationError.new("Permalink attributes have not been assigned!")
58
+ end
59
+ end
60
+
61
+ # Stores current new permalink object or updates an existing one that matches.
62
+ def store_permalink_object
63
+ @permalink_object.save! if @permalink_object
64
+ end
65
+
66
+ def destroy_permalink_objects
67
+ ::Permalink.delete_all(:conditions => {:linkable_uuid => uuid})
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,2 @@
1
+ require "permalink/mongoid"
2
+ require "permalink/dispatcher"
@@ -0,0 +1,14 @@
1
+ require "rails"
2
+ require "mongoid"
3
+ require "vidibus-core_extensions"
4
+ require "vidibus-uuid"
5
+ require "vidibus-words"
6
+
7
+ $:.unshift(File.join(File.dirname(__FILE__), "vidibus"))
8
+ require "permalink"
9
+
10
+ if defined?(Rails)
11
+ module Vidibus::Permalink
12
+ class Engine < ::Rails::Engine; end
13
+ end
14
+ end
data/spec/models.rb ADDED
@@ -0,0 +1,20 @@
1
+ require "vidibus-uuid"
2
+
3
+ class Asset
4
+ include Mongoid::Document
5
+ include Vidibus::Uuid::Mongoid
6
+ field :label
7
+ end
8
+
9
+ class Category
10
+ include Mongoid::Document
11
+ include Vidibus::Uuid::Mongoid
12
+ field :label
13
+ end
14
+
15
+ # class Article
16
+ # include Mongoid::Document
17
+ # field :label
18
+ # field :date, :format => Date
19
+ # #permalink :label, :date
20
+ # end