slices 1.0.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +85 -17
  4. data/app/assets/images/slices/i18n.png +0 -0
  5. data/app/assets/images/slices/icon_collapse.png +0 -0
  6. data/app/assets/javascripts/slices/app/helpers/apply_defaults.js +9 -0
  7. data/app/assets/javascripts/slices/app/helpers/composer.js +2 -0
  8. data/app/assets/javascripts/slices/app/helpers/i18n.js +7 -0
  9. data/app/assets/javascripts/slices/app/helpers/markdown_cheat_sheet.js +50 -0
  10. data/app/assets/javascripts/slices/app/helpers/radio_field.js +21 -0
  11. data/app/assets/javascripts/slices/app/helpers/sitemap.js +82 -1
  12. data/app/assets/javascripts/slices/app/helpers/uploader.js +1 -1
  13. data/app/assets/javascripts/slices/app/helpers/url_field.js +17 -0
  14. data/app/assets/javascripts/slices/app/slices.js +14 -11
  15. data/app/assets/javascripts/slices/app/views/composer_view.js +16 -0
  16. data/app/assets/javascripts/slices/app/views/radio_field_view.js +67 -0
  17. data/app/assets/javascripts/slices/app/views/token_field_view.js +81 -24
  18. data/app/assets/javascripts/slices/app/views/url_field_view.js +44 -0
  19. data/app/assets/javascripts/slices/slices.js +1 -0
  20. data/app/assets/javascripts/slices/vendor/jquery.js +4 -2
  21. data/app/assets/stylesheets/slices/admin.css.erb +156 -31
  22. data/app/controllers/admin/admin_controller.rb +16 -1
  23. data/app/controllers/admin/assets_controller.rb +1 -1
  24. data/app/controllers/admin/entries_controller.rb +1 -1
  25. data/app/controllers/admin/pages_controller.rb +4 -13
  26. data/app/controllers/admin/site_maps_controller.rb +1 -2
  27. data/app/controllers/slices_controller.rb +12 -1
  28. data/app/helpers/admin/admin_helper.rb +8 -0
  29. data/app/helpers/admin/site_maps_helper.rb +41 -6
  30. data/app/helpers/navigation_helper.rb +8 -3
  31. data/app/helpers/pages_helper.rb +2 -2
  32. data/app/helpers/snippets_helper.rb +26 -0
  33. data/app/models/admin.rb +2 -3
  34. data/app/models/asset.rb +9 -20
  35. data/app/models/attachment.rb +10 -1
  36. data/app/models/page.rb +45 -34
  37. data/app/models/site_map.rb +1 -2
  38. data/app/models/slice.rb +46 -1
  39. data/app/models/snippet.rb +13 -4
  40. data/app/presenters/page_presenter.rb +8 -13
  41. data/app/views/admin/auth/sessions/_form.html.erb +3 -0
  42. data/app/views/admin/pages/_fields.html.erb +1 -0
  43. data/app/views/admin/pages/_slices.html.erb +8 -4
  44. data/app/views/admin/pages/new.html.erb +5 -1
  45. data/app/views/admin/pages/show.html.erb +30 -14
  46. data/app/views/admin/site_maps/_page_actions.html.erb +2 -0
  47. data/app/views/admin/site_maps/_page_li.html.erb +18 -7
  48. data/app/views/admin/site_maps/_set_page_li.html.erb +5 -5
  49. data/app/views/layouts/admin.html.erb +0 -17
  50. data/config/routes.rb +13 -16
  51. data/lib/generators/slice/templates/presenter.rb +11 -12
  52. data/lib/generators/slice/templates/set.html.erb +2 -2
  53. data/lib/generators/slices/install_generator.rb +13 -3
  54. data/lib/generators/slices/templates/slices.rb +3 -4
  55. data/lib/mongo_search.rb +1 -1
  56. data/lib/slices.rb +2 -9
  57. data/lib/slices/asset/maker.rb +2 -3
  58. data/lib/slices/available_slices.rb +8 -1
  59. data/lib/slices/config.rb +49 -8
  60. data/lib/slices/engine.rb +0 -4
  61. data/lib/slices/has_attachments.rb +25 -37
  62. data/lib/slices/has_slices.rb +15 -8
  63. data/lib/slices/localized_fields.rb +9 -0
  64. data/lib/slices/page_as_json.rb +7 -1
  65. data/lib/slices/renderer.rb +2 -2
  66. data/lib/slices/tree.rb +12 -3
  67. data/lib/slices/version.rb +1 -1
  68. data/lib/tasks/db.rake +6 -10
  69. data/lib/tasks/seeds.rake +1 -1
  70. data/public/slices/templates/page_main.hbs +1 -1
  71. data/public/slices/templates/page_meta.hbs +2 -2
  72. metadata +56 -84
  73. data/app/views/admin/shared/_custom_links.html.erb +0 -1
  74. data/app/views/admin/shared/_custom_navigation.html.erb +0 -1
  75. data/app/views/layouts/slices/application.html.erb +0 -14
  76. data/lib/ext/file_store_cache.rb +0 -18
  77. data/lib/generators/slices/templates/mongoid.yml +0 -12
  78. data/lib/generators/templates/slices.rb +0 -209
  79. data/lib/rack_utf8_fix.rb +0 -10
  80. data/lib/set_link_renderer.rb +0 -31
  81. data/lib/slices/i18n.rb +0 -6
  82. data/lib/slices/i18n/backend.rb +0 -32
  83. data/lib/slices/will_paginate_mongoid.rb +0 -45
  84. data/lib/standard_tree.rb +0 -193
