public
Description: Pretty url support for Ruby on Rails applications
Homepage: http://www.caring.com
Clone URL: git://github.com/caring/acts_as_url_param.git
acts_as_url_param / lib / acts_as_url_param.rb
100644 181 lines (154 sloc) 5.984 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
module ActsAsUrlParam
  def self.included(base)
    base.extend ActMethods
  end
  
  module ActMethods
    
    def acts_as_url_param(*args, &block)
      extend ClassMethods
      include InstanceMethods
      include Caring::Utilities::UrlUtils
      extend Caring::Utilities::UrlUtils
      
      class_inheritable_accessor :acts_as_url_options, :acts_as_url_param_base
      # No extract options in rails 1.2.x
      options = args.respond_to?(:extract_options!) ? args.extract_options! : extract_options_from_args!(args)
      self.acts_as_url_options = options
      options[:column] = args.first || 'url_name'
      options[:from] ||= default_from_column
      
      if options[:redirectable]
        options[:on] ||= :update
        make_redirectable
      end
      
      options[:on] ||= :create
      options[:block] = block if block_given?
      callback = "before_validation"
      if options[:on] == :create
        callback += "_on_create"
        before_validation :set_url_param_if_non_existant
      end
      send callback, :set_url_param
      validates_presence_of(options[:from], :if => :empty_param?) unless options[:allow_blank]
      
      define_finder
      define_url_param_setter
      define_availability_check
      
      self.class_eval do
        alias_method_chain :validate, :unique_url unless method_defined? :validate_without_unique_url
      end
    end
    
    private
    
    def make_redirectable
      has_many :redirects, :as => :redirectable
      before_save :add_redirect
      
      class_def :add_redirect do
        if !new_record? && @name_changed && @old_name
          redirects.create(:url_name => @old_name)
        end
      end
      
      meta_def :find_redirect do |name|
        redirect = Redirect.find_by_class_and_name(self,name)
        redirect.redirectable if redirect
      end
    end
    
    def define_finder
      meta_def :find_by_url do |*args|
        send("find_by_#{acts_as_url_options[:column]}", *args)
      end
    end
    
    def define_url_param_setter
      class_def "#{acts_as_url_options[:column]}=" do |value|
        @url_name_manually_set = true if value
        @old_name = read_attribute(acts_as_url_options[:column]) unless @name_changed
        write_attribute(acts_as_url_options[:column], url_safe(value))
        @name_changed = true unless read_attribute(acts_as_url_options[:column]) == @old_name || !@old_name
      end
    end
    
    def define_availability_check
      klass = self
      meta_def :url_param_available_for_model? do |*args|
        candidate, id = *args
        conditions = acts_as_url_options[:conditions] + ' AND ' if acts_as_url_options[:conditions]
        conditions ||= ''
        conditions += "#{acts_as_url_options[:column]} = ?"
        conditions += " AND id != ?" if id
        conditions = [conditions, candidate]
        conditions << id if id
        available = if descends_from_active_record? or self == klass
          count(:conditions => conditions) == 0
        else
          base_class.count(:conditions => conditions) == 0
        end
        if acts_as_url_options[:redirectable] && available
          re_conditions = "url_name = ? AND redirectable_class = ?"
          re_conditions += "AND redirectable_id != ?" if id
          re_conditions = [re_conditions, candidate, self.to_s]
          re_conditions << id if id
          available = Redirect.count(:conditions => re_conditions) == 0
        end
        available
      end
    end
    
    def default_from_column
      %W(name label title).detect do |column_name|
        column_or_method_exists?(column_name) and self.acts_as_url_options[:to].to_s != column_name
      end
    end
    
    def column_or_method_exists?(name)
      column_names.include? name.to_s or method_defined? name
    end
    
    module ClassMethods
      def url_param_available?(candidate, id=nil)
        if proc = acts_as_url_options[:block]
          !(proc.arity == 1 ? proc.call(candidate) : proc.call(candidate, id))
        else
          url_param_available_for_model?(candidate, id)
        end
      end
      
      def compute_url_param(candidate, id=nil)
        return if candidate.blank?
        # raise ArgumentError, "The url canidate cannot be empty" if candidate.blank?
        uniquify_proc = acts_as_url_options[:block] || Proc.new { |candidate| url_param_available? candidate, id }
        uniquify(url_safe(candidate), &uniquify_proc)
      end
    end
    
    module InstanceMethods
      def compute_url_param
        # raise ArgumentError, "The column used for generating the url_param is empty" unless url_from
        self.class.compute_url_param(url_from, id)
      end
      
      def url_from
        self.class.method_defined?(acts_as_url_options[:from]) ? send(acts_as_url_options[:from]) : read_attribute(acts_as_url_options[:from])
      end
      
      def to_param
        url_param || id.to_s
      end
      
      def url_param
        read_attribute(acts_as_url_options[:column])
      end
      
      def empty_param?
        !url_param
      end
      
      private
      
      def set_url_param_if_non_existant
        unless new_record?
          set_url_param if url_param.blank?
        end
      end
      
      def set_url_param
        if url_param.blank? or (acts_as_url_options[:on] != :create && !@url_name_manually_set)
          send(acts_as_url_options[:before]) if acts_as_url_options[:before]
          url = compute_url_param
          send("#{acts_as_url_options[:column]}=", url) unless url.blank?
          @url_name_manually_set = false
          @url_param_validated = true
        end
      end
      
      def validate_with_unique_url
        return true if @url_param_validated
        avail_id = new_record? ? nil : id
        unless self.class.url_param_available? to_param, avail_id
          errors.add_to_base "The url is not unique"
        end
        validate_without_unique_url
      end
    end
  end
end