require 'globalize3' class Page < ActiveRecord::Base if self.respond_to?(:translates) translates :title, :custom_title, :meta_keywords, :meta_description, :browser_title, :include => :seo_meta # Set up support for meta tags through translations. if defined?(::Page::Translation) attr_accessible :title # set allowed attributes for mass assignment ::Page::Translation.send :attr_accessible, :browser_title, :meta_description, :meta_keywords, :locale if ::Page::Translation.table_exists? def translation if @translation.nil? or @translation.try(:locale) != ::Globalize.locale @translation = translations.with_locale(::Globalize.locale).first @translation ||= translations.build(:locale => ::Globalize.locale) end @translation end # Instruct the Translation model to have meta tags. ::Page::Translation.send :is_seo_meta fields = ::SeoMeta.attributes.keys.reject{|f| self.column_names.map(&:to_sym).include?(f) }.map{|a| [a, :"#{a}="]}.flatten delegate *(fields << {:to => :translation}) after_save proc {|m| m.translation.save} end end before_create :ensure_locale, :if => proc { |c| defined?(::Refinery::I18n) && ::Refinery::I18n.enabled? } end 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, :custom_title_type, :parent_id, :custom_title, :created_at, :updated_at, :page_id 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 :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 => RefinerySetting.find_or_set(:approximate_ascii, false, :scoping => "pages"), :strip_non_ascii => RefinerySetting.find_or_set(:strip_non_ascii, false, :scoping => "pages") has_many :parts, :class_name => "PagePart", :order => "position ASC", :inverse_of => :page, :dependent => :destroy, :include => ((:translations) if defined?(::PagePart::Translation)) 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, :custom_title, :browser_title, :all_page_part_content] before_destroy :deletable? after_save :reposition_parts!, :invalidate_child_cached_url, :expire_page_caching after_destroy :expire_page_caching # Wrap up the logic of finding the pages based on the translations table. if defined?(::Page::Translation) def self.with_globalize(conditions = {}) conditions = {:locale => Globalize.locale}.merge(conditions) where(:id => ::Page::Translation.where(conditions).select('page_id AS id')).includes(:children, :slugs) end else def self.with_globalize(conditions = {}) where(conditions).includes(:children, :slugs) end end 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 } # when a dialog pops up to link to a page, how many pages per page should there be PAGES_PER_DIALOG = 14 # when listing pages out in the admin area, how many pages should show per page PAGES_PER_ADMIN_INDEX = 20 # when collecting the pages path how is each of the pages seperated? PATH_SEPARATOR = " - " # 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 if deletable? super else 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 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.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 self.class.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 defined?(::Refinery::I18n) 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, :id => nil) end def url_normal {:controller => '/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, # 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, super].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 class << self # Accessor to find out the default page parts created for each new page def default_parts RefinerySetting.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? defined?(::Refinery::I18n) && ::Refinery::I18n.enabled? && ::Refinery::I18n.current_frontend_locale != ::I18n.locale end # Returns how many pages per page should there be when paginating pages def per_page(dialog = false) dialog ? PAGES_PER_DIALOG : PAGES_PER_ADMIN_INDEX end def use_marketable_urls? RefinerySetting.find_or_set(:use_marketable_urls, true, :scoping => 'pages') 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 ***" end end end # Accessor method to get a page part from a page. # Example: # # Page.first[:body] # # Will return the body page part of the first page. def [](part_title) # Allow for calling attributes with [] shorthand (eg page[:parent_id]) return super if self.attributes.has_key?(part_title.to_s) # the way that we call page parts seems flawed, will probably revert to page.parts[:title] in a future release. # self.parts is 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 def title_with_meta title = if self.title.nil? [::Page::Translation.where(:page_id => self.id, :locale => Globalize.locale).first.try(:title).to_s] else [self.title.to_s] end title << "(#{::I18n.t('hidden', :scope => 'admin.pages.page')})" unless show_in_menu? title << "(#{::I18n.t('draft', :scope => 'admin.pages.page')})" 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 self.class.use_marketable_urls? && self.class.friendly_id_config.reserved_words.include?(sluggified) sluggified << "-page" end sluggified end private def invalidate_child_cached_url return true unless self.class.use_marketable_urls? children.each do |child| Rails.cache.delete(child.url_cache_key) Rails.cache.delete(child.path_cache_key) end end 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