1 require 'acts_as_tree'
2
3 class Page < ActiveRecord::Base
4
5 class MissingRootPageError < StandardError
6 def initialize(message = 'Database missing root page'); super end
7 end
8
9 # Callbacks
10 before_save :update_virtual, :update_status, :set_allowed_children_cache
11
12 # Associations
13 acts_as_tree :order => 'virtual DESC, title ASC'
14 has_many :parts, :class_name => 'PagePart', :order => 'id', :dependent => :destroy
15 accepts_nested_attributes_for :parts, :allow_destroy => true
16 has_many :fields, :class_name => 'PageField', :order => 'id', :dependent => :destroy
17 accepts_nested_attributes_for :fields, :allow_destroy => true
18 belongs_to :layout
19 belongs_to :created_by, :class_name => 'User'
20 belongs_to :updated_by, :class_name => 'User'
21
22 # Validations
23 validates_presence_of :title, :slug, :breadcrumb, :status_id
24
25 validates_length_of :title, :maximum => 255
26 validates_length_of :slug, :maximum => 100
27 validates_length_of :breadcrumb, :maximum => 160
28
29 validates_format_of :slug, :with => %r{^([-_.A-Za-z0-9]*|/)$}
30 validates_uniqueness_of :slug, :scope => :parent_id
31
32 validate :valid_class_name
33
34 include Radiant::Taggable
35 include StandardTags
36 include DeprecatedTags
37 include Annotatable
38
39 annotate :description
40 attr_accessor :request, :response, :pagination_parameters
41 class_inheritable_accessor :default_child
42 self.default_child = self
43
44 set_inheritance_column :class_name
45
46 def layout_with_inheritance
47 unless layout_without_inheritance
48 parent.layout if parent?
49 else
50 layout_without_inheritance
51 end
52 end
53 alias_method_chain :layout, :inheritance
54
55 def description
56 self["description"]
57 end
58
59 def description=(value)
60 self["description"] = value
61 end
62
63 def cache?
64 true
65 end
66
67 def child_path(child)
68 clean_path(path + '/' + child.slug)
69 end
70 alias_method :child_url, :child_path
71
72 def headers
73 # Return a blank hash that child classes can override or merge
74 { }
75 end
76
77 def part(name)
78 if new_record? or parts.to_a.any?(&:new_record?)
79 parts.to_a.find {|p| p.name == name.to_s }
80 else
81 parts.find_by_name name.to_s
82 end
83 end
84
85 def has_part?(name)
86 !part(name).nil?
87 end
88
89 def has_or_inherits_part?(name)
90 has_part?(name) || inherits_part?(name)
91 end
92
93 def inherits_part?(name)
94 !has_part?(name) && self.ancestors.any? { |page| page.has_part?(name) }
95 end
96
97 def field(name)
98 if new_record? or fields.any?(&:new_record?)
99 fields.detect { |f| f.name.downcase == name.to_s.downcase }
100 else
101 fields.find_by_name name.to_s
102 end
103 end
104
105 def published?
106 status == Status[:published]
107 end
108
109 def scheduled?
110 status == Status[:scheduled]
111 end
112
113 def status
114 Status.find(self.status_id)
115 end
116
117 def status=(value)
118 self.status_id = value.id
119 end
120
121 def path
122 if parent?
123 parent.child_path(self)
124 else
125 clean_path(slug)
126 end
127 end
128 alias_method :url, :path
129
130 def process(request, response)
131 @request, @response = request, response
132 if layout
133 content_type = layout.content_type.to_s.strip
134 @response.headers['Content-Type'] = content_type unless content_type.empty?
135 end
136 headers.each { |k,v| @response.headers[k] = v }
137 @response.body = render
138 @response.status = response_code
139 end
140
141 def response_code
142 200
143 end
144
145 def render
146 if layout
147 parse_object(layout)
148 else
149 render_part(:body)
150 end
151 end
152
153 def render_part(part_name)
154 part = part(part_name)
155 if part
156 parse_object(part)
157 else
158 ''
159 end
160 end
161
162 def render_snippet(snippet)
163 parse_object(snippet)
164 end
165
166 def find_by_path(path, live = true, clean = true)
167 return nil if virtual?
168 path = clean_path(path) if clean
169 my_path = self.path
170 if (my_path == path) && (not live or published?)
171 self
172 elsif (path =~ /^#{Regexp.quote(my_path)}([^\/]*)/)
173 slug_child = children.find_by_slug($1)
174 if slug_child
175 found = slug_child.find_by_url(path, live, clean) # TODO: set to find_by_path after deprecation
176 return found if found
177 end
178 children.each do |child|
179 found = child.find_by_url(path, live, clean) # TODO: set to find_by_path after deprecation
180 return found if found
181 end
182 file_not_found_types = ([FileNotFoundPage] + FileNotFoundPage.descendants)
183 file_not_found_names = file_not_found_types.collect { |x| x.name }
184 condition = (['class_name = ?'] * file_not_found_names.length).join(' or ')
185 condition = "status_id = #{Status[:published].id} and (#{condition})" if live
186 children.find(:first, :conditions => [condition] + file_not_found_names)
187 end
188 end
189 alias_method :find_by_url, :find_by_path
190
191 def update_status
192 self.published_at = Time.zone.now if published? && self.published_at == nil
193
194 if self.published_at != nil && (published? || scheduled?)
195 self[:status_id] = Status[:scheduled].id if self.published_at > Time.zone.now
196 self[:status_id] = Status[:published].id if self.published_at <= Time.zone.now
197 end
198
199 true
200 end
201
202 def to_xml(options={}, &block)
203 super(options.reverse_merge(:include => :parts), &block)
204 end
205
206 def default_child
207 self.class.default_child
208 end
209
210 def allowed_children_lookup
211 [default_child, *Page.descendants.sort_by(&:name)].uniq
212 end
213
214 def set_allowed_children_cache
215 self.allowed_children_cache = allowed_children_lookup.collect(&:name).join(',')
216 end
217
218 class << self
219
220 def root
221 find_by_parent_id(nil)
222 end
223
224 def find_by_path(path, live = true)
225 raise MissingRootPageError unless root
226 root.find_by_path(path, live)
227 end
228 def find_by_url(*args)
229 ActiveSupport::Deprecation.warn("`find_by_url' has been deprecated; use `find_by_path' instead.", caller)
230 find_by_path(*args)
231 end
232
233 def date_column_names
234 self.columns.collect{|c| c.name if c.sql_type =~ /(date|time)/}.compact
235 end
236
237 def display_name(string = nil)
238 if string
239 @display_name = string
240 else
241 @display_name ||= begin
242 n = name.to_s
243 n.sub!(/^(.+?)Page$/, '\1')
244 n.gsub!(/([A-Z])/, ' \1')
245 n.strip
246 end
247 end
248 @display_name = @display_name + " - not installed" if missing? && @display_name !~ /not installed/
249 @display_name
250 end
251
252 def display_name=(string)
253 display_name(string)
254 end
255
256 def load_subclasses
257 ([RADIANT_ROOT] + Radiant::Extension.descendants.map(&:root)).each do |path|
258 Dir["#{path}/app/models/*_page.rb"].each do |page|
259 $1.camelize.constantize if page =~ %r{/([^/]+)\.rb}
260 end
261 end
262 if ActiveRecord::Base.connection.tables.include?('pages') && Page.column_names.include?('class_name') # Assume that we have bootstrapped
263 Page.connection.select_values("SELECT DISTINCT class_name FROM pages WHERE class_name <> '' AND class_name IS NOT NULL").each do |p|
264 begin
265 p.constantize
266 rescue NameError, LoadError
267 eval(%Q{class #{p} < Page; acts_as_tree; def self.missing?; true end end}, TOPLEVEL_BINDING)
268 end
269 end
270 end
271 end
272
273 def new_with_defaults(config = Radiant::Config)
274 page = new
275 page.parts.concat default_page_parts(config)
276 page.fields.concat default_page_fields(config)
277 default_status = config['defaults.page.status']
278 page.status = Status[default_status] if default_status
279 page
280 end
281
282 def is_descendant_class_name?(class_name)
283 (Page.descendants.map(&:to_s) + [nil, "", "Page"]).include?(class_name)
284 end
285
286 def descendant_class(class_name)
287 raise ArgumentError.new("argument must be a valid descendant of Page") unless is_descendant_class_name?(class_name)
288 if ["", nil, "Page"].include?(class_name)
289 Page
290 else
291 class_name.constantize
292 end
293 end
294
295 def missing?
296 false
297 end
298
299 private
300
301 def default_page_parts(config = Radiant::Config)
302 default_parts = config['defaults.page.parts'].to_s.strip.split(/\s*,\s*/)
303 default_parts.map do |name|
304 PagePart.new(:name => name, :filter_id => config['defaults.page.filter'])
305 end
306 end
307
308 def default_page_fields(config = Radiant::Config)
309 default_fields = config['defaults.page.fields'].to_s.strip.split(/\s*,\s*/)
310 default_fields.map do |name|
311 PageField.new(:name => name)
312 end
313 end
314 end
315
316 private
317
318 def valid_class_name
319 unless Page.is_descendant_class_name?(class_name)
320 errors.add :class_name, "must be set to a valid descendant of Page"
321 end
322 end
323
324 def attributes_protected_by_default
325 super - [self.class.inheritance_column]
326 end
327
328 def update_virtual
329 unless self.class == Page.descendant_class(class_name)
330 self.virtual = Page.descendant_class(class_name).new.virtual?
331 else
332 self.virtual = virtual?
333 end
334 true
335 end
336
337 def clean_path(path)
338 "/#{ path.to_s.strip }/".gsub(%r{//+}, '/')
339 end
340 alias_method :clean_url, :clean_path
341
342 def parent?
343 !parent.nil?
344 end
345
346 def lazy_initialize_parser_and_context
347 unless @parser and @context
348 @context = PageContext.new(self)
349 @parser = Radius::Parser.new(@context, :tag_prefix => 'r')
350 end
351 @parser
352 end
353
354 def parse(text)
355 lazy_initialize_parser_and_context.parse(text)
356 end
357
358 def parse_object(object)
359 text = object.content || ''
360 text = parse(text)
361 text = object.filter.filter(text) if object.respond_to? :filter_id
362 text
363 end
364
365 end