forked from refinery/refinerycms
/
page.rb
394 lines (327 loc) · 13.8 KB
/
page.rb
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
module Refinery
class Page < ActiveRecord::Base
# when collecting the pages path how is each of the pages seperated?
PATH_SEPARATOR = " - "
if self.respond_to?(:translates)
translates :title, :menu_title, :meta_keywords, :meta_description, :browser_title, :custom_slug, :include => :seo_meta
end
attr_accessible :title
# Delegate SEO Attributes to globalize3 translation
seo_fields = ::SeoMeta.attributes.keys.map{|a| [a, :"#{a}="]}.flatten
delegate *(seo_fields << {:to => :translation})
after_save proc {|m| m.translation.save}
# Wrap up the logic of finding the pages based on the translations table.
def self.with_globalize(conditions = {})
conditions = {:locale => Globalize.locale}.merge(conditions)
globalized_conditions = {}
conditions.keys.each do |key|
if (translated_attribute_names.map(&:to_s) | %w(locale)).include?(key.to_s)
globalized_conditions["#{self.translation_class.table_name}.#{key}"] = conditions.delete(key)
end
end
# A join implies readonly which we don't really want.
joins(:translations).where(globalized_conditions).where(conditions).readonly(false)
end
before_create :ensure_locale, :if => proc { |c| ::Refinery.i18n_enabled? }
attr_accessible :id, :deletable, :link_url, :menu_match, :meta_keywords,
:skip_to_first_child, :position, :show_in_menu, :draft,
:parts_attributes, :browser_title, :meta_description,
:parent_id, :menu_title, :created_at, :updated_at,
:page_id, :layout_template, :view_template, :custom_slug
attr_accessor :locale # to hold temporarily
validates :title, :presence => true
# Docs for acts_as_nested_set https://github.com/collectiveidea/awesome_nested_set
acts_as_nested_set :dependent => :destroy # rather than :delete_all
# Docs for friendly_id http://github.com/norman/friendly_id
has_friendly_id :custom_slug_or_title, :use_slug => true,
:default_locale => (::Refinery::I18n.default_frontend_locale rescue :en),
:reserved_words => %w(index new session login logout users refinery admin images wymiframe),
:approximate_ascii => ::Refinery::Setting.find_or_set(:approximate_ascii, false, :scoping => "pages"),
:strip_non_ascii => ::Refinery::Setting.find_or_set(:strip_non_ascii, false, :scoping => "pages")
def custom_slug_or_title
if custom_slug.present?
custom_slug
elsif menu_title.present?
menu_title
else
title
end
end
has_many :parts,
:foreign_key => :refinery_page_id,
:class_name => '::Refinery::PagePart',
:order => 'position ASC',
:inverse_of => :page,
:dependent => :destroy,
:include => ((:translations) if ::Refinery::PagePart.respond_to?(:translation_class))
accepts_nested_attributes_for :parts, :allow_destroy => true
# Docs for acts_as_indexed http://github.com/dougal/acts_as_indexed
acts_as_indexed :fields => [:title, :meta_keywords, :meta_description,
:menu_title, :browser_title, :all_page_part_content]
before_destroy :deletable?
after_save :reposition_parts!, :invalidate_cached_urls, :expire_page_caching
after_update :invalidate_cached_urls
after_destroy :expire_page_caching
scope :live, where(:draft => false)
scope :by_title, proc {|t| with_globalize(:title => t)}
# Shows all pages with :show_in_menu set to true, but it also
# rejects any page that has not been translated to the current locale.
# This works using a query against the translated content first and then
# using all of the page_ids we further filter against this model's table.
scope :in_menu, proc { where(:show_in_menu => true).with_globalize }
scope :fast_menu, proc {
# First, apply a filter to determine which pages to show.
# We need to join to the page's slug to avoid multiple queries.
pages = live.in_menu.includes(:slug, :slugs).order('lft ASC')
# Now we only want to select particular columns to avoid any further queries.
# Title and menu_title are retrieved in the next block below so they are not here.
menu_columns.each do |column|
pages = pages.select(arel_table[column.to_sym])
end
# We have to get title and menu_title from the translations table.
# To avoid calling globalize3 an extra time, we get title as page_title
# and we get menu_title as page_menu_title.
# These is used in 'to_refinery_menu_item' in the Page model.
%w(title menu_title).each do |column|
pages = pages.joins(:translations).select(
"#{translation_class.table_name}.#{column} as page_#{column}"
)
end
pages
}
# Am I allowed to delete this page?
# If a link_url is set we don't want to break the link so we don't allow them to delete
# If deletable is set to false then we don't allow this page to be deleted. These are often Refinery system pages
def deletable?
deletable && link_url.blank? and menu_match.blank?
end
# Repositions the child page_parts that belong to this page.
# This ensures that they are in the correct 0,1,2,3,4... etc order.
def reposition_parts!
parts.each_with_index do |part, index|
part.update_attribute(:position, index)
end
end
# Before destroying a page we check to see if it's a deletable page or not
# Refinery system pages are not deletable.
def destroy
return super if deletable?
unless Rails.env.test?
# give useful feedback when trying to delete from console
puts "This page is not deletable. Please use .destroy! if you really want it deleted "
puts "unset .link_url," if link_url.present?
puts "unset .menu_match," if menu_match.present?
puts "set .deletable to true" unless deletable
end
return false
end
# If you want to destroy a page that is set to be not deletable this is the way to do it.
def destroy!
self.menu_match = nil
self.link_url = nil
self.deletable = true
destroy
end
# Used for the browser title to get the full path to this page
# It automatically prints out this page title and all of it's parent page titles joined by a PATH_SEPARATOR
def path(options = {})
# Override default options with any supplied.
options = {:reversed => true}.merge(options)
unless parent_id.nil?
parts = [title, parent.path(options)]
parts.reverse! if options[:reversed]
parts.join(PATH_SEPARATOR)
else
title
end
end
# When this page is rendered in the navigation, where should it link?
# If a custom "link_url" is set, it uses that otherwise it defaults to a normal page URL.
# The "link_url" is often used to link to a plugin rather than a page.
#
# For example if I had a "Contact" page I don't want it to just render a contact us page
# I want it to show the Inquiries form so I can collect inquiries. So I would set the "link_url"
# to "/contact"
def url
if link_url.present?
link_url_localised?
elsif ::Refinery::Pages.use_marketable_urls?
with_locale_param url_marketable
elsif to_param.present?
with_locale_param url_normal
end
end
def link_url_localised?
return link_url unless ::Refinery.i18n_enabled?
current_url = link_url
if current_url =~ %r{^/} && ::Refinery::I18n.current_frontend_locale != ::Refinery::I18n.default_frontend_locale
current_url = "/#{::Refinery::I18n.current_frontend_locale}#{current_url}"
end
current_url
end
def url_marketable
# :id => nil is important to prevent any other params[:id] from interfering with this route.
url_normal.merge(:path => nested_url).except(:id)
end
def url_normal
{:controller => '/refinery/pages', :action => 'show', :path => nil, :id => to_param}
end
def with_locale_param(url_hash)
if self.class.different_frontend_locale?
url_hash.update(:locale => ::Refinery::I18n.current_frontend_locale)
end
url_hash
end
# Returns an array with all ancestors to_param, allow with its own
# Ex: with an About page and a Mission underneath,
# ::Refinery::Page.find('mission').nested_url would return:
#
# ['about', 'mission']
#
def nested_url
Rails.cache.fetch(url_cache_key) { uncached_nested_url }
end
def uncached_nested_url
[parent.try(:nested_url), to_param].compact.flatten
end
# Returns the string version of nested_url, i.e., the path that should be generated
# by the router
def nested_path
Rails.cache.fetch(path_cache_key) { ['', nested_url].join('/') }
end
def path_cache_key
[cache_key, 'nested_path'].join('#')
end
def url_cache_key
[cache_key, 'nested_url'].join('#')
end
def cache_key
[Refinery.base_cache_key, ::I18n.locale, to_param].compact.join('/')
end
# Returns true if this page is "published"
def live?
not draft?
end
# Return true if this page can be shown in the navigation.
# If it's a draft or is set to not show in the menu it will return false.
def in_menu?
live? && show_in_menu?
end
def not_in_menu?
not in_menu?
end
# Returns true if this page is the home page or links to it.
def home?
link_url == '/'
end
# Returns all visible sibling pages that can be rendered for the menu
def shown_siblings
siblings.reject(&:not_in_menu?)
end
def to_refinery_menu_item
{
:id => id,
:lft => lft,
:menu_match => menu_match,
:parent_id => parent_id,
:rgt => rgt,
:title => page_menu_title.blank? ? page_title : page_menu_title,
:type => self.class.name,
:url => url
}
end
class << self
# Accessor to find out the default page parts created for each new page
def default_parts
::Refinery::Setting.find_or_set(:default_page_parts, ["Body", "Side Body"])
end
# Wraps up all the checks that we need to do to figure out whether
# the current frontend locale is different to the current one set by ::I18n.locale.
# This terminates in a false if i18n engine is not defined or enabled.
def different_frontend_locale?
::Refinery.i18n_enabled? && ::Refinery::I18n.current_frontend_locale != ::I18n.locale
end
# Override this method to change which columns you want to select to render your menu.
# title and menu_title are always retrieved so omit these.
def menu_columns
%w(id depth parent_id lft rgt link_url menu_match)
end
# Returns how many pages per page should there be when paginating pages
def per_page(dialog = false)
dialog ? Pages::Options.pages_per_dialog : Pages::Options.pages_per_admin_index
end
def expire_page_caching
begin
Rails.cache.delete_matched(/.*pages.*/)
rescue NotImplementedError
Rails.cache.clear
warn "**** [REFINERY] The cache store you are using is not compatible with Rails.cache#delete_matched - clearing entire cache instead ***"
ensure
return true # so that other callbacks process.
end
end
end
# Accessor method to get a page part from a page.
# Example:
#
# ::Refinery::Page.first.content_for(:body)
#
# Will return the body page part of the first page.
def content_for(part_title)
# self.parts is usually already eager loaded so we can now just grab
# the first element matching the title we specified.
part = self.parts.detect do |part|
part.title.present? and # protecting against the problem that occurs when have nil title
part.title == part_title.to_s or
part.title.downcase.gsub(" ", "_") == part_title.to_s.downcase.gsub(" ", "_")
end
part.try(:body)
end
# In the admin area we use a slightly different title to inform the which pages are draft or hidden pages
# We show the title from the next available locale if there is no title for the current locale
def title_with_meta
if self.title.present?
title = [self.title]
else
title = [self.translations.detect {|t| t.title.present?}.title]
end
title << "<em>(#{::I18n.t('hidden', :scope => 'refinery.admin.pages.page')})</em>" unless show_in_menu?
title << "<em>(#{::I18n.t('draft', :scope => 'refinery.admin.pages.page')})</em>" if draft?
title.join(' ')
end
# Used to index all the content on this page so it can be easily searched.
def all_page_part_content
parts.collect {|p| p.body}.join(" ")
end
##
# Protects generated slugs from title if they are in the list of reserved words
# This applies mostly to plugin-generated pages.
#
# Returns the sluggified string
def normalize_friendly_id(slug_string)
slug_string.gsub!('_', '-')
sluggified = super
if ::Refinery::Pages.use_marketable_urls? && self.class.friendly_id_config.reserved_words.include?(sluggified)
sluggified << "-page"
end
sluggified
end
private
def invalidate_cached_urls
return true unless ::Refinery::Pages.use_marketable_urls?
[self, children].flatten.each do |page|
Rails.cache.delete(page.url_cache_key)
Rails.cache.delete(page.path_cache_key)
end
end
alias_method :invalidate_child_cached_url, :invalidate_cached_urls
def ensure_locale
unless self.translations.present?
self.translations.build :locale => ::Refinery::I18n.default_frontend_locale
end
end
def expire_page_caching
self.class.expire_page_caching
end
end
end