From ce0596393e918fb9e54113f5f3c1cdc89150fd74 Mon Sep 17 00:00:00 2001 From: Norman Clarke Date: Fri, 8 Jan 2010 17:23:17 -0300 Subject: [PATCH] Began refactoring into adapters. Removed more code smells detected by reek. --- History.txt | 7 + README.rdoc | 23 +- Rakefile | 2 +- config.reek | 75 +++++ lib/friendly_id.rb | 24 +- .../adapters/active_record/simple_model.rb | 133 ++++++++ .../{ => adapters/active_record}/slug.rb | 0 .../adapters/active_record/slugged_model.rb | 315 ++++++++++++++++++ lib/friendly_id/config.rb | 8 + lib/friendly_id/finder.rb | 114 +++++++ lib/friendly_id/helpers.rb | 12 - lib/friendly_id/simple/class_methods.rb | 38 --- lib/friendly_id/simple/instance_methods.rb | 49 --- lib/friendly_id/slugged/class_methods.rb | 126 ------- lib/friendly_id/slugged/finder.rb | 52 --- lib/friendly_id/slugged/instance_methods.rb | 139 -------- lib/friendly_id/status.rb | 36 ++ lib/friendly_id/version.rb | 4 +- test/friendly_id_test.rb | 2 +- test/non_slugged_test.rb | 2 +- test/scoped_model_test.rb | 4 +- 21 files changed, 720 insertions(+), 445 deletions(-) create mode 100644 config.reek create mode 100644 lib/friendly_id/adapters/active_record/simple_model.rb rename lib/friendly_id/{ => adapters/active_record}/slug.rb (100%) create mode 100644 lib/friendly_id/adapters/active_record/slugged_model.rb create mode 100644 lib/friendly_id/finder.rb delete mode 100644 lib/friendly_id/helpers.rb delete mode 100644 lib/friendly_id/simple/class_methods.rb delete mode 100644 lib/friendly_id/simple/instance_methods.rb delete mode 100644 lib/friendly_id/slugged/class_methods.rb delete mode 100644 lib/friendly_id/slugged/finder.rb delete mode 100644 lib/friendly_id/slugged/instance_methods.rb create mode 100644 lib/friendly_id/status.rb diff --git a/History.txt b/History.txt index 5da405d6d..1b665c0f7 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,10 @@ +== 2.3.0 NOT RELEASED + +* 1 major enhancement + * Major refactorings, cleanups and deprecations en route to the 3.0 release. +* 1 minor enhancement + * New option to pass arguments to approximate_ascii, allowing custom approximations specific to German or Spanish. + == 2.2.7 2009-12-16 * 1 minor fix diff --git a/README.rdoc b/README.rdoc index 27b865213..a30185ca6 100644 --- a/README.rdoc +++ b/README.rdoc @@ -53,13 +53,13 @@ models by numeric id, you can detect whether they were found by the friendly_id: @post = Post.find(params[:id]) - raise "some error" if !@post.found_using_friendly_id? + raise "some error" if @post.friendly_id_status.unfriendly? or, you can 301 redirect if the model was found by the numeric id if you don't care about numeric access, but want the SEO value of the friendly_id: @post = Post.find(params[:id]) - redirect_to @post, :status => 301 if @post.has_better_id? + redirect_to @post, :status => 301 unless @post.friendly_id_status.best? The "has_better_id?" method returns true if the model was found with the numeric id, or with an outdated slug. @@ -79,7 +79,7 @@ to do a 301 redirect to your updated URL. ... def ensure_current_post_url - redirect_to @post, :status => :moved_permanently if @post.has_better_id? + redirect_to @post, :status => :moved_permanently unless @post.friendly_id_status.best? end end @@ -215,7 +215,7 @@ FriendlyId's slugging can strip diacritics from Western European characters, so that you can have ASCII-only URL's; for example, conveting "ñøîéçü" to "noiecu." - has_friendly_id :title, :use_slug => true, :strip_diacritics => true + has_friendly_id :title, :use_slug => true, :approximate_ascii => true If you are not using slugs, you'll have to do this manually for whatever value you're using as the friendly_id. @@ -249,19 +249,26 @@ that uses a non-Roman writing system, your feedback would be most welcome. === Custom Slug Generation While FriendlyId's slug generation options work for most people, you may need -something else. As of version 2.0.4 you can pass in your own custom slug -generation block: +something else. As of version 2.3.0 you can simply override the ++normalize_friendly_id+ method in your model class: class Post < ActiveRecord::Base - has_friendly_id :title, :use_slug => true do |text| + has_friendly_id :title, :use_slug => true + + def normalize_friendly_id(text) MySlugGeneratorClass::my_slug_method(text) end + end FriendlyId will still respect your settings for max length and reserved words, -but will use your block rather than the baked-in methods to normalize the +but will use your method rather than the baked-in methods to normalize the friendly_id text. +Note that previous versions of friendly_id allowed this same functionality by +passing a block to +has_friendly_id+, but this is deprecated as of version +2.3.0. + == Getting it FriendlyId is best installed as a Ruby Gem: diff --git a/Rakefile b/Rakefile index b43b9448c..3747f7595 100644 --- a/Rakefile +++ b/Rakefile @@ -19,7 +19,7 @@ end begin require "yard" YARD::Rake::YardocTask.new do |t| - t.options = ["--output-dir=docs"] + t.options = ["--output-dir=docs", "--private"] end rescue LoadError end diff --git a/config.reek b/config.reek new file mode 100644 index 000000000..4b926544e --- /dev/null +++ b/config.reek @@ -0,0 +1,75 @@ +--- +LargeClass: + max_methods: 25 + exclude: [] + + enabled: true + max_instance_variables: 9 +LongParameterList: + max_params: 3 + exclude: [] + + enabled: true + overrides: + initialize: + max_params: 5 +FeatureEnvy: + exclude: &id001 [!ruby/regexp /=$/] + + enabled: true +ClassVariable: + exclude: *id001 + enabled: true +UncommunicativeName: + accept: + - Inline::C + exclude: [] + + enabled: true + reject: + - !ruby/regexp /^.$/ + - !ruby/regexp /[0-9]$/ +NestedIterators: + exclude: *id001 + enabled: true +LongMethod: + max_statements: 5 + exclude: + - initialize + enabled: true +Duplication: + exclude: [] + + enabled: true + max_calls: 2 +UtilityFunction: + max_helper_calls: 1 + exclude: [] + + enabled: true +Attribute: + exclude: [] + + enabled: false +SimulatedPolymorphism: + exclude: [] + + enabled: true + max_ifs: 2 +DataClump: + exclude: [] + + enabled: true + max_copies: 2 + min_clump_size: 2 +ControlCouple: + exclude: *id001 + enabled: true +LongYieldList: + max_params: 3 + exclude: [] + + enabled: true + overrides: + initialize: + max_params: 5 diff --git a/lib/friendly_id.rb b/lib/friendly_id.rb index d3b77f480..ab4d4e32b 100644 --- a/lib/friendly_id.rb +++ b/lib/friendly_id.rb @@ -1,13 +1,7 @@ -require File.join(File.dirname(__FILE__), "friendly_id", "helpers") -require File.join(File.dirname(__FILE__), "friendly_id", "slug") require File.join(File.dirname(__FILE__), "friendly_id", "slug_string") require File.join(File.dirname(__FILE__), "friendly_id", "config") -require File.join(File.dirname(__FILE__), "friendly_id", "slugged", "finder") -require File.join(File.dirname(__FILE__), "friendly_id", "slugged", "class_methods") -require File.join(File.dirname(__FILE__), "friendly_id", "slugged", "instance_methods") -require File.join(File.dirname(__FILE__), "friendly_id", "simple", "class_methods") -require File.join(File.dirname(__FILE__), "friendly_id", "simple", "instance_methods") - +require File.join(File.dirname(__FILE__), "friendly_id", "finder") +require File.join(File.dirname(__FILE__), "friendly_id", "status") # FriendlyId is a comprehensive Ruby library for slugging and permalinks with # ActiveRecord. # @author Norman Clarke @@ -57,16 +51,18 @@ def self.parse_friendly_id(string) return name, sequence end - private + # Load either the SluggedModel or SimpleModel modules. + # @TODO figure out best way to selectively load adapted modules def load_adapters + require File.join(File.dirname(__FILE__), "friendly_id", "adapters", "active_record", "simple_model") + require File.join(File.dirname(__FILE__), "friendly_id", "adapters", "active_record", "slugged_model") + require File.join(File.dirname(__FILE__), "friendly_id", "adapters", "active_record", "slug") if friendly_id_config.use_slug? - extend Slugged::ClassMethods - include Slugged::InstanceMethods + include Adapters::ActiveRecord::SluggedModel else - extend Simple::ClassMethods - include Simple::InstanceMethods + include Adapters::ActiveRecord::SimpleModel end end @@ -76,4 +72,4 @@ def load_adapters # ActiveRecord::Base. class ActiveRecord::Base extend FriendlyId -end +end \ No newline at end of file diff --git a/lib/friendly_id/adapters/active_record/simple_model.rb b/lib/friendly_id/adapters/active_record/simple_model.rb new file mode 100644 index 000000000..4069b630d --- /dev/null +++ b/lib/friendly_id/adapters/active_record/simple_model.rb @@ -0,0 +1,133 @@ +module FriendlyId + module Adapters + module ActiveRecord + + module SimpleModel + + class Finder < FriendlyId::Finder + + def column + "#{table_name}.#{friendly_id_config.method}" + end + + def primary_key + "#{quoted_table_name}.#{model.send :primary_key}" + end + + end + + class MultipleFinder < Finder + + attr_reader :friendly_ids, :results, :unfriendly_ids + + def initialize(ids, model, options={}) + @friendly_ids, @unfriendly_ids = ids.partition {|id| self.class.friendly?(id) && id.to_s } + super + end + + def error_message + "Couldn't find all %s with IDs (%s) AND %s (found %d results, but was looking for %d)" % [ + model.name.pluralize, + ids * ', ', + sanitize_sql(options[:conditions]), + results.size, + expected_size + ] + end + + def find + @results = with_scope(:find => options) { all(:conditions => conditions) } + raise(::ActiveRecord::RecordNotFound, error_message) if @results.size != expected_size + friendly_results.each { |result| result.friendly_id_status.name = result.friendly_id } + @results + end + + def friendly_results + results.select { |result| friendly_ids.include? result.friendly_id.to_s } + end + + def conditions + ["#{primary_key} IN (?) OR #{column} IN (?)", unfriendly_ids, friendly_ids] + end + + end + + class SingleFinder < Finder + + def find + result = with_scope(:find => find_options) { find_initial options } + raise ::ActiveRecord::RecordNotFound.new if !result + result.friendly_id_status.name = id + result + end + + def find_options + {:conditions => {column => id}} + end + + end + + class Status < FriendlyId::Status + # Did the find operation use a friendly id? + def friendly? + !! name + end + alias :best? :friendly? + end + + module Finders + def find_one(id, options) + finder = SingleFinder.new(id, self, options) + finder.unfriendly? ? super : finder.find + end + + def find_some(ids_and_names, options) + MultipleFinder.new(ids_and_names, self, options).find + end + protected :find_one, :find_some + end + + def self.included(base) + base.validate :validate_friendly_id + base.extend Finders + end + + def friendly_id_status + @friendly_id_status ||= Status.new(:model => self) + end + + # Was the record found using one of its friendly ids? + def found_using_friendly_id? + friendly_id_status.friendly? + end + + # Was the record found using its numeric id? + def found_using_numeric_id? + friendly_id_status.numeric? + end + alias has_better_id? found_using_numeric_id? + + # Returns the friendly_id. + def friendly_id + send friendly_id_config.method + end + alias best_id friendly_id + + # Returns the friendly id, or if none is available, the numeric id. + def to_param + (friendly_id || id).to_s + end + + private + + def validate_friendly_id + if result = friendly_id_config.reserved_error_message(friendly_id) + self.errors.add(*result) + return false + end + end + + end + end + end +end \ No newline at end of file diff --git a/lib/friendly_id/slug.rb b/lib/friendly_id/adapters/active_record/slug.rb similarity index 100% rename from lib/friendly_id/slug.rb rename to lib/friendly_id/adapters/active_record/slug.rb diff --git a/lib/friendly_id/adapters/active_record/slugged_model.rb b/lib/friendly_id/adapters/active_record/slugged_model.rb new file mode 100644 index 000000000..b9186589e --- /dev/null +++ b/lib/friendly_id/adapters/active_record/slugged_model.rb @@ -0,0 +1,315 @@ +module FriendlyId + module Adapters + module ActiveRecord + module SluggedModel + + class Status < ::FriendlyId::Status + + attr_accessor :slug + + # The slug that was used to find the model. + def slug + @slug ||= model.slugs.find_by_name_and_sequence(*Finder.parse(name)) + end + + # Did the find operation use a friendly id? + def friendly? + !! (name or slug) + end + + # Did the find operation use the current slug? + def current? + !! slug && slug.is_most_recent? + end + + # Did the find operation use an outdated slug? + def outdated? + !current? + end + + # Did the find operation use the best possible id? True if +id+ is + # numeric, but the model has no slug, or +id+ is friendly and current + def best? + current? || (numeric? && !model.slug) + end + + end + + class MultipleFinder < FriendlyId::Finder + def all_friendly? + [friendly?].flatten.compact.uniq == [true] + end + + def all_unfriendly? + [unfriendly?].flatten.compact.uniq == [true] + end + + def friendly? + ids.map {|id| self.class.friendly? id} + end + + def unfriendly? + ids.map {|id| self.class.unfriendly? id} + end + end + + class SingleFinder < FriendlyId::Finder + + def find + result = model.send(:with_scope, {:find => find_options}) { model.send(:find_initial, options) } + raise ::ActiveRecord::RecordNotFound.new if friendly? and !result + result.friendly_id_status.name = name if result + result + rescue ::ActiveRecord::RecordNotFound => @error + friendly_id_config.scope? ? raise_scoped_error : (raise @error) + end + + def find_options + slug_table = Slug.table_name + { + :select => "#{model.table_name}.*", + :joins => slugs_included? ? options[:joins] : :slugs, + :conditions => { + "#{slug_table}.name" => name, + "#{slug_table}.scope" => scope, + "#{slug_table}.sequence" => sequence + } + } + end + + def raise_scoped_error + scope_message = options[:scope] || "expected, but none given" + message = "%s, scope: %s" % [@error.message, scope_message] + raise ::ActiveRecord::RecordNotFound, message + end + + end + + module Finders + + # Finds a single record using the friendly id, or the record's id. + def find_one(id_or_name, options) #:nodoc:# + finder = SingleFinder.new(id_or_name, self, options) + finder.unfriendly? ? super : finder.find or super + end + + # Finds multiple records using the friendly ids, or the records' ids. + def find_some(ids_and_names, options) #:nodoc:# + + finder = MultipleFinder.new(ids_and_names, options) + slugs, ids = get_slugs_and_ids(ids_and_names, options) + results = [] + + find_options = {:select => "#{self.table_name}.*"} + find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs) + find_options[:conditions] = "#{quoted_table_name}.#{primary_key} IN (#{ids.empty? ? 'NULL' : ids.join(',')}) " + find_options[:conditions] << "OR slugs.id IN (#{slugs.to_s(:db)})" + + results = with_scope(:find => find_options) { find_every(options) }.uniq + + if results.size != expected = finder.expected_size + raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{ name.pluralize } with IDs (#{ ids_and_names * ', ' }) AND #{ sanitize_sql options[:conditions] } (found #{ results.size } results, but was looking for #{ expected })" + end + + assign_finders(slugs, results) + + results + end + + def validate_find_options(options) #:nodoc:# + options.assert_valid_keys([:conditions, :include, :joins, :limit, :offset, + :order, :select, :readonly, :group, :from, :lock, :having, :scope]) + end + + def cache_column + if defined?(@cache_column) + return @cache_column + elsif friendly_id_config.cache_column + @cache_column = friendly_id_config.cache_column + elsif columns.any? { |c| c.name == 'cached_slug' } + @cache_column = :cached_slug + else + @cache_column = nil + end + end + + private + + # Assign finder slugs for the results found in find_some_with_friendly + def assign_finders(slugs, results) #:nodoc:# + slugs.each do |slug| + results.select { |r| r.id == slug.sluggable_id }.each do |result| + result.friendly_id_status.slug = slug + end + end + end + + # Build arrays of slugs and ids, for the find_some_with_friendly method. + def get_slugs_and_ids(ids_and_names, options) #:nodoc:# + scope = options.delete(:scope) + slugs = [] + ids = [] + ids_and_names.each do |id_or_name| + name, sequence = FriendlyId.parse_friendly_id id_or_name.to_s + slug = Slug.find(:first, :conditions => { + :name => name, + :scope => scope, + :sequence => sequence, + :sluggable_type => base_class.name + }) + # If the slug was found, add it to the array for later use. If not, and + # the id_or_name is a number, assume that it is a regular record id. + slug ? slugs << slug : (ids << id_or_name if id_or_name.to_s =~ /\A\d*\z/) + end + return slugs, ids + end + + end + + module DeprecatedMethods + # @deprecated Please use #friendly_id_status.slug. + def finder_slug + friendly_id_status.slug + end + + # Was the record found using one of its friendly ids? + # @deprecated Please use #friendly_id_status.friendly? + def found_using_friendly_id? + friendly_id_status.friendly? + end + + # Was the record found using its numeric id? + # @deprecated Please use #friendly_id_status.numeric? + def found_using_numeric_id? + friendly_id_status.numeric? + end + + # Was the record found using an old friendly id? + # @deprecated Please use #friendly_id_status.outdated? + def found_using_outdated_friendly_id? + friendly_id_status.outdated? + end + + # Was the record found using an old friendly id, or its numeric id? + # @deprecated Please use !#friendly_id_status.best? + def has_better_id? + ! friendly_id_status.best? + end + end + + def self.included(base) + base.class_eval do + has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy + before_save :set_slug + after_save :set_slug_cache + # only protect the column if the class is not already using attributes_accessible + if !accessible_attributes + if friendly_id_config.cache_column + attr_protected friendly_id_config.cache_column + end + attr_protected :cached_slug + end + extend(Finders) + end + end + + include DeprecatedMethods + + def friendly_id_status + @friendly_id_status ||= Status.new(:model => self) + end + + # Does the record have (at least) one slug? + def slug? + !! slug + end + alias :has_a_slug? :slug? + + # Returns the friendly id. + # @FIXME + def friendly_id + slug(true).to_friendly_id + end + alias best_id friendly_id + + # Has the basis of our friendly id changed, requiring the generation of a + # new slug? + def new_slug_needed? + !slug || slug_text != slug.name + end + + # Returns the most recent slug, which is used to determine the friendly + # id. + def slug(reload = false) + @slug = nil if reload + @slug ||= slugs.first(:order => "id DESC") + end + + # Returns the friendly id, or if none is available, the numeric id. + def to_param + if cache_column + read_attribute(cache_column) || id.to_s + else + slug ? slug.to_friendly_id : id.to_s + end + end + + def normalize_friendly_id(string) + if friendly_id_config.normalizer? + SlugString.new friendly_id_config.normalizer.call(string) + else + string = SlugString.new string + string.approximate_ascii! if friendly_id_config.approximate_ascii? + string.to_ascii! if friendly_id_config.strip_non_ascii? + string.normalize! + string + end + end + + private + + # Get the processed string used as the basis of the friendly id. + def slug_text + base = normalize_friendly_id(send(friendly_id_config.method)) + if base.length > friendly_id_config.max_length + base = base[0...friendly_id_config.max_length] + end + if friendly_id_config.reserved_words.include?(base.to_s) + raise SlugGenerationError.new("The slug text is a reserved value") + elsif base.blank? + raise SlugGenerationError.new("The slug text is blank") + end + return base.to_s + end + + + def cache_column + self.class.cache_column + end + + # Set the slug using the generated friendly id. + def set_slug + if friendly_id_config.use_slug? && new_slug_needed? + @slug = nil + slug_attributes = {:name => slug_text} + if friendly_id_config.scope? + scope = send(friendly_id_config.scope) + slug_attributes[:scope] = scope.respond_to?(:to_param) ? scope.to_param : scope.to_s + end + # If we're renaming back to a previously used friendly_id, delete the + # slug so that we can recycle the name without having to use a sequence. + slugs.find(:all, :conditions => {:name => slug_text, :scope => slug_attributes[:scope]}).each { |s| s.destroy } + slugs.build slug_attributes + end + end + + def set_slug_cache + if cache_column && send(cache_column) != slug.to_friendly_id + send "#{cache_column}=", slug.to_friendly_id + send :update_without_callbacks + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/friendly_id/config.rb b/lib/friendly_id/config.rb index 28c7ec2c1..11b2346e8 100644 --- a/lib/friendly_id/config.rb +++ b/lib/friendly_id/config.rb @@ -67,6 +67,14 @@ def initialize(configured_class, method, options = nil, &block) def reserved_words=(*words) @reserved_words = words.flatten.uniq end + + def reserved?(word) + reserved_words.include? word + end + + def reserved_error_message(word) + return method, (reserved_message % word) if reserved? word + end %w[approximate_ascii normalizer scope strip_non_ascii use_slug].each do |method| class_eval(<<-EOM) diff --git a/lib/friendly_id/finder.rb b/lib/friendly_id/finder.rb new file mode 100644 index 000000000..a3dd6b9bd --- /dev/null +++ b/lib/friendly_id/finder.rb @@ -0,0 +1,114 @@ +module FriendlyId + + # @abstract + class Finder + + attr_accessor :ids + attr_accessor :options + attr_accessor :scope + attr_accessor :model + + class << self + + # Is the id friendly or numeric? Not that the return value here is + # +false+ if the +id+ is definitely not friendly, and +nil+ if it can + # not be determined. + # The return value will be: + # * +true+ - if the id is definitely friendly (i.e., any string with non-numeric characters) + # * +false+ - if the id is definitely unfriendly (i.e., an Integer, ActiveRecord::Base, etc.) + # * +nil+ - if it can not be determined (i.e., a numeric string like "206".) + # @return [true, false, nil] + # @see #unfriendly? + def friendly?(id) + if id.is_a?(Integer) || id.kind_of?(ActiveRecord::Base) + return false + elsif id.to_i.to_s != id.to_s + return true + else + return nil + end + end + + # Is the id friendly or numeric? + # @return [true, false, nil] +true+ if definitely unfriendly, +false+ if + # definitely friendly, else +nil+. + # @see #friendly? + def unfriendly?(id) + !friendly?(id) unless friendly?(id) == nil + end + + def parse(id) + name, sequence = id.to_s.split("--") + sequence ||= 1 + return name, sequence + end + + end + + def initialize(ids, model, options={}) + self.ids = ids + self.options = options + self.model = model + self.scope = options[:scope] + end + + def method_missing(*args, &block) + model.send(*args, &block) + end + + def expected_size + limited? ? limit : offset_size + end + + def friendly? + self.class.friendly?(id) + end + + def id + ids[0] + end + + def ids=(ids) + @ids = [ids].flatten + end + alias :id= :ids= + + def limit + options[:limit] + end + + def limited? + offset_size > limit if limit + end + + def name + id.split("--")[0] if id + end + + def offset + options[:offset].to_i + end + + def offset_size + ids.size - offset + end + + def scope=(scope) + @scope = scope.to_param unless scope.nil? + end + + def sequence + (id.split("--")[1] || 1) if id + end + + def slugs_included? + [*(options[:include] or [])].flatten.include?(:slugs) + end + + def unfriendly? + self.class.unfriendly?(id) + end + + end + +end \ No newline at end of file diff --git a/lib/friendly_id/helpers.rb b/lib/friendly_id/helpers.rb deleted file mode 100644 index 4f8050ebe..000000000 --- a/lib/friendly_id/helpers.rb +++ /dev/null @@ -1,12 +0,0 @@ -module FriendlyId - - module Helpers - # Calculate expected result size for find_some_with_friendly (taken from - # active_record/base.rb) - def expected_size(ids_and_names, options) #:nodoc:# - size = options[:offset] ? ids_and_names.size - options[:offset] : ids_and_names.size - size = options[:limit] if options[:limit] && size > options[:limit] - size - end - end -end diff --git a/lib/friendly_id/simple/class_methods.rb b/lib/friendly_id/simple/class_methods.rb deleted file mode 100644 index b65373a92..000000000 --- a/lib/friendly_id/simple/class_methods.rb +++ /dev/null @@ -1,38 +0,0 @@ -module FriendlyId - module Simple - module ClassMethods - - include FriendlyId::Helpers - - protected - - def find_one(id, options) #:nodoc:# - if id.respond_to?(:to_str) && result = send("find_by_#{ friendly_id_config.method}", id.to_str, options) - result.send(:found_using_friendly_id=, true) - else - result = super id, options - end - result - end - - def find_some(ids_and_names, options) #:nodoc:# - - names, ids = ids_and_names.partition {|id_or_name| id_or_name.respond_to?(:to_str) && id_or_name.to_str } - results = with_scope :find => options do - find :all, :conditions => ["#{quoted_table_name}.#{primary_key} IN (?) OR #{friendly_id_config.method} IN (?)", - ids, names] - end - - expected = expected_size(ids_and_names, options) - if results.size != expected - raise ActiveRecord::RecordNotFound, "Couldn't find all #{ name.pluralize } with IDs (#{ ids_and_names * ', ' }) AND #{ sanitize_sql options[:conditions] } (found #{ results.size } results, but was looking for #{ expected })" - end - - results.each {|r| r.send(:found_using_friendly_id=, true) if names.include?(r.friendly_id)} - - results - - end - end - end -end diff --git a/lib/friendly_id/simple/instance_methods.rb b/lib/friendly_id/simple/instance_methods.rb deleted file mode 100644 index 5d73f35ef..000000000 --- a/lib/friendly_id/simple/instance_methods.rb +++ /dev/null @@ -1,49 +0,0 @@ -module FriendlyId - module Simple - module InstanceMethods - - def self.included(base) - base.validate :validate_friendly_id - end - - attr :found_using_friendly_id - - # Was the record found using one of its friendly ids? - def found_using_friendly_id? - @found_using_friendly_id - end - - # Was the record found using its numeric id? - def found_using_numeric_id? - !@found_using_friendly_id - end - alias has_better_id? found_using_numeric_id? - - # Returns the friendly_id. - def friendly_id - send friendly_id_config.method - end - alias best_id friendly_id - - # Returns the friendly id, or if none is available, the numeric id. - def to_param - (friendly_id || id).to_s - end - - private - - def validate_friendly_id - if self.class.friendly_id_config.reserved_words.include? friendly_id - self.errors.add(self.class.friendly_id_config.method, - self.class.friendly_id_config.reserved_message % friendly_id) - return false - end - end - - def found_using_friendly_id=(value) #:nodoc# - @found_using_friendly_id = value - end - - end - end -end diff --git a/lib/friendly_id/slugged/class_methods.rb b/lib/friendly_id/slugged/class_methods.rb deleted file mode 100644 index 86fd1a99a..000000000 --- a/lib/friendly_id/slugged/class_methods.rb +++ /dev/null @@ -1,126 +0,0 @@ -module FriendlyId - module Slugged - module ClassMethods - - include FriendlyId::Helpers - - # Finds a single record using the friendly id, or the record's id. - def find_one(id_or_name, options) #:nodoc:# - - scope = options.delete(:scope) - scope = scope.to_param if scope && scope.respond_to?(:to_param) - - if id_or_name.is_a?(Integer) || id_or_name.kind_of?(ActiveRecord::Base) - return super(id_or_name, options) - end - - find_options = {:select => "#{self.table_name}.*"} - find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs) - - name, sequence = FriendlyId.parse_friendly_id(id_or_name) - - find_options[:conditions] = { - "#{Slug.table_name}.name" => name, - "#{Slug.table_name}.scope" => scope, - "#{Slug.table_name}.sequence" => sequence - } - - result = with_scope(:find => find_options) { find_initial(options) } - if result - result.friendly_id_finder = Finder.new(:model => result, :name => id_or_name) - elsif id_or_name.to_i.to_s != id_or_name - raise ActiveRecord::RecordNotFound - else - result = super id_or_name, options - end - - result - - rescue ActiveRecord::RecordNotFound => e - - if friendly_id_config.scope? - if !scope - raise ActiveRecord::RecordNotFound.new("%s; expected scope but got none" % e.message) - else - raise ActiveRecord::RecordNotFound.new("%s and scope=#{scope}" % e.message) - end - end - - raise e - - end - - # Finds multiple records using the friendly ids, or the records' ids. - def find_some(ids_and_names, options) #:nodoc:# - - slugs, ids = get_slugs_and_ids(ids_and_names, options) - results = [] - - find_options = {:select => "#{self.table_name}.*"} - find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs) - find_options[:conditions] = "#{quoted_table_name}.#{primary_key} IN (#{ids.empty? ? 'NULL' : ids.join(',')}) " - find_options[:conditions] << "OR slugs.id IN (#{slugs.to_s(:db)})" - - results = with_scope(:find => find_options) { find_every(options) }.uniq - - expected = expected_size(ids_and_names, options) - if results.size != expected - raise ActiveRecord::RecordNotFound, "Couldn't find all #{ name.pluralize } with IDs (#{ ids_and_names * ', ' }) AND #{ sanitize_sql options[:conditions] } (found #{ results.size } results, but was looking for #{ expected })" - end - - assign_finders(slugs, results) - - results - end - - def validate_find_options(options) #:nodoc:# - options.assert_valid_keys([:conditions, :include, :joins, :limit, :offset, - :order, :select, :readonly, :group, :from, :lock, :having, :scope]) - end - - def cache_column - if defined?(@cache_column) - return @cache_column - elsif friendly_id_config.cache_column - @cache_column = friendly_id_config.cache_column - elsif columns.any? { |c| c.name == 'cached_slug' } - @cache_column = :cached_slug - else - @cache_column = nil - end - end - - private - - # Assign finder slugs for the results found in find_some_with_friendly - def assign_finders(slugs, results) #:nodoc:# - slugs.each do |slug| - results.select { |r| r.id == slug.sluggable_id }.each do |result| - result.friendly_id_finder = Finder.new(:model => result, :slug => slug) - end - end - end - - # Build arrays of slugs and ids, for the find_some_with_friendly method. - def get_slugs_and_ids(ids_and_names, options) #:nodoc:# - scope = options.delete(:scope) - slugs = [] - ids = [] - ids_and_names.each do |id_or_name| - name, sequence = FriendlyId.parse_friendly_id id_or_name.to_s - slug = Slug.find(:first, :conditions => { - :name => name, - :scope => scope, - :sequence => sequence, - :sluggable_type => base_class.name - }) - # If the slug was found, add it to the array for later use. If not, and - # the id_or_name is a number, assume that it is a regular record id. - slug ? slugs << slug : (ids << id_or_name if id_or_name.to_s =~ /\A\d*\z/) - end - return slugs, ids - end - - end - end -end diff --git a/lib/friendly_id/slugged/finder.rb b/lib/friendly_id/slugged/finder.rb deleted file mode 100644 index a6245f696..000000000 --- a/lib/friendly_id/slugged/finder.rb +++ /dev/null @@ -1,52 +0,0 @@ -module FriendlyId - module Slugged - - # FriendlyId::Finder presents information about the status of the - # id that was used to find the model: whether it was found using a - # numeric id or friendly id, whether the friendly id used to find the - # model is the most current one. - class Finder - - attr_accessor :name - attr_accessor :slug - attr_accessor :model - - def initialize(options={}) - options.each {|key, value| self.send("#{key}=".to_sym, value)} - end - - # The slug that was used to find the model. - def slug - @slug ||= model.slugs.find_by_name_and_sequence(*FriendlyId.parse_friendly_id(name)) - end - - # Did the find operation use a friendly id? - def friendly? - !! (name or slug) - end - - # Did the find operation use a numeric id? - def numeric? - !friendly? - end - - # Did the find operation use the current slug? - def current? - slug.is_most_recent? - end - - # Did the find operation use an outdated slug? - def outdated? - current? - end - - # Did the find operation use the best possible id? True if there is - # a slug, and the most recent one was used. - def best? - friendly? and current? - end - - end - - end -end diff --git a/lib/friendly_id/slugged/instance_methods.rb b/lib/friendly_id/slugged/instance_methods.rb deleted file mode 100644 index 790fedc44..000000000 --- a/lib/friendly_id/slugged/instance_methods.rb +++ /dev/null @@ -1,139 +0,0 @@ -module FriendlyId - module Slugged - module InstanceMethods - - def self.included(base) - base.class_eval do - has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy - before_save :set_slug - after_save :set_slug_cache - # only protect the column if the class is not already using attributes_accessible - if !accessible_attributes - if friendly_id_config.cache_column - attr_protected friendly_id_config.cache_column - end - attr_protected :cached_slug - end - end - end - - attr_accessor :friendly_id_finder - - def finder_slug - @friendly_id_finder && @friendly_id_finder.slug - end - - # Was the record found using one of its friendly ids? - def found_using_friendly_id? - @friendly_id_finder ? @friendly_id_finder.friendly? : false - end - - # Was the record found using its numeric id? - def found_using_numeric_id? - @friendly_id_finder ? @friendly_id_finder.numeric? : true - end - - # Was the record found using an old friendly id? - def found_using_outdated_friendly_id? - @friendly_id_finder.outdated? if @friendly_id_finder - end - - # Was the record found using an old friendly id, or its numeric id? - def has_better_id? - @friendly_id_finder ? !@friendly_id_finder.best? : true - end - - # Does the record have (at least) one slug? - def slug? - !! slug - end - alias :has_a_slug? :slug? - - # Returns the friendly id. - # @FIXME - def friendly_id - slug(true).to_friendly_id - end - alias best_id friendly_id - - # Has the basis of our friendly id changed, requiring the generation of a - # new slug? - def new_slug_needed? - !slug || slug_text != slug.name - end - - # Returns the most recent slug, which is used to determine the friendly - # id. - def slug(reload = false) - @slug = nil if reload - @slug ||= slugs.first(:order => "id DESC") - end - - # Returns the friendly id, or if none is available, the numeric id. - def to_param - if cache_column - read_attribute(cache_column) || id.to_s - else - slug ? slug.to_friendly_id : id.to_s - end - end - - def normalize_friendly_id(string) - if friendly_id_config.normalizer? - SlugString.new friendly_id_config.normalizer.call(string) - else - string = SlugString.new string - string.approximate_ascii! if friendly_id_config.approximate_ascii? - string.to_ascii! if friendly_id_config.strip_non_ascii? - string.normalize! - string - end - end - - private - - # Get the processed string used as the basis of the friendly id. - def slug_text - base = normalize_friendly_id(send(friendly_id_config.method)) - if base.length > friendly_id_config.max_length - base = base[0...friendly_id_config.max_length] - end - if friendly_id_config.reserved_words.include?(base.to_s) - raise FriendlyId::SlugGenerationError.new("The slug text is a reserved value") - elsif base.blank? - raise FriendlyId::SlugGenerationError.new("The slug text is blank") - end - return base.to_s - end - - - def cache_column - self.class.cache_column - end - - # Set the slug using the generated friendly id. - def set_slug - if friendly_id_config.use_slug? && new_slug_needed? - @slug = nil - slug_attributes = {:name => slug_text} - if friendly_id_config.scope? - scope = send(friendly_id_config.scope) - slug_attributes[:scope] = scope.respond_to?(:to_param) ? scope.to_param : scope.to_s - end - # If we're renaming back to a previously used friendly_id, delete the - # slug so that we can recycle the name without having to use a sequence. - slugs.find(:all, :conditions => {:name => slug_text, :scope => slug_attributes[:scope]}).each { |s| s.destroy } - slugs.build slug_attributes - end - end - - def set_slug_cache - if cache_column && send(cache_column) != slug.to_friendly_id - send "#{cache_column}=", slug.to_friendly_id - send :update_without_callbacks - end - end - - end - end -end diff --git a/lib/friendly_id/status.rb b/lib/friendly_id/status.rb new file mode 100644 index 000000000..d96d84510 --- /dev/null +++ b/lib/friendly_id/status.rb @@ -0,0 +1,36 @@ +module FriendlyId + + # FriendlyId::AbstractStatus presents information about the status of the + # id that was used to find the model. + # @abstract + class Status + + # The id or name used as the finder argument + attr_accessor :name + + # The found result, if any + attr_accessor :model + + def initialize(options={}) + options.each {|key, value| self.send("#{key}=".to_sym, value)} + end + + # Did the find operation use a friendly id? + def friendly? + raise NotImplemtedError.new + end + + # Did the find operation use a numeric id? + def numeric? + !friendly? + end + + # Did the find operation use the best possible id? True if there is + # a slug, and the most recent one was used. + def best? + raise NotImplementedError.new + end + + end + +end diff --git a/lib/friendly_id/version.rb b/lib/friendly_id/version.rb index e7e6ae070..745fedbae 100644 --- a/lib/friendly_id/version.rb +++ b/lib/friendly_id/version.rb @@ -1,8 +1,8 @@ module FriendlyId #:nodoc: module Version #:nodoc: MAJOR = 2 - MINOR = 2 - TINY = 7 + MINOR = 3 + TINY = 0 STRING = [MAJOR, MINOR, TINY].join('.') end end diff --git a/test/friendly_id_test.rb b/test/friendly_id_test.rb index 04e3f8eb9..e4f5ce2a8 100644 --- a/test/friendly_id_test.rb +++ b/test/friendly_id_test.rb @@ -1,7 +1,7 @@ require File.dirname(__FILE__) + '/test_helper' class FriendlyIdTest < Test::Unit::TestCase - + context "the FriendlyId module" do should "parse a friendly_id name and sequence" do diff --git a/test/non_slugged_test.rb b/test/non_slugged_test.rb index 9ec1484f2..7e6eabb19 100644 --- a/test/non_slugged_test.rb +++ b/test/non_slugged_test.rb @@ -11,7 +11,7 @@ class NonSluggedTest < Test::Unit::TestCase teardown do User.delete_all end - + should "have a friendly_id config" do assert_not_nil User.friendly_id_config end diff --git a/test/scoped_model_test.rb b/test/scoped_model_test.rb index 84a24f961..91b32b85f 100644 --- a/test/scoped_model_test.rb +++ b/test/scoped_model_test.rb @@ -46,7 +46,7 @@ class ScopedModelTest < Test::Unit::TestCase Resident.find(@resident.friendly_id) fail "The find should not have succeeded" rescue ActiveRecord::RecordNotFound => e - assert_match /expected scope/, e.message + assert_match /scope: expected/, e.message end end @@ -55,7 +55,7 @@ class ScopedModelTest < Test::Unit::TestCase Resident.find(@resident.friendly_id, :scope => "badscope") fail "The find should not have succeeded" rescue ActiveRecord::RecordNotFound => e - assert_match /scope=badscope/, e.message + assert_match /scope: badscope/, e.message end end