browsercms 3.1.4 → 3.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. data/app/controllers/cms/content_block_controller.rb +2 -2
  2. data/app/controllers/cms/section_nodes_controller.rb +6 -1
  3. data/app/controllers/cms/sections_controller.rb +1 -1
  4. data/app/helpers/cms/application_helper.rb +1 -1
  5. data/app/helpers/cms/content_block_helper.rb +27 -0
  6. data/app/helpers/cms/section_nodes_helper.rb +43 -5
  7. data/app/models/abstract_file_block.rb +16 -1
  8. data/app/models/attachment.rb +17 -35
  9. data/app/models/file_block.rb +0 -12
  10. data/app/models/image_block.rb +0 -12
  11. data/app/models/link.rb +4 -21
  12. data/app/models/page.rb +31 -34
  13. data/app/models/section.rb +82 -44
  14. data/app/models/section_node.rb +39 -24
  15. data/app/models/user.rb +5 -0
  16. data/app/views/cms/blocks/index.html.erb +4 -4
  17. data/app/views/cms/file_blocks/_form.html.erb +1 -1
  18. data/app/views/cms/image_blocks/_form.html.erb +1 -1
  19. data/app/views/cms/section_nodes/_link.html.erb +6 -3
  20. data/app/views/cms/section_nodes/_node.html.erb +11 -1
  21. data/app/views/cms/section_nodes/_page.html.erb +13 -7
  22. data/app/views/cms/section_nodes/_section.html.erb +24 -8
  23. data/app/views/cms/section_nodes/index.html.erb +28 -16
  24. data/app/views/layouts/templates/default.html.erb +17 -0
  25. data/browsercms.gemspec +28 -1413
  26. data/db/migrate/20120117144039_browsercms315.rb +94 -0
  27. data/db/migrate/{20081114172307_load_seed_data.rb → 20121114172307_load_seeds.rb} +8 -1
  28. data/lib/acts_as_list.rb +1 -1
  29. data/lib/browsercms.rb +2 -0
  30. data/lib/cms/addressable.rb +83 -0
  31. data/lib/cms/behaviors/attaching.rb +44 -24
  32. data/lib/cms/behaviors/connecting.rb +2 -1
  33. data/lib/cms/behaviors/publishing.rb +12 -3
  34. data/lib/cms/behaviors/versioning.rb +83 -53
  35. data/lib/cms/content_rendering_support.rb +3 -3
  36. data/lib/cms/error_pages.rb +8 -0
  37. data/lib/cms/init.rb +5 -3
  38. data/lib/cms/version.rb +1 -1
  39. data/templates/blank.rb +2 -0
  40. data/templates/demo.rb +2 -0
  41. data/templates/module.rb +2 -0
  42. data/test/custom_assertions.rb +7 -1
  43. data/test/factories.rb +3 -1
  44. data/test/factories/sitemap_factories.rb +28 -0
  45. data/test/fixtures/connectors.yml +97 -0
  46. data/test/fixtures/content_type_groups.yml +13 -0
  47. data/test/fixtures/content_types.yml +50 -0
  48. data/test/fixtures/dynamic_view_versions.yml +26 -0
  49. data/test/fixtures/dynamic_views.yml +26 -0
  50. data/test/fixtures/group_permissions.yml +16 -0
  51. data/test/fixtures/group_sections.yml +31 -0
  52. data/test/fixtures/group_type_permissions.yml +11 -0
  53. data/test/fixtures/group_types.yml +25 -0
  54. data/test/fixtures/groups.yml +25 -0
  55. data/test/fixtures/html_block_versions.yml +67 -0
  56. data/test/fixtures/html_blocks.yml +63 -0
  57. data/test/fixtures/page_versions.yml +265 -0
  58. data/test/fixtures/pages.yml +85 -0
  59. data/test/fixtures/permissions.yml +28 -0
  60. data/test/fixtures/section_nodes.yml +46 -0
  61. data/test/fixtures/sections.yml +19 -0
  62. data/test/fixtures/sites.yml +9 -0
  63. data/test/fixtures/user_group_memberships.yml +11 -0
  64. data/test/fixtures/users.yml +15 -0
  65. data/test/functional/cms/content_controller_test.rb +6 -1
  66. data/test/functional/cms/file_blocks_controller_test.rb +1 -0
  67. data/test/functional/cms/html_blocks_controller_test.rb +1 -0
  68. data/test/functional/cms/image_blocks_controller_test.rb +39 -32
  69. data/test/functional/cms/section_nodes_controller_test.rb +48 -20
  70. data/test/functional/cms/sections_controller_test.rb +3 -1
  71. data/test/functional/tests/pretend_controller_test.rb +6 -3
  72. data/test/integration/cms/ckeditor_test.rb +5 -2
  73. data/test/integration/sitemap_performance_test.rb +26 -0
  74. data/test/selenium-core/Blank.html +7 -0
  75. data/test/selenium-core/InjectedRemoteRunner.html +8 -0
  76. data/test/selenium-core/RemoteRunner.html +110 -0
  77. data/test/selenium-core/SeleniumLog.html +109 -0
  78. data/test/selenium-core/TestPrompt.html +145 -0
  79. data/test/selenium-core/TestRunner-splash.html +55 -0
  80. data/test/selenium-core/TestRunner.hta +176 -0
  81. data/test/selenium-core/TestRunner.html +176 -0
  82. data/test/selenium-core/domviewer/butmin.gif +0 -0
  83. data/test/selenium-core/domviewer/butplus.gif +0 -0
  84. data/test/selenium-core/domviewer/domviewer.css +298 -0
  85. data/test/selenium-core/domviewer/domviewer.html +16 -0
  86. data/test/selenium-core/domviewer/selenium-domviewer.js +205 -0
  87. data/test/selenium-core/icons/all.png +0 -0
  88. data/test/selenium-core/icons/continue.png +0 -0
  89. data/test/selenium-core/icons/continue_disabled.png +0 -0
  90. data/test/selenium-core/icons/pause.png +0 -0
  91. data/test/selenium-core/icons/pause_disabled.png +0 -0
  92. data/test/selenium-core/icons/selected.png +0 -0
  93. data/test/selenium-core/icons/step.png +0 -0
  94. data/test/selenium-core/icons/step_disabled.png +0 -0
  95. data/test/selenium-core/iedoc-core.xml +1515 -0
  96. data/test/selenium-core/iedoc.xml +1469 -0
  97. data/test/selenium-core/lib/cssQuery/cssQuery-p.js +6 -0
  98. data/test/selenium-core/lib/cssQuery/src/cssQuery-level2.js +142 -0
  99. data/test/selenium-core/lib/cssQuery/src/cssQuery-level3.js +150 -0
  100. data/test/selenium-core/lib/cssQuery/src/cssQuery-standard.js +53 -0
  101. data/test/selenium-core/lib/cssQuery/src/cssQuery.js +356 -0
  102. data/test/selenium-core/lib/prototype.js +2006 -0
  103. data/test/selenium-core/lib/scriptaculous/builder.js +101 -0
  104. data/test/selenium-core/lib/scriptaculous/controls.js +815 -0
  105. data/test/selenium-core/lib/scriptaculous/dragdrop.js +915 -0
  106. data/test/selenium-core/lib/scriptaculous/effects.js +958 -0
  107. data/test/selenium-core/lib/scriptaculous/scriptaculous.js +47 -0
  108. data/test/selenium-core/lib/scriptaculous/slider.js +283 -0
  109. data/test/selenium-core/lib/scriptaculous/unittest.js +383 -0
  110. data/test/selenium-core/scripts/find_matching_child.js +69 -0
  111. data/test/selenium-core/scripts/htmlutils.js +894 -0
  112. data/test/selenium-core/scripts/injection.html +72 -0
  113. data/test/selenium-core/scripts/js2html.js +70 -0
  114. data/test/selenium-core/scripts/narcissus-defs.js +175 -0
  115. data/test/selenium-core/scripts/narcissus-exec.js +1054 -0
  116. data/test/selenium-core/scripts/narcissus-parse.js +1003 -0
  117. data/test/selenium-core/scripts/se2html.js +63 -0
  118. data/test/selenium-core/scripts/selenium-api.js +2409 -0
  119. data/test/selenium-core/scripts/selenium-browserbot.js +2203 -0
  120. data/test/selenium-core/scripts/selenium-browserdetect.js +150 -0
  121. data/test/selenium-core/scripts/selenium-commandhandlers.js +377 -0
  122. data/test/selenium-core/scripts/selenium-executionloop.js +175 -0
  123. data/test/selenium-core/scripts/selenium-logging.js +147 -0
  124. data/test/selenium-core/scripts/selenium-remoterunner.js +571 -0
  125. data/test/selenium-core/scripts/selenium-testrunner.js +1333 -0
  126. data/test/selenium-core/scripts/selenium-version.js +5 -0
  127. data/test/selenium-core/scripts/user-extensions.js +3 -0
  128. data/test/selenium-core/scripts/user-extensions.js.sample +75 -0
  129. data/test/selenium-core/scripts/xmlextras.js +153 -0
  130. data/test/selenium-core/selenium-logo.png +0 -0
  131. data/test/selenium-core/selenium-test.css +43 -0
  132. data/test/selenium-core/selenium.css +299 -0
  133. data/test/selenium-core/xpath/dom.js +428 -0
  134. data/test/selenium-core/xpath/misc.js +252 -0
  135. data/test/selenium-core/xpath/xpath.js +2223 -0
  136. data/test/selenium/_login_as_cmsadmin.rsel +4 -0
  137. data/test/selenium/dashboard.rsel +5 -0
  138. data/test/selenium/html_blocks.rsel +4 -0
  139. data/test/selenium/login/failed_login.rsel +8 -0
  140. data/test/selenium/login/successful_login.rsel +9 -0
  141. data/test/selenium/page_templates.rsel +12 -0
  142. data/test/selenium/pages/edit_properties.rsel +5 -0
  143. data/test/selenium/site/view_home_page.rsel +4 -0
  144. data/test/selenium/sitemap/move_page.rsel +9 -0
  145. data/test/selenium/sitemap/open_section.rsel +6 -0
  146. data/test/selenium/sitemap/select_page.rsel +12 -0
  147. data/test/selenium/sitemap/select_section.rsel +17 -0
  148. data/test/test_helper.rb +30 -12
  149. data/test/unit/behaviors/attaching_test.rb +4 -6
  150. data/test/unit/behaviors/connectable_test.rb +29 -0
  151. data/test/unit/behaviors/publishable_test.rb +40 -9
  152. data/test/unit/behaviors/versioning_test.rb +36 -0
  153. data/test/unit/helpers/menu_helper_test.rb +5 -2
  154. data/test/unit/helpers/page_helper_test.rb +2 -0
  155. data/test/unit/lib/cms/sitemap_test.rb +206 -0
  156. data/test/unit/models/attachment_test.rb +51 -31
  157. data/test/unit/models/file_block_test.rb +74 -55
  158. data/test/unit/models/link_test.rb +44 -0
  159. data/test/unit/models/page_test.rb +290 -224
  160. data/test/unit/models/sections_test.rb +144 -44
  161. data/test/unit/models/user_test.rb +28 -18
  162. metadata +581 -350
  163. data/app/views/cms/section_nodes/_section_node.html.erb +0 -10
  164. data/test/unit/models/section_node_test.rb +0 -92
