friendly_id_datamapper 3.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog.md ADDED
@@ -0,0 +1,8 @@
1
+ # FriendlyId DataMapper Adapter Changelog
2
+
3
+ * Table of Contents
4
+ {:toc}
5
+
6
+ ## 3.1.0.beta1 (2010-09-13)
7
+
8
+ * Initial beta release.
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Norman Clarke, Alex Coles
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # FriendlyId DataMapper Adapter
2
+
3
+ This is an pre-release (beta) adapter for
4
+ [FriendlyId](http://norman.github.com/friendly_id) using DataMapper.
5
+
6
+ ## FriendlyId Features
7
+
8
+ It currently supports all of FriendlyId's features except:
9
+
10
+ * Rails Generator
11
+ * Support for multiple finders
12
+
13
+ Currently, only finds using `get` is supported.
14
+
15
+ @post = Post.get("this-is-a-title")
16
+ @post.friendly_id # this-is-a-title
17
+
18
+ ## Compatibility
19
+
20
+ The FriendlyId DataMapper Adapter keeps in lock-step with major and
21
+ minor versions of the FriendlyId gem, i.e.
22
+ `friendly_id_datamapper 3.1.x` is compatible with `friendly_id 3.1.x series`.
23
+ Patch and build versions are not kept in lock-step.
24
+
25
+ ## Usage
26
+
27
+ gem install friendly_id friendly_id_datamapper
28
+
29
+ require "friendly_id"
30
+ require "friendly_id/datamapper"
31
+
32
+ class Post
33
+ include DataMapper::Resource
34
+
35
+ property :id, Serial
36
+ property :title, String
37
+
38
+ has_friendly_id :title, :use_slug => true
39
+ end
40
+
41
+
42
+ For more information on the available features, please see the
43
+ [FriendlyId Guide](http://norman.github.com/friendly_id/file.Guide.html).
44
+
45
+ ## Bugs
46
+
47
+ Please report them on the [Github issue tracker](http://github.com/myabc/friendly_id_datamapper/issues)
48
+ for this project.
49
+
50
+ If you have a bug to report, please include the following information:
51
+
52
+ * **Version information for FriendlyId, friendly_id_datamapper, Rails and Ruby.**
53
+ * Stack trace and error message.
54
+ * Any snippets of relevant model, view or controller code that shows how your
55
+ are using FriendlyId.
56
+
57
+ If you are able to, it helps even more if you can fork FriendlyId on Github,
58
+ and add a test that reproduces the error you are experiencing.
59
+
60
+ ## Credits
61
+
62
+ Copyright (c) 2010, released under the MIT license.
63
+
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ require "rake"
2
+ require "rake/testtask"
3
+ require "rake/gempackagetask"
4
+ require "rake/clean"
5
+
6
+ CLEAN << "pkg" << "doc" << "coverage" << ".yardoc"
7
+
8
+ Rake::GemPackageTask.new(eval(File.read("friendly_id_datamapper.gemspec"))) { |pkg| }
9
+ Rake::TestTask.new(:test) { |t| t.pattern = "test/*_test.rb" }
10
+
11
+ task :default => :test
12
+
13
+ begin
14
+ require "yard"
15
+ YARD::Rake::YardocTask.new do |t|
16
+ t.options = ["--output-dir=docs"]
17
+ end
18
+ rescue LoadError
19
+ end
20
+
21
+ begin
22
+ require "rcov/rcovtask"
23
+ Rcov::RcovTask.new do |r|
24
+ r.test_files = FileList["test/**/*_test.rb"]
25
+ r.verbose = true
26
+ r.rcov_opts << "--exclude gems/*"
27
+ end
28
+ rescue LoadError
29
+ end
@@ -0,0 +1,67 @@
1
+ module FriendlyId
2
+ module DataMapperAdapter
3
+
4
+ # Extends FriendlyId::Configuration with some implementation details and
5
+ # features specific to DataMapper.
6
+ class Configuration < FriendlyId::Configuration
7
+
8
+ # The column used to cache the friendly_id string. If no column is specified,
9
+ # FriendlyId will look for a column named +cached_slug+ and use it automatically
10
+ # if it exists. If for some reason you have a column named +cached_slug+
11
+ # but don't want FriendlyId to modify it, pass the option
12
+ # +:cache_column => false+ to {FriendlyId::DataMapperAdapter#has_friendly_id has_friendly_id}.
13
+ attr_accessor :cache_column
14
+
15
+ # An array of classes for which the configured class serves as a
16
+ # FriendlyId scope.
17
+ attr_reader :child_scopes
18
+
19
+ attr_reader :custom_cache_column
20
+
21
+ def cache_column
22
+ return @cache_column if defined?(@cache_column)
23
+ @cache_column = autodiscover_cache_column
24
+ end
25
+
26
+ def cache_column?
27
+ !! cache_column
28
+ end
29
+
30
+ def cache_column=(cache_column)
31
+ @cache_column = cache_column
32
+ @custom_cache_column = cache_column
33
+ end
34
+
35
+ def child_scopes
36
+ @child_scopes ||= associated_friendly_classes.select do |klass|
37
+ klass.friendly_id_config.scopes_over?(configured_class)
38
+ end
39
+ end
40
+
41
+ def custom_cache_column?
42
+ !! custom_cache_column
43
+ end
44
+
45
+ def scope_for(record)
46
+ scope? ? record.send(scope).to_param : nil
47
+ end
48
+
49
+ def scopes_over?(klass)
50
+ scope? && scope == klass.to_s.underscore.to_sym
51
+ end
52
+
53
+ private
54
+
55
+ def autodiscover_cache_column
56
+ :cached_slug if configured_class.properties[:cached_slug]
57
+ end
58
+
59
+ def associated_friendly_classes
60
+ configured_class.relationships.values.select { |relationship|
61
+ relationship.child_model.respond_to?(:friendly_id_config)
62
+ }.map(&:child_model)
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,83 @@
1
+
2
+
3
+ module FriendlyId
4
+ module DataMapperAdapter
5
+
6
+ module SimpleModel
7
+
8
+ def self.included(base)
9
+ base.class_eval do
10
+ column = friendly_id_config.column
11
+ validates_presence_of column, :unless => :skip_friendly_id_validations
12
+ validates_length_of column, :maximum => friendly_id_config.max_length, :unless => :skip_friendly_id_validations
13
+ validates_with_method column, :method => :validate_friendly_id, :unless => :skip_friendly_id_validations
14
+
15
+ before :update do
16
+ @old_friendly_id = original_attributes[properties[friendly_id_config.column]]
17
+ end
18
+
19
+ after :update, :update_scopes
20
+ end
21
+
22
+ def base.get(*key)
23
+ if key.size == 1
24
+ return super if key.first.unfriendly_id?
25
+ column = self.friendly_id_config.column
26
+ repository = self.repository
27
+ key = self.key(repository.name).typecast(key)
28
+ result = self.first(column.to_sym => key)
29
+ return super unless result
30
+ result.friendly_id_status.name = name
31
+ result
32
+ else
33
+ super
34
+ end
35
+ end
36
+ end
37
+
38
+ # Get the {FriendlyId::Status} after the find has been performed.
39
+ def friendly_id_status
40
+ @friendly_id_status ||= Status.new(:record => self)
41
+ end
42
+
43
+ # Returns the friendly_id.
44
+ def friendly_id
45
+ send friendly_id_config.column
46
+ end
47
+
48
+ # Returns the friendly id, or if none is available, the numeric id.
49
+ def to_param
50
+ (friendly_id || id).to_s
51
+ end
52
+
53
+ private
54
+
55
+ # Update the slugs for any model that is using this model as its
56
+ # FriendlyId scope.
57
+ def update_scopes
58
+ if @old_friendly_id != friendly_id
59
+ friendly_id_config.child_scopes.each do |klass|
60
+ Slug.all(:sluggable_type => klass, :scope => @old_friendly_id).update(:scope => friendly_id)
61
+ end
62
+ end
63
+ end
64
+
65
+ def friendly_id_config
66
+ self.class.friendly_id_config
67
+ end
68
+
69
+ def skip_friendly_id_validations
70
+ friendly_id.nil? && friendly_id_config.allow_nil?
71
+ end
72
+
73
+ def validate_friendly_id
74
+ if result = friendly_id_config.reserved_error_message(friendly_id)
75
+ return [false, result.join(' ')]
76
+ else
77
+ return true
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,72 @@
1
+ # A Slug is a unique, human-friendly identifier for a DataMapper model
2
+ class Slug
3
+ include ::DataMapper::Resource
4
+
5
+ property :id, Serial
6
+ property :name, String, :index => :index_slugs_on_n_s_s_and_s, :required => true, :length => 1..255
7
+ property :sluggable_id, Integer, :index => :sluggable_id
8
+ property :sequence, Integer, :index => :index_slugs_on_n_s_s_and_s, :required => true, :default => 1
9
+ property :sluggable_type, Class, :index => :index_slugs_on_n_s_s_and_s
10
+ property :scope, String, :index => :index_slugs_on_n_s_s_and_s
11
+ property :created_at, DateTime
12
+
13
+ before :save do
14
+ self.sequence = next_sequence
15
+ self.created_at = DateTime.now
16
+ end
17
+
18
+ def self.similar_to(slug)
19
+ all({
20
+ :name => slug.name,
21
+ :scope => slug.scope,
22
+ :sluggable_type => slug.sluggable_type,
23
+ :order => [:sequence.asc]
24
+ })
25
+ end
26
+
27
+ # Whether this slug is the most recent of its owner's slugs.
28
+ def current?
29
+ sluggable.slug == self
30
+ end
31
+
32
+ def outdated?
33
+ !current?
34
+ end
35
+
36
+ def to_friendly_id
37
+ sequence > 1 ? friendly_id_with_sequence : name
38
+ end
39
+
40
+ def sluggable
41
+ sluggable_type.get(sluggable_id)
42
+ end
43
+
44
+ def sluggable=(instance)
45
+ attribute_set(:sluggable_type, instance.class)
46
+ attribute_set(:sluggable_id, instance.id)
47
+ end
48
+
49
+ private
50
+
51
+ def enable_name_reversion
52
+ conditions = { :sluggable_id => sluggable_id, :sluggable_type => sluggable_type,
53
+ :name => name, :scope => scope }
54
+ self.class.all(conditions).destroy
55
+ end
56
+
57
+ def friendly_id_with_sequence
58
+ "#{name}#{separator}#{sequence}"
59
+ end
60
+
61
+ def next_sequence
62
+ enable_name_reversion
63
+ conditions = { :name => name, :scope => scope, :sluggable_type => sluggable_type }
64
+ prev = self.class.first(conditions.update(:order => :sequence.desc))
65
+ prev ? prev.sequence.succ : 1
66
+ end
67
+
68
+ def separator
69
+ sluggable_type.friendly_id_config.sequence_separator
70
+ end
71
+
72
+ end
@@ -0,0 +1,191 @@
1
+ require 'friendly_id/slugged'
2
+ require 'friendly_id/status'
3
+
4
+ module FriendlyId
5
+ module DataMapperAdapter
6
+
7
+ module SluggedModel
8
+
9
+ include FriendlyId::Slugged::Model
10
+
11
+ def self.included(base)
12
+ base.class_eval do
13
+ has n, :slugs,
14
+ :model => ::Slug,
15
+ :child_key => [:sluggable_id],
16
+ :conditions => { :sluggable_type => base },
17
+ :order => [:id.desc]
18
+
19
+ before :save do
20
+ begin
21
+ build_slug if new_slug_needed?
22
+ method = friendly_id_config.method
23
+ rescue FriendlyId::BlankError
24
+ @errors ||= ValidationErrors.new
25
+ @errors[method] = "can't be blank"
26
+ throw :halt, false
27
+ rescue FriendlyId::ReservedError
28
+ @errors ||= ValidationErrors.new
29
+ @errors[method] = "is reserved"
30
+ throw :halt, false
31
+ end
32
+ end
33
+
34
+ after(:save) do
35
+ throw :halt, false if friendly_id_config.allow_nil? && !slug
36
+
37
+ slug.sluggable_id = id
38
+ slug.save
39
+ set_slug_cache
40
+ end
41
+
42
+ after :update, :update_scope
43
+ after :update, :update_dependent_scopes
44
+ end
45
+
46
+ def base.extract_options!(args)
47
+ options = args.last
48
+ if options.respond_to?(:to_hash)
49
+ args.pop
50
+ options.to_hash.dup
51
+ else
52
+ {}
53
+ end
54
+ end
55
+
56
+ def base.get(*key)
57
+ options = extract_options!(key)
58
+
59
+ if key.size == 1
60
+ return super if key.first.unfriendly_id?
61
+ name, sequence = key.first.to_s.parse_friendly_id
62
+
63
+ if !friendly_id_config.scope? && friendly_id_config.cache_column?
64
+ result = self.first(friendly_id_config.cache_column => key.first)
65
+ end
66
+
67
+ conditions = {
68
+ slugs.name => name,
69
+ slugs.sequence => sequence
70
+ }
71
+ conditions.merge!({
72
+ slugs.scope => (options[:scope].to_param if options[:scope] && options[:scope].respond_to?(:to_param))
73
+ }) if friendly_id_config.scope?
74
+
75
+ result ||= self.first(conditions)
76
+ return super unless result
77
+ result.friendly_id_status.name = name
78
+ result.friendly_id_status.sequence = sequence
79
+ result
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def base.get!(*key)
86
+ return super unless friendly_id_config.scope?
87
+
88
+ result = get(*key)
89
+ if result
90
+ result
91
+ else
92
+ options = extract_options!(key)
93
+ scope = options[:scope]
94
+ message = "Could not find #{self.name} with key #{key.inspect}"
95
+ message << " and scope #{scope.inspect}" if scope
96
+ message << ". Scope expected but none given." unless scope
97
+ raise(::DataMapper::ObjectNotFoundError, message)
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+ def slug
104
+ @slug ||= slugs.first
105
+ end
106
+
107
+ def find_slug(name, sequence)
108
+ @slug = slugs.first(:name => name, :sequence => sequence)
109
+ end
110
+
111
+ # Returns the friendly id, or if none is available, the numeric id. Note that this
112
+ # method will use the cached_slug value if present, unlike {#friendly_id}.
113
+ def to_param
114
+ friendly_id_config.cache_column ? to_param_from_cache : to_param_from_slug
115
+ end
116
+
117
+ private
118
+
119
+ def scope_changed?
120
+ friendly_id_config.scope? && send(friendly_id_config.scope).to_param != slug.scope
121
+ end
122
+
123
+ # Respond with the cached value if available.
124
+ def to_param_from_cache
125
+ attribute_get(friendly_id_config.cache_column) || id.to_s
126
+ end
127
+
128
+ # Respond with the slugged value if available.
129
+ def to_param_from_slug
130
+ slug? ? slug.to_friendly_id : id.to_s
131
+ end
132
+
133
+ # Build the new slug using the generated friendly id.
134
+ def build_slug
135
+ return unless new_slug_needed?
136
+ @slug = slugs.new(:name => slug_text, :scope => friendly_id_config.scope_for(self))
137
+ raise FriendlyId::BlankError unless @slug.valid?
138
+ @new_friendly_id = @slug.to_friendly_id
139
+ @slug
140
+ end
141
+
142
+ # Reset the cached friendly_id?
143
+ def new_cache_needed?
144
+ uses_slug_cache? && slug? && send(friendly_id_config.cache_column) != slug.to_friendly_id
145
+ end
146
+
147
+ # Reset the cached friendly_id.
148
+ def set_slug_cache
149
+ if new_cache_needed?
150
+ self.attribute_set(friendly_id_config.cache_column, slug.to_friendly_id)
151
+ self.save_self(false) # save!
152
+ end
153
+ end
154
+
155
+ def skip_friendly_id_validations
156
+ friendly_id.nil? && friendly_id_config.allow_nil?
157
+ end
158
+
159
+ def update_scope
160
+ return unless slug && scope_changed?
161
+ transaction do
162
+ slug.scope = send(friendly_id_config.scope).to_param
163
+ similar = Slug.similar_to(slug)
164
+ if !similar.empty?
165
+ slug.sequence = similar.first.sequence.succ
166
+ end
167
+ slug.save
168
+ end
169
+ end
170
+
171
+ # Update the slugs for any model that is using this model as its
172
+ # FriendlyId scope.
173
+ def update_dependent_scopes
174
+ return unless friendly_id_config.class.scopes_used?
175
+ # slugs.reload.size == 1, slugs.dirty? == true
176
+ if slugs.size > 1 && @new_friendly_id
177
+ friendly_id_config.child_scopes.each do |klass|
178
+ # slugs.first -- ordering not respected when dirty
179
+ Slug.all(:sluggable_type => klass, :scope => slugs.first.to_friendly_id).update(:scope => @new_friendly_id)
180
+ end
181
+ end
182
+ end
183
+
184
+ # Does the model use slug caching?
185
+ def uses_slug_cache?
186
+ friendly_id_config.cache_column?
187
+ end
188
+
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,69 @@
1
+ require 'active_support/core_ext'
2
+
3
+ module FriendlyId
4
+ class TaskRunner
5
+
6
+ extend Forwardable
7
+
8
+ attr_accessor :days
9
+ attr_accessor :klass
10
+ attr_accessor :task_options
11
+
12
+ def_delegators :klass, :find, :friendly_id_config
13
+
14
+ OLD_SLUG_DAYS = 45
15
+
16
+ def initialize(&block)
17
+ self.klass = ENV["MODEL"]
18
+ self.days = ENV["DAYS"]
19
+ end
20
+
21
+ def days=(days)
22
+ @days ||= days.blank? ? OLD_SLUG_DAYS : days.to_i
23
+ end
24
+
25
+ def klass=(klass)
26
+ @klass ||= klass.to_s.classify.constantize unless klass.blank?
27
+ end
28
+
29
+ def make_slugs
30
+ validate_uses_slugs
31
+ options = {:limit => 100, :slugs => nil, :order => [:id.asc]}.merge(task_options || {})
32
+ while records = klass.all(options) do
33
+ break if records.size == 0
34
+ records.each do |record|
35
+ record.send(:build_slug) # FIXME: DataMapper Hack: this hook is not getting called
36
+ record.save
37
+ yield(record) if block_given?
38
+ end
39
+ options.merge!(:id.gt => records.last.id)
40
+ end
41
+ end
42
+
43
+ def delete_slugs
44
+ validate_uses_slugs
45
+ Slug.all(:sluggable_type => klass).destroy!
46
+ if column = friendly_id_config.cache_column
47
+ klass.all.update!(column => nil)
48
+ end
49
+ end
50
+
51
+ def delete_old_slugs
52
+ conditions = ["created_at < ?", DateTime.now - days]
53
+ if klass
54
+ conditions[0] << " AND sluggable_type = ?"
55
+ conditions << klass.to_s
56
+ end
57
+ Slug.all(:conditions => conditions).select(&:outdated?).map(&:destroy)
58
+ end
59
+
60
+ def validate_uses_slugs
61
+ (raise "You need to pass a MODEL=<model name> argument to rake") if klass.blank?
62
+ unless friendly_id_config.use_slug?
63
+ raise "Class '%s' doesn't use slugs" % klass.to_s
64
+ end
65
+ rescue NoMethodError
66
+ raise "Class '%s' doesn't use FriendlyId" % klass.to_s
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,11 @@
1
+ module FriendlyId
2
+ module DataMapperAdapter
3
+ module Version
4
+ MAJOR = 3
5
+ MINOR = 1
6
+ TINY = 0
7
+ BUILD = 'beta1'
8
+ STRING = [MAJOR, MINOR, TINY, BUILD].compact.join('.')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ require 'dm-core'
2
+ require 'dm-transactions'
3
+ require 'dm-validations'
4
+ require 'friendly_id/datamapper_adapter/configuration'
5
+ require 'friendly_id/datamapper_adapter/slug'
6
+ require 'friendly_id/datamapper_adapter/simple_model'
7
+ require 'friendly_id/datamapper_adapter/slugged_model'
8
+ # require 'friendly_id/datamapper_adapter/tasks'
9
+ require 'forwardable'
10
+
11
+ module FriendlyId
12
+ module DataMapperAdapter
13
+
14
+ include FriendlyId::Base
15
+
16
+ def has_friendly_id(method, options = {})
17
+ extend FriendlyId::DataMapperAdapter::ClassMethods
18
+ @friendly_id_config = Configuration.new(self, method, options)
19
+
20
+ if friendly_id_config.use_slug?
21
+ include ::FriendlyId::DataMapperAdapter::SluggedModel
22
+ else
23
+ include ::FriendlyId::DataMapperAdapter::SimpleModel
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ attr_accessor :friendly_id_config
29
+ end
30
+
31
+ end
32
+ end
33
+
34
+ DataMapper::Model.append_extensions FriendlyId::DataMapperAdapter
@@ -0,0 +1,16 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ module FriendlyId
4
+ module Test
5
+ module DataMapperAdapter
6
+
7
+ # Tests for DataMapper models using FriendlyId with slugs.
8
+ class BasicSluggedModelTest < ::Test::Unit::TestCase
9
+ include FriendlyId::Test::Generic
10
+ include FriendlyId::Test::Slugged
11
+ include FriendlyId::Test::DataMapperAdapter::Slugged
12
+ include FriendlyId::Test::DataMapperAdapter::Core
13
+ end
14
+ end
15
+ end
16
+ end