@@ -1,10 +1,25 @@
1
- class Admin::AdminController < SlicesController
1
+ class Admin::AdminController < ActionController::Base
2
2
  before_filter :authenticate_admin!
3
+ around_filter :set_locale
4
+
5
+ append_view_path(File.join(Rails.root, *%w[app slices]))
3
6
 
4
7
  protected
5
8
 
6
9
  def presenter_class(page_class)
7
10
  Object.const_get("#{page_class.name}Presenter")
8
11
  end
12
+
13
+ private
14
+
15
+ def request_locale
16
+ params[:locale]
17
+ end
18
+
19
+ def set_locale
20
+ I18n.with_locale(request_locale || I18n.default_locale) do
21
+ yield
22
+ end
23
+ end
9
24
  end
10
25
 
@@ -32,7 +32,7 @@ class Admin::AssetsController < Admin::AdminController
32
32
 
33
33
  def destroy
34
34
  asset = Asset.find(params[:id])
35
- asset.soft_destroy!
35
+ asset.destroy
36
36
  respond_to do |format|
37
37
  format.html { redirect_to admin_assets_path }
38
38
  format.json { render json: true }
@@ -4,7 +4,7 @@ class Admin::EntriesController < Admin::AdminController
4
4
  respond_to :json, :html
5
5
 
6
6
  def index
7
- @page = Page.find_by_id! params[:page_id]
7
+ @page = Page.find params[:page_id]
8
8
 
9
9
  respond_to do |format|
10
10
  format.html do
@@ -6,7 +6,6 @@ class Admin::PagesController < Admin::AdminController
6
6
  respond_to :text, only: [:show]
7
7
 
8
8
  before_filter :find_all_slices, only: :update
9
- after_filter :expire_fragments, only: [:update, :create, :destroy]
10
9
 
11
10
  def new
12
11
  @page = Page.new(parent_id: params[:parent_id]) # TODO: change parent_id to path
@@ -27,7 +26,7 @@ class Admin::PagesController < Admin::AdminController
27
26
  end
28
27
 
29
28
  def show
30
- page = Page.find_by_id!(params[:id])
29
+ page = Page.find(params[:id])
31
30
  @page = presenter_class(page.class).new(page)
32
31
  @layout = Layout.new(page.layout)
33
32
 
@@ -44,12 +43,12 @@ class Admin::PagesController < Admin::AdminController
44
43
  render hbs_path
45
44
  end
46
45
  end
47
- rescue Page::NotFound
46
+ rescue Mongoid::Errors::DocumentNotFound
48
47
  redirect_to admin_site_maps_path
49
48
  end
50
49
 
51
50
  def update
52
- @page = Page.find_by_id!(params[:id])
51
+ @page = Page.find(params[:id])
53
52
  if entry_page?
54
53
  params[:page][:set_slices] = params[:page].delete(:slices)
55
54
  end
@@ -64,7 +63,7 @@ class Admin::PagesController < Admin::AdminController
64
63
  end
65
64
 
66
65
  def destroy