@@ -0,0 +1,94 @@
1
+ class Browsercms315 < ActiveRecord::Migration
2
+ def self.up
3
+ generate_ancestry_from_section_id
4
+ update_latest_version_cache
5
+
6
+ INDEXES.each do |index|
7
+ add_index *index
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ # This migration is not reversible since it removes the original section_id column.
13
+ end
14
+
15
+ # Add some very commonly used indexes to improve the site performance as the # of pages/content grows (i.e. several thousand pages)
16
+ INDEXES = [
17
+ [:pages, :deleted],
18
+ [:pages, :path],
19
+ [:pages, :version],
20
+ [:page_versions, :page_id],
21
+ [:groups, :code],
22
+ [:groups, :group_type_id],
23
+ [:group_types, :cms_access],
24
+ [:group_sections, :section_id],
25
+ [:group_sections, :group_id],
26
+ [:users, :expires_at],
27
+ [:user_group_memberships, :group_id],
28
+ [:user_group_memberships, :user_id],
29
+ [:group_permissions, :group_id],
30
+ [:group_permissions, :permission_id],
31
+ [:group_permissions, [:group_id, :permission_id]],
32
+ [:section_nodes, :node_type],
33
+ [:section_nodes, :ancestry],
34
+ [:connectors, :page_id],
35
+ [:connectors, :page_version],
36
+ [:html_blocks, :deleted],
37
+ [:html_block_versions, :html_block_id],
38
+ [:html_block_versions, :version],
39
+ [:portlet_attributes, :portlet_id],
40
+ [:portlets, :name],
41
+ [:sections, :path],
42
+ [:redirects, :from_path],
43
+ [:connectors, :connectable_version],
44
+ [:connectors, :connectable_type],
45
+ [:content_types, :content_type_group_id],
46
+ [:content_types, :name],
47
+ [:file_block_versions, :file_block_id],
48
+ [:file_block_versions, :version],
49
+ [:file_blocks, :deleted],
50
+ [:file_blocks, :type],
51
+ [:attachment_versions, :attachment_id],
52
+ [:tasks, :page_id],
53
+ [:tasks, :completed_at],
54
+ [:tasks, :assigned_to_id],
55
+ ]
56
+
57
+ private
58
+
59
+ # v3.1.5 uses Ancestry to manage the parent child relationship between sections and their children.
60
+ # This converts the data from the old section_id to use the ancestry column.
61
+ def self.generate_ancestry_from_section_id
62
+ add_column :section_nodes, :ancestry, :string
63
+ add_column :section_nodes, :temp_parent_id, :integer
64
+
65
+ SectionNode.reset_column_information
66
+ root_section = Section.root.first
67
+ SectionNode.create!(:node => root_section) if root_section
68
+
69
+ all_nodes_but_root = SectionNode.find(:all, :conditions=>["section_id IS NOT NULL"])
70
+ all_nodes_but_root.each do |sn|
71
+ parent_node = SectionNode.find(:first, :conditions => ["node_id = ? and node_type = 'Section'", sn.section_id])
72
+ sn.temp_parent_id = parent_node.id
73
+ sn.save!
74
+ end
75
+ rename_column :section_nodes, :temp_parent_id, :parent_id # Ancestry works off the 'parent_id' column.
76
+
77
+ SectionNode.build_ancestry_from_parent_ids!
78
+ remove_column :section_nodes, :section_id
79
+ remove_column :section_nodes, :parent_id
80
+ SectionNode.reset_column_information
81
+ end
82
+
83
+ # Adds a 'latest_version' pointer to pages and links. Greatly reduces the number of queries the sitemap requires to determine if pages are in draft/published mode
84
+ def self.update_latest_version_cache
85
+ add_column :pages, :latest_version, :integer
86
+ add_column :links, :latest_version, :integer
87
+ Page.all.each do |p|
88
+ p.update_latest_version
89
+ end
90
+ Link.all.each do |link|
91
+ link.update_latest_version
92
+ end
93
+ end
94
+ end
@@ -1,6 +1,13 @@
1
- class LoadSeedData < ActiveRecord::Migration
1
+ # This is a new seed data file (new timestamp and name) so migrations will work in order.
2
+ # It should be able to peacefully coexist next to the previous load_seed_data
3
+ class LoadSeeds < ActiveRecord::Migration
2
4
  extend Cms::DataLoader
