isa-friendly_id_datamapper 3.2.0.beta1

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/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,186 @@
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
+
45
+ before(:destroy) do
46
+ slugs.destroy!
47
+ end
48
+ end
49
+
50
+ base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
51
+ def self.get(*key)
52
+ options = key.extract_options!
53
+
54
+ if key.size == 1
55
+ return super if key.first.is_a?(Array) || key.first.unfriendly_id?
56
+ name, sequence = key.first.to_s.parse_friendly_id
57
+
58
+ if !friendly_id_config.scope? && friendly_id_config.cache_column?
59
+ result = self.first(friendly_id_config.cache_column => key.first)
60
+ end
61
+
62
+ conditions = {
63
+ slugs.name => name,
64
+ slugs.sequence => sequence
65
+ }
66
+ conditions.merge!({
67
+ slugs.scope => (options[:scope].to_param if options[:scope] && options[:scope].respond_to?(:to_param))
68
+ }) if friendly_id_config.scope?
69
+
70
+ result ||= self.first(conditions)
71
+ return super unless result
72
+ result.friendly_id_status.name = name
73
+ result.friendly_id_status.sequence = sequence
74
+ result
75
+ else
76
+ super
77
+ end
78
+ end
79
+
80
+ def self.get!(*key)
81
+ return super unless friendly_id_config.scope?
82
+
83
+ result = get(*key)
84
+ if result
85
+ result
86
+ else
87
+ options = key.extract_options!
88
+ scope = options[:scope]
89
+ message = "Could not find \#{self.name} with key \#{key.inspect}"
90
+ message << " and scope \#{scope.inspect}" if scope
91
+ message << ". Scope expected but none given." unless scope
92
+ raise(::DataMapper::ObjectNotFoundError, message)
93
+ end
94
+ end
95
+ RUBY
96
+ end
97
+
98
+ def slug
99
+ @slug ||= slugs.first
100
+ end
101
+
102
+ def find_slug(name, sequence)
103
+ @slug = slugs.first(:name => name, :sequence => sequence)
104
+ end
105
+
106
+ # Returns the friendly id, or if none is available, the numeric id. Note that this
107
+ # method will use the cached_slug value if present, unlike {#friendly_id}.
108
+ def to_param
109
+ friendly_id_config.cache_column ? to_param_from_cache : to_param_from_slug
110
+ end
111
+
112
+ private
113
+
114
+ def scope_changed?
115
+ friendly_id_config.scope? && send(friendly_id_config.scope).to_param != slug.scope
116
+ end
117
+
118
+ # Respond with the cached value if available.
119
+ def to_param_from_cache
120
+ attribute_get(friendly_id_config.cache_column) || id.to_s
121
+ end
122
+
123
+ # Respond with the slugged value if available.
124
+ def to_param_from_slug
125
+ slug? ? slug.to_friendly_id : id.to_s
126
+ end
127
+
128
+ # Build the new slug using the generated friendly id.
129
+ def build_slug
130
+ return unless new_slug_needed?
131
+ @slug = slugs.new(:name => slug_text, :scope => friendly_id_config.scope_for(self))
132
+ raise FriendlyId::BlankError unless @slug.valid?
133
+ @new_friendly_id = @slug.to_friendly_id
134
+ @slug
135
+ end
136
+
137
+ # Reset the cached friendly_id?
138
+ def new_cache_needed?
139
+ uses_slug_cache? && slug? && send(friendly_id_config.cache_column) != slug.to_friendly_id
140
+ end
141
+
142
+ # Reset the cached friendly_id.
143
+ def set_slug_cache
144
+ if new_cache_needed?
145
+ self.attribute_set(friendly_id_config.cache_column, slug.to_friendly_id)
146
+ self.save_self(false) # save!
147
+ end
148
+ end
149
+
150
+ def skip_friendly_id_validations
151
+ friendly_id.nil? && friendly_id_config.allow_nil?
152
+ end
153
+
154
+ def update_scope
155
+ return unless slug && scope_changed?
156
+ transaction do
157
+ slug.scope = send(friendly_id_config.scope).to_param
158
+ similar = Slug.similar_to(slug)
159
+ if !similar.empty?
160
+ slug.sequence = similar.first.sequence.succ
161
+ end
162
+ slug.save
163
+ end
164
+ end
165
+
166
+ # Update the slugs for any model that is using this model as its
167
+ # FriendlyId scope.
168
+ def update_dependent_scopes
169
+ return unless friendly_id_config.class.scopes_used?
170
+ # slugs.reload.size == 1, slugs.dirty? == true
171
+ if slugs.size > 1 && @new_friendly_id
172
+ friendly_id_config.child_scopes.each do |klass|
173
+ # slugs.first -- ordering not respected when dirty
174
+ Slug.all(:sluggable_type => klass, :scope => slugs.first.to_friendly_id).update(:scope => @new_friendly_id)
175
+ end
176
+ end
177
+ end
178
+
179
+ # Does the model use slug caching?
180
+ def uses_slug_cache?
181
+ friendly_id_config.cache_column?
182
+ end
183
+
184
+ end
185
+ end
186
+ 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 = 2
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