67
- @page = Page.find_by_id!(params[:id])
66
+ @page = Page.find(params[:id])
68
67
  @page.destroy
69
68
  respond_to do |format|
70
69
  format.html { redirect_to admin_site_maps_path }
@@ -90,14 +89,6 @@ class Admin::PagesController < Admin::AdminController
90
89
  params.has_key?(:entries)
91
90
  end
92
91
 
93
- def expire_fragments
94
- record = @page
95
- type = record.class.to_s.underscore
96
- id = record.id.to_s
97
- expire_fragment(/.*#{type}.*/)
98
- expire_fragment(/.*#{id}.*/)
99
- end
100
-
101
92
  def find_all_slices
102
93
  Slices::AvailableSlices.all
103
94
  end
@@ -8,8 +8,7 @@ class Admin::SiteMapsController < Admin::AdminController
8
8
 
9
9
  def update
10
10
  SiteMap.rebuild(params[:sitemap])
11
- expire_fragment(/navigation/)
12
- head status: :ok
11
+ head :no_content
13
12
  end
14
13
  end
15
14
 
@@ -8,10 +8,11 @@ class SlicesController < ActionController::Base
8
8
 
9
9
  append_view_path(File.join(Rails.root, *%w[app slices]))
10
10
 
11
+ around_filter :set_locale
11
12
  define_callbacks :render_page, terminator: "response_body"
12
13
 
13
14
  def self.should_raise_exceptions?
14
- ! Rails.env.production?
15
+ Rails.application.config.consider_all_requests_local
15
16
  end
16
17
 
17
18
  protected
@@ -59,5 +60,15 @@ class SlicesController < ActionController::Base
59
60
  def page_layout(page)
60
61
  page.layout
61
62
  end
63
+
64
+ def request_locale
65
+ params[:locale]
66
+ end
67
+
68
+ def set_locale
69
+ I18n.with_locale(request_locale || I18n.default_locale) do
70
+ yield
71
+ end
72
+ end
62
73
  end
63
74
 
@@ -60,4 +60,12 @@ module Admin::AdminHelper
60
60
  @page.try(:name) || "Slices CMS"
61
61
  end
62
62
 
63
+ # Create a link to view the current page on the live site
64
+ #
65
+ # @!visibility private
66
+ def link_to_view_page(page, options = {})
67
+ link_options = { class: 'view-page', target: '_blank' }.merge(options)
68
+ path = page_path(page.path, locale: I18n.locale).gsub('//', '/')
69
+ link_to 'View page on site', path, link_options
70
+ end
63
71
  end
@@ -3,12 +3,46 @@
3
3
  module Admin::SiteMapsHelper
4
4
  include Admin::AdminHelper
5
5
 
6
+ # Returns all pages that should appear in the sitemap
7
+ #
8
+ # @!visibility private
9
+ def sitemap_pages
10
+ @sitemap_pages ||= proper_pages.only(:id, :name, :page_id, :path).group_by(&:page_id)
11
+ end
12
+
13
+ # Returns children for a given page
14
+ #
15
+ # @!visibility private
16
+ def sitemap_children_for(page)
17
+ sitemap_pages.fetch(page.id, []).sort_by(&:position)
18
+ end
19
+
20
+ # Returns criteria for pages that aren't entries
21
+ #
22
+ # @!visibility private
23
+ def proper_pages
24
+ Page.where(:_type.in => proper_types)
25
+ end
26
+
27
+ # Returns page types that aren't for entries
28
+ #
29
+ # @!visibility private
30
+ def proper_types
31
+ Page.distinct(:_type).reject { |type| type.constantize.new.entry? }
32
+ end
33
+
6
34
  # Render a list of pages
7
35
  #
8
36
  # @!visibility private
9
37
  def list_pages(pages)
10
38
  pages.inject(''.html_safe) do |html, page|
11
- html << render(partial: page_partial(page), locals: {page: page} )
39
+ html << render(
40
+ partial: page_partial(page),
41
+ locals: {
42
+ page: page,
43
+ children: sitemap_children_for(page),
44
+ }
45
+ )
12
46
  end
13
47
  end
14
48
 
@@ -93,12 +127,13 @@ module Admin::SiteMapsHelper
93
127
  link_to 'Edit Entry Template', url, class: 'button'
94
128
  end
95
129
 
96
- # Create a link to view the current page on the live site
130
+ # Return HTMl class names for a page
97
131
  #
98
132
  # @!visibility private
99
- def link_to_view_page(page)
100
- return unless page.active?
101
- link_to 'View page on site', page.path,
102
- class: 'view-page', target: '_blank'
133
+ def page_classes(page)
134
+ classes = []
135
+ classes << 'home' if page.home?
136
+ classes << page.class.name.underscore.dasherize
137
+ classes.join ' '
103
138
  end
104
139
  end
@@ -23,7 +23,7 @@ module NavigationHelper
23
23
  # @return [String]
24
24
  #
25
25
  def primary_navigation(id = 'primary_navigation')
26
- cache 'page/navigation/' + id + '/' + @page.id.to_s do
26
+ cache cache_key_for_navigation(id) do
27
27
  benchmark 'Rendered primary_navigation' do
28
28
  nav = navigation(page: Page.home, depth: 1, id: id)
29
29
  safe_concat nav
@@ -48,11 +48,12 @@ module NavigationHelper
48
48
  #
49
49
  # @param [Hash] options
50
50
  # @option options [Integer] :depth (1) Number of levels to render
51
- # @option options [Integer] :id ('secondary_navigation') Id of surrounding UL
51
+ # @option options [String] :id ('secondary_navigation') Id of surrounding UL
52
52
  # @return [String]
53
53
  #
54
54
  def secondary_navigation(options = {})
55
- cache 'page/navigation/secondary/' + @page.id.to_s do
55
+ name = options.fetch('id') { 'secondary_navigation' }
56
+ cache cache_key_for_navigation(name) do
56
57
  benchmark 'Rendered secondary_navigation' do
57
58
  secondary_ancestors = (@page.ancestors[-2] || @page).navigable_children
58
59
  nav = if secondary_ancestors.empty? || @page.home?
@@ -191,5 +192,9 @@ module NavigationHelper
191
192
  def nav_list_identifier(page)
192
193
  page.home? ? 'nav-home' : ('nav' + page.path.gsub('/', '-'))
193
194
  end
195
+
196
+ def cache_key_for_navigation(name)
197
+ ['pages', @page.id, name, Page.last_changed_at]
198
+ end
194
199
  end
195
200
 
@@ -56,8 +56,8 @@ module PagesHelper
56
56
  def edit_in_cms
57
57
  if admin_signed_in?
58
58
  links = []
59
- links << link_to("Edit #{@page.class.to_s.underscore.humanize} in CMS", admin_page_path(@page), target: '_blank')
60
- links << link_to('Edit template in CMS', admin_page_path(@page.parent, entries: 1), target: '_blank') if @page.entry?
59
+ links << link_to("Edit #{@page.class.to_s.underscore.humanize} in CMS", admin_page_path(@page, locale: I18n.locale), target: '_blank')
60
+ links << link_to('Edit template in CMS', admin_page_path(@page.parent, entries: 1, locale: I18n.locale), target: '_blank') if @page.entry?
61
61
  content = links.collect {|l| content_tag(:p, l) }.join.html_safe
62
62
 
63
63
  content_tag(:div, content, id: 'edit_in_cms')
@@ -0,0 +1,26 @@
1
+ module SnippetsHelper
2
+
3
+ # Renders a snippet on the page
4
+ #
5
+ # snippet 'address'
6
+ # # => "54B Downham Road"
7
+ #
8
+ # @param [String] key Snippet key to lookup
9
+ # @return [String]
10
+ #
11
+ def snippet(key = nil)
12
+ Snippet.find_for_key(key)
13
+ end
14
+
15
+ # Renders a localized snippet for the current locale
16
+ #
17
+ # localized_snippet 'address'
18
+ # # => "54B Downham Road"
19
+ #
20
+ # @param [String] key Snippet key to lookup
21
+ # @return [String]
22
+ #
23
+ def localized_snippet(key = nil)
24
+ snippet("#{I18n.locale}.#{key}")
25
+ end
26
+ end
data/app/models/admin.rb CHANGED
@@ -25,8 +25,7 @@ class Admin
25
25
 
26
26
  text_search_in :name, :email
27
27
 
28
- validates_uniqueness_of :email, case_sensitive: false, scope: :site_id
29
- validates_presence_of :email
28
+ validates_uniqueness_of :email, case_sensitive: false
30
29
  validates_presence_of :encrypted_password
31
30
 
32
31
  devise :database_authenticatable, :recoverable, :rememberable,
@@ -36,7 +35,7 @@ class Admin
36
35
  self.super_user == true
37
36
  end
38
37
 
39
- def as_json(options)
38
+ def as_json(options = {})
40
39
  super(options).tap do |json|
41
40
  json[:last_sign_in_at] = last_sign_in_at
42
41
 
data/app/models/asset.rb CHANGED
@@ -15,7 +15,6 @@ class Asset
15
15
  field :file_fingerprint, type: String
16
16
  field :file_dimensions, type: Hash, default: {}
17
17
 
18
- field :destroyed_at, type: Time
19
18
  field :tags, type: String
20
19
  field :page_cache, type: Array, default: []
21
20
 
@@ -34,17 +33,12 @@ class Asset
34
33
  after_file_post_process :store_dimensions, if: :is_image?
35
34
  before_save :reset_file_dimensions!, if: :file_fingerprint_changed?
36
35
  before_save :rename_file
36
+ after_destroy :remove_asset_from_pages
37
37
 
38
38
  has_and_belongs_to_many :pages
39
39
 
40
- index(
41
- [
42
- [ :destroyed_at, Mongo::ASCENDING ],
43
- [ :created_at, Mongo::DESCENDING ]
44
- ]
45
- )
46
- index :file_fingerprint
47
- index :_keywords
40
+ index({ file_fingerprint: 1 })
41
+ index({ _keywords: 1 }, { background: true })
48
42
 
49
43
  def self.make(*args)
50
44
  Slices::Asset::Maker.run(*args)
@@ -59,7 +53,7 @@ class Asset
59
53
  end
60
54
 
61
55
  def self.ordered_active
62
- where(destroyed_at: nil).desc(:created_at)
56
+ desc(:created_at)
63
57
  end
64
58
 
65
59
  def as_json(options = nil)
@@ -139,16 +133,11 @@ class Asset
139
133
  end
140
134
  end
141
135
 
142
- def soft_destroy!
143
- update_attributes!(destroyed_at: Time.now)
144
- end
145
-
146
- def soft_destroyed?
147
- destroyed_at.present?
148
- end
149
-
150
- def soft_restore!
151
- update_attributes!(destroyed_at: nil)
136
+ def remove_asset_from_pages
137
+ pages.each do |page|
138
+ page.remove_asset(self)
139
+ page.save if page.changed?
140
+ end
152
141
  end
153
142
 
154
143
  def name
@@ -1,10 +1,19 @@
1
1
  class Attachment
2
2
  include Mongoid::Document
3
+ include Slices::LocalizedFields
3
4
 
4
5
  belongs_to :asset
5
6
 
7
+ embedded_in :object, polymorphic: true
8
+
6
9
  def as_json *args
7
- attributes.as_json.merge asset: asset.as_json
10
+ result = attributes.symbolize_keys.merge asset: asset.as_json
11
+
12
+ localized_field_names.each do |name|
13
+ result.merge!(name => send(name))
14
+ end
15
+
16
+ result
8
17
  end
9
18
 
10
19
  end
data/app/models/page.rb CHANGED
@@ -9,37 +9,30 @@ class Page
9
9
  include Slices::HasAttachments::PageInstanceMethods
10
10
 
11
11
  DESCRIPTION_DEPRECATION_WARNING = "Page#description is now meta_description. If you are upgrading, run 'rake slices:migrate:meta_description' to update."
12
+ LAST_CHANGED_AT_CACHE_KEY = 'page-last-changed'
12
13
 
13
- field :name
14
+ field :name, localize: Slices::Config.i18n?
14
15
  field :role # only relevant for virtual pages
15
16
  field :active, type: Boolean, default: false
16
17
  field :layout, type: String, default: 'default'
17
- field :meta_description
18
- field :title
18
+ field :meta_description, localize: Slices::Config.i18n?
19
+ field :title, localize: Slices::Config.i18n?
19
20
  field :has_content, type: Boolean, default: false
20
21
 
21
22
  belongs_to :author, class_name: 'Admin'
22
-
23
- index :_keywords, background: true
24
-
25
23
  has_slices :slices
26
24
  has_and_belongs_to_many :assets
27
25
 
28
26
  text_search_in :introduction, :extended, :name
29
27
 
30
- scope :entries, all
28
+ scope :entries, ->{ all }
29
+ scope :virtual, ->{ where(:role.ne => nil).asc(:name) }
31
30
 
32
- def self.available_layouts
33
- Layout.all.map do |human_name, machine_name|
34
- { human_name: human_name, machine_name: machine_name }
35
- end
36
- end
37
-
38
- scope :virtual, where(:role.ne => nil).asc(:name)
39
31
  validates_presence_of :name
40
32
 
41
33
  before_save :update_has_content
42
34
  before_destroy :destroy_children
35
+ after_save :update_last_changed_at
43
36
  after_save :cache_virtual_page
44
37
 
45
38
  class NotFound < RuntimeError; end
@@ -57,10 +50,16 @@ class Page
57
50
  end
58
51
  end
59
52
 
60
- # Virtual pages don't live in the and are not associated with a
61
- # specific URL. Instead, they can be rendered at any path, depending
62
- # on the circumstances (e.g. when a page isn't found, or when an error
63
- # occurs). Consequently they aren't created with a :parent attribute.
53
+ def self.available_layouts
54
+ Layout.all.map do |human_name, machine_name|
55
+ { human_name: human_name, machine_name: machine_name }
56
+ end
57
+ end
58
+
59
+ # Virtual pages are not associated with a specific URL. Instead, they
60
+ # can be rendered at any path, depending on the circumstances (e.g.
61
+ # when a page isn't found, or when an error occurs). Consequently they
62
+ # aren't created with a :parent attribute.
64
63
  def self.make(attributes = {})
65
64
  attributes = attributes.symbolize_keys
66
65
  parent = parent_from_attributes(attributes)
@@ -78,17 +77,27 @@ class Page
78
77
  end
79
78
 
80
79
  def self.find_by_id(id)
80
+ ActiveSupport::Deprecation::warn 'Page.find_by_id is depreciated, please use Page.find instead.'
81
81
  find(id)
82
- rescue BSON::InvalidObjectId, Mongoid::Errors::DocumentNotFound
82
+ rescue Mongoid::Errors::DocumentNotFound
83
83
  nil
84
84
  end
85
85
 
86
86
  def self.find_by_id!(id)
87
+ ActiveSupport::Deprecation::warn 'Page.find_by_id is depreciated, please use Page.find instead.'
87
88
  find_by_id(id) || (raise NotFound)
88
89
  end
89
90
 
90
91
  def self.find_virtual(role)
91
- first(conditions: { role: role })
92
+ find_by(role: role)
93
+ end
94
+
95
+ def update_last_changed_at
96
+ Rails.cache.write(LAST_CHANGED_AT_CACHE_KEY, Time.now.to_i)
97
+ end
98
+
99
+ def self.last_changed_at
100
+ Rails.cache.read(LAST_CHANGED_AT_CACHE_KEY) || 0
92
101
  end
93
102
 
94
103
  def cacheable_virtual_page?
@@ -177,23 +186,25 @@ class Page
177
186
  self.meta_description = value
178
187
  end
179
188
 
180
- private
181
- def self.parent_from_attributes(attributes)
182
- if attributes.has_key?(:parent_path)
183
- Page.find_by_path(attributes.delete(:parent_path))
184
- elsif attributes.has_key?(:parent_id)
185
- Page.find_by_id(attributes.delete(:parent_id))
186
- else
187
- attributes.delete(:parent)
188
- end
189
+ def self.parent_from_attributes(attributes)
190
+ if attributes.has_key?(:parent_path)
191
+ Page.find_by_path(attributes.delete(:parent_path))
192
+ elsif attributes.has_key?(:parent_id)
193
+ Page.find(attributes.delete(:parent_id))
194
+ else
195
+ attributes.delete(:parent)
189
196
  end
197
+ end
198
+ private_class_method :parent_from_attributes
190
199
 
191
- def self.page_exists?(path)
192
- Page.find_by_path(path)
193
- rescue NotFound
194
- false
195
- end
200
+ def self.page_exists?(path)
201
+ Page.find_by_path(path)
202
+ rescue NotFound
203
+ false
204
+ end
205
+ private_class_method :page_exists?
196
206
 
207
+ private
197
208
  def update_has_content
198
209
  self.has_content = slices.any?
199
210
  true # must be true otherwise save will fail