3
5
  def self.up
6
+ if User.count > 0
7
+ puts "Database has already been seeded. So skip reloading."
8
+ return
9
+ end
10
+
4
11
  if %w[development test dev local].include?(Rails.env)
5
12
  pwd = "cmsadmin"
6
13
  else
data/lib/acts_as_list.rb CHANGED
@@ -7,7 +7,7 @@ module ActsAsList
7
7
  # The class that has this specified needs to have a +position+ column defined as an integer on
8
8
  # the mapped database table.
9
9
  #
10
- # Todo list example:
10
+ # To Do list example:
11
11
  #
12
12
  # class TodoList < ActiveRecord::Base
13
13
  # has_many :todo_items, :order => "position"
data/lib/browsercms.rb CHANGED
@@ -2,6 +2,8 @@ require 'cms/extensions'
2
2
  require 'cms/init'
3
3
  require 'cms/routes'
4
4
  require 'cms/caching'
5
+ require 'cms/addressable'
6
+ require 'cms/error_pages'
5
7
 
6
8
  #Load libraries that are included with CMS
7
9
  require 'acts_as_list'
@@ -0,0 +1,83 @@
1
+ # Represents any object which exists in a Sitemap.
2
+ #
3
+ # Can have parents (using SectionNodes) and children.
4
+ module Addressable
5
+
6
+ # Returns a list of all Addressable objects that are ancestors to this record.
7
+ # @param [Hash] options
8
+ # @option [Symbol] :include_self If this object should be included in the Array
9
+ # @return [Array<Addressable]
10
+ #
11
+ def ancestors(options={})
12
+ ancestor_nodes = node.ancestors
13
+ ancestors = ancestor_nodes.collect { |node| node.node }
14
+ ancestors << self if options[:include_self]
15
+ ancestors
16
+ end
17
+
18
+ def parent
19
+ @parent if @parent
20
+ node ? node.section : nil
21
+ end
22
+
23
+ def cache_parent(section)
24
+ @parent = section
25
+ end
26
+
27
+ def parent=(sec)
28
+ if node
29
+ node.move_to_end(sec)
30
+ else
31
+ build_section_node(:node => self, :section => sec)
32
+ end
33
+ end
34
+
35
+ # Computes the name of the partial used to render this object in the sitemap.
36
+ def partial_for
37
+ self.class.name.underscore
38
+ end
39
+
40
+ # Pages/Links/Attachments use their parent to determine access
41
+ module LeafNode
42
+ def access_status
43
+ parent.status
44
+ end
45
+ end
46
+
47
+ module NodeAccessors
48
+ def node
49
+ section_node
50
+ end
51
+
52
+ def node=(n)
53
+ self.section_node = n
54
+ end
55
+ end
56
+ # These exist for backwards compatibility to avoid having to change tests.
57
+ # I want to get rid of these in favor of parent and parent_id
58
+ module DeprecatedPageAccessors
59
+ include LeafNode
60
+ include NodeAccessors
61
+
62
+ def build_node(opts)
63
+ build_section_node(opts)
64
+ end
65
+
66
+ def section_id
67
+ section ? section.id : nil
68
+ end
69
+
70
+ def section_id=(sec_id)
71
+ self.section = Section.find(sec_id)
72
+ end
73
+
74
+ def section
75
+ parent
76
+ end
77
+
78
+ def section=(sec)
79
+ self.parent = sec
80
+
81
+ end
82
+ end
83
+ end
@@ -1,34 +1,36 @@
1
1
  module Cms
