vidibus-permalink 0.0.1

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/.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