2
2
  module Behaviors
3
3
  module Attaching
4
- SANITIZATION_REGEXES = [ [/\s/, '_'], [/[&+()]/, '-'], [/[=?!'"{}\[\]#<>%]/, ''] ]
5
-
4
+ SANITIZATION_REGEXES = [[/\s/, '_'], [/[&+()]/, '-'], [/[=?!'"{}\[\]#<>%]/, '']]
5
+
6
6
  def self.included(model_class)
7
7
  model_class.extend(MacroMethods)
8
8
  end
9
- module MacroMethods
9
+
10
+ module MacroMethods
10
11
  def belongs_to_attachment?
11
12
  !!@belongs_to_attachment
12
13
  end
14
+
13
15
  def belongs_to_attachment(options={})
14
16
  @belongs_to_attachment = true
15
17
  include InstanceMethods
16
- before_validation :process_attachment
18
+ before_validation :process_attachment
17
19
  before_save :update_attachment_if_changed
18
20
  after_save :clear_attachment_ivars
19
- belongs_to :attachment, :dependent => :destroy
20
-
21
+ belongs_to :attachment, :dependent => :destroy
22
+
21
23
  validates_each :attachment_file do |record, attr, value|
22
24
  if record.attachment && !record.attachment.valid?
23
25
  record.attachment.errors.each do |err_field, err_value|
24
26
  if err_field.to_sym == :file_path
25
27
  record.errors.add(:attachment_file_path, err_value)
26
- else
28
+ else
27
29
  record.errors.add(:attachment_file, err_value)
28
30
  end
29
- end
31
+ end
30
32
  end
31
- end
33
+ end
32
34
  end
33
35
  end
34
36
  module InstanceMethods
@@ -92,20 +94,30 @@ module Cms
92
94
  unless attachment_section_id.blank?
93
95
  errors.add(:attachment_file, "You must upload a file")
94
96
  return false
95
- end
97
+ end
96
98
  else
97
- build_attachment if attachment.nil?
98
- attachment.temp_file = attachment_file
99
- set_attachment_file_path
99
+ build_attachment if attachment.nil?
100
+ attachment.temp_file = attachment_file
101
+ handle_setting_attachment_path
100
102
  if attachment.file_path.blank?
101
103
  errors.add(:attachment_file_path, "File Name is required for attachment")
102
104
  return false
103
105
  end
104
- set_attachment_section
105
- if attachment.section_id.blank?
106
+ handle_setting_attachment_section
107
+ unless attachment.section
106
108
  errors.add(:attachment_file, "Section is required for attachment")
107
109
  return false
108
110
  end
111
+
112
+ end
113
+ end
114
+
115
+ # Define at :set_attachment_path if you would like to override the way file_path is set
116
+ def handle_setting_attachment_path
117
+ if self.respond_to? :set_attachment_path
118
+ set_attachment_path
119
+ else
120
+ use_default_attachment_path
109
121
  end
110
122
  end
111
123
 
@@ -113,18 +125,26 @@ module Cms
113
125
  @attachment_file = nil
114
126
  @attachment_file_path = nil
115
127
  @attachment_section_id = nil
116
- @attachment_section = nil
128
+ @attachment_section = nil
129
+ end
130
+
131
+ # Implement a :set_attachment_section method if you would like to override the way the section is set
132
+ def handle_setting_attachment_section
133
+ if self.respond_to? :set_attachment_section
134
+ set_attachment_section
135
+ else
136
+ use_default_attachment_section
137
+ end
117
138
  end
118
139
 
119
- # Override this method if you would like to override the way the section is set
120
- def set_attachment_section
140
+ # Default behavior for assigning a section, if a block does not define its own.
141
+ def use_default_attachment_section
121
142
  if !attachment_file.blank?
122
143
  attachment.section = Section.root.first
123
144
  end
124
145
  end
125
146
 
126
- # Override this method if you would like to override the way file_path is set
127
- def set_attachment_file_path
147
+ def use_default_attachment_path
128
148
  if !attachment_file.blank?
129
149
  attachment.file_path = "/attachments/#{File.basename(attachment_file.original_filename).to_s.downcase}"
130
150
  end
@@ -168,16 +188,16 @@ module Cms
168
188
  (published? && live_version?) ? attachment_file_path : "/cms/attachments/#{attachment_id}?version=#{attachment_version}"
169
189
  else
170
190
  nil
171
- end
191
+ end
172
192
  end
173
-
193
+
174
194
  # Forces this record to be changed, even if nothing has changed
175
195
  # This is necessary if just the section.id has changed, for example
176
196
  def dirty!
177
197
  # Seems like a hack, is there a better way?
178
198
  self.updated_at = Time.now
179
- end
180
-
199
+ end
200
+
181
201
  end
182
202
  end
183
203
  end
@@ -39,7 +39,8 @@ module Cms
39
39
  module InstanceMethods
40
40
 
41
41
  def connected_pages
42
- Page.connected_to(self)
42
+ return @connected_pages if @connected_pages
43
+ @connected_pages = Page.connected_to(self)
43
44
  end
44
45
 
45
46
  def connected_page_count
@@ -106,15 +106,24 @@ module Cms
106
106
  end
107
107
 
108
108
  def status
109
- live? ? :published : :draft
110
- end
109
+ return @status if @status
110
+ @status = live? ? :published : :draft
111
+ end
111
112
 
112
113
  def status_name
113
114
  status.to_s.titleize
114
115
  end
115
116
 
116
117
  def live?
117
- self.class.versioned? ? live_version.version == draft.version && published? : true
118
+ if self.class.versioned?
119
+ if (respond_to?(:latest_version) && self.latest_version)
120
+ version == latest_version && published?
121
+ else
122
+ live_version.version == draft.version && published?
123
+ end
124
+ else
125
+ true
126
+ end
118
127
  end
119
128
 
120
129
  end
@@ -4,10 +4,12 @@ module Cms
4
4
  def self.included(model_class)
5
5
  model_class.extend(MacroMethods)
6
6
  end
7
- module MacroMethods
7
+
8
+ module MacroMethods
8
9
  def versioned?
9
10
  !!@is_versioned
10
11
  end
12
+
11
13
  def is_versioned(options={})
12
14
  @is_versioned = true
13
15
 
@@ -17,25 +19,29 @@ module Cms
17
19
  extend ClassMethods
18
20
  include InstanceMethods
19
21
 
20
- has_many :versions, :class_name => version_class_name, :foreign_key => version_foreign_key
22
+ has_many :versions, :class_name => version_class_name, :foreign_key => version_foreign_key
21
23
 
22
24
  before_validation_on_create :initialize_version
23
25
 
24
26
  attr_accessor :revert_to_version
25
27
 
26
28
  #Define the version class
27
- const_set("Version", Class.new(ActiveRecord::Base)).class_eval do
28
- class << self; attr_accessor :versioned_class end
29
+ const_set("Version", Class.new(ActiveRecord::Base)).class_eval do
30
+ class << self;
31
+ attr_accessor :versioned_class
32
+ end
29
33
 
30
34
  def versioned_class
31
35
  self.class.versioned_class
32
36
  end
37
+
33
38
  def versioned_object_id
34
39
  send("#{versioned_class.name.underscore}_id")
35
40
  end
41
+
36
42
  def versioned_object
37
43
  send(versioned_class.name.underscore.to_sym)
38
- end
44
+ end
39
45
  end unless self.const_defined?("Version")
40
46
 
41
47
  version_class.versioned_class = self
@@ -46,33 +52,47 @@ module Cms
46
52
 
47
53
  end
48
54
  end
49
- module ClassMethods
55
+ module ClassMethods
50
56
  def version_class
51
57
  const_get "Version"
52
58
  end
53
59
 
54
60
  def version_class_name
55
61
  "#{name}::Version"
56
- end
62
+ end
57
63
 
58
64
  def version_foreign_key
59
65
  @version_foreign_key
60
66
  end
61
67
 
62
- def version_table_name
68
+ def version_table_name
63
69
  @version_table_name
64
70
  end
65
71
 
66
72
  def versioned_columns
67
- @versioned_columns ||= (version_class.new.attributes.keys -
68
- (%w[id lock_version position version_comment created_at updated_at created_by_id updated_by_id type] + [version_foreign_key]))
69
- end
73
+ @versioned_columns ||= (version_class.new.attributes.keys -
74
+ (%w[id lock_version position version_comment created_at updated_at created_by_id updated_by_id type] + [version_foreign_key]))
75
+ end
70
76
  end
71
77
  module InstanceMethods
72
78
  def initialize_version
73
79
  self.version = 1
74
80
  end
75
81
 
82
+ def after_save
83
+ update_latest_version
84
+ end
85
+
86
+ # Used in migrations and as a callback.
87
+ def update_latest_version
88
+ #Rails 3 could use update_column here instead
89
+ if respond_to? :latest_version
90
+ sql = "UPDATE #{self.class.table_name} SET latest_version = #{draft.version} where id = #{self.id}"
91
+ connection.execute sql
92
+ self.latest_version = draft.version # So we don't need to #reload this object. Probably marks it as dirty though, which could have weird side effects.
93
+ end
94
+ end
95
+
76
96
  def build_new_version
77
97
  # First get the values from the draft
78
98
  attrs = draft_attributes
@@ -83,7 +103,7 @@ module Cms
83
103
  end
84
104
 
85
105
  attrs[:version_comment] = @version_comment || default_version_comment
86
- @version_comment = nil
106
+ @version_comment = nil
87
107
  new_version = versions.build(attrs)
88
108
  new_version.version = new_record? ? 1 : (draft.version.to_i + 1)
89
109
  after_build_new_version(new_version) if respond_to?(:after_build_new_version)
@@ -94,8 +114,8 @@ module Cms
94
114
  # When there is no draft, we'll just copy the attibutes from this object
95
115
  # Otherwise we need to use the draft
96
116
  d = new_record? ? self : draft
97
- self.class.versioned_columns.inject({}){|attrs, col| attrs[col] = d.send(col); attrs }
98
- end
117
+ self.class.versioned_columns.inject({}) { |attrs, col| attrs[col] = d.send(col); attrs }
118
+ end
99
119
 
100
120
  def default_version_comment
101
121
  if new_record?
@@ -108,22 +128,22 @@ module Cms
108
128
  def save(perform_validations=true)
109
129
  transaction do
110
130
  #logger.info "..... Calling valid?"
111
- return false unless !perform_validations || valid?
112
-
131
+ return false unless !perform_validations || valid?
132
+
113
133
  if different_from_last_draft?
114
134
  #logger.info "..... Changes => #{changes.inspect}"
115
135
  else
116
136
  #logger.info "..... No Changes"
117
137
  return true
118
138
  end
119
-
139
+
120
140
  #logger.info "..... Calling before_save"
121
141
  return false if callback(:before_save) == false
122
142
 
123
143
  if new_record?
124
144
  #logger.info "..... Calling before_create"
125
145
  return false if callback(:before_create) == false
126
- else
146
+ else
127
147
  #logger.info "..... Calling before_update"
128
148
  return false if callback(:before_update) == false
129
149
  end
@@ -140,12 +160,12 @@ module Cms
140
160
  #logger.info "..... Calling after_save"
141
161
  callback(:after_save)
142
162
  end
143
-
163
+
144
164
  if @publish_on_save
145
165
  publish
146
166
  @publish_on_save = nil
147
- end
148
- changed_attributes.clear
167
+ end
168
+ changed_attributes.clear
149
169
  end
150
170
  result
151
171
  elsif new_version
@@ -156,11 +176,11 @@ module Cms
156
176
  #logger.info "..... Calling after_update"
157
177
  callback(:after_save)
158
178
  end
159
-
179
+
160
180
  if @publish_on_save
161
181
  publish
162
182
  @publish_on_save = nil
163
- end
183
+ end
164
184
  changed_attributes.clear
165
185
  end
166
186
  result
@@ -174,13 +194,13 @@ module Cms
174
194
  end
175
195
 
176
196
  def draft
177
- versions.first(:order => "version desc")
197
+ versions.first(:order => "version desc")
178
198
  end
179
-
199
+
180
200
  def draft_version?
181
201
  version == draft.version
182
202
  end
183
-
203
+
184
204
  def live_version
185
205
  find_version(self.class.find(id).version)
186
206
  end
@@ -192,38 +212,19 @@ module Cms
192
212
  def current_version
193
213
  find_version(self.version)
194
214
  end
195
-
215
+
196
216
  def find_version(number)
197
- versions.first(:conditions => { :version => number })
217
+ versions.first(:conditions => {:version => number})
198
218
  end
199
219
 
200
220
  def as_of_draft_version
201
- as_of_version(draft.version)
221
+ build_object_from_version(draft)
202
222
  end
203
223
 
204
224
  def as_of_version(version)
205
225
  v = find_version(version)
206
226
  raise ActiveRecord::RecordNotFound.new("version #{version.inspect} does not exist for <#{self.class}:#{id}>") unless v
207
- obj = self.class.new
208
-
209
- (self.class.versioned_columns + [:version, :created_at, :created_by_id, :updated_at, :updated_by_id]).each do |a|
210
- obj.send("#{a}=", v.send(a))
211
- end
212
- obj.id = id
213
- obj.lock_version = lock_version
214
-
215
- # Need to do this so associations can be loaded
216
- obj.instance_variable_set("@new_record", false)
217
-
218
- # Callback to allow us to load other data when an older version is loaded
219
- obj.after_as_of_version if obj.respond_to?(:after_as_of_version)
220
-
221
- # Last but not least, clear the changed attributes
222
- if changed_attrs = obj.send(:changed_attributes)
223
- changed_attrs.clear
224
- end
225
-
226
- obj
227
+ build_object_from_version(v)
227
228
  end
228
229
 
229
230
  def revert
@@ -237,15 +238,15 @@ module Cms
237
238
  raise "Could not find version #{version}" unless revert_to_version
238
239
  (self.class.versioned_columns - ["version"]).each do |a|
239
240
  send("#{a}=", revert_to_version.send(a))
240
- end
241
+ end
241
242
  self.version_comment = "Reverted to version #{version}"
242
- self
243
- end
243
+ self
244
+ end
244
245
 
245
246
  def revert_to(version)
246
247
  revert_to_without_save(version)
247
248
  save
248
- end
249
+ end
249
250
 
250
251
  def version_comment
251
252
  @version_comment
@@ -266,6 +267,35 @@ module Cms
266
267
  return false
267
268
  end
268
269
 
270
+ private
271
+
272
+ # Given a ::Version object of a given type, create an original object from its attributes.
273
+ #
274
+ # @param [Class#name::Version] version (i.e. HtmlBlock::Version)
275
+ # @return [Class#name] i.e. HtmlBlock
276
+ def build_object_from_version(version_of_object)
277
+ obj = self.class.new
278
+
279
+ (self.class.versioned_columns + [:version, :created_at, :created_by_id, :updated_at, :updated_by_id]).each do |a|
280
+ obj.send("#{a}=", version_of_object.send(a))
281
+ end
282
+ obj.id = id
283
+ obj.lock_version = lock_version
284
+
285
+ # Need to do this so associations can be loaded
286
+ obj.instance_variable_set("@new_record", false)
287
+
288
+ # Callback to allow us to load other data when an older version is loaded
289
+ obj.after_as_of_version if obj.respond_to?(:after_as_of_version)
290
+
291
+ # Last but not least, clear the changed attributes
292
+ if changed_attrs = obj.send(:changed_attributes)
293
+ changed_attrs.clear
294
+ end
295
+
296
+ obj
297
+ end
298
+
269
299
  end
270
300
  end
271
301