radiant-concurrent_draft-extension 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/HELP.rdoc +32 -0
  2. data/README.textile +52 -0
  3. data/Rakefile +136 -0
  4. data/VERSION +1 -0
  5. data/app/views/admin/_draft_controls.html.haml +85 -0
  6. data/app/views/admin/_edit_buttons.html.haml +10 -0
  7. data/app/views/admin/layouts/_edit_content.html.haml +5 -0
  8. data/app/views/admin/page_parts/_page_part.html.haml +20 -0
  9. data/app/views/admin/pages/_edit_layout_and_type.html.haml +11 -0
  10. data/app/views/admin/pages/_published_meta.html.haml +4 -0
  11. data/app/views/admin/snippets/_edit_content.html.haml +5 -0
  12. data/app/views/admin/users/_edit_roles.html.haml +26 -0
  13. data/concurrent_draft_extension.rb +30 -0
  14. data/config/initializers/radiant_config.rb +3 -0
  15. data/config/locales/en.yml +11 -0
  16. data/config/routes.rb +9 -0
  17. data/db/migrate/001_update_schemata.rb +29 -0
  18. data/db/migrate/002_create_draft_page_elements.rb +10 -0
  19. data/db/migrate/003_add_publisher_role.rb +9 -0
  20. data/lib/concurrent_draft/admin_controller_extensions.rb +52 -0
  21. data/lib/concurrent_draft/helper_extensions.rb +39 -0
  22. data/lib/concurrent_draft/model_extensions.rb +77 -0
  23. data/lib/concurrent_draft/page_extensions.rb +28 -0
  24. data/lib/concurrent_draft/site_controller_extensions.rb +16 -0
  25. data/lib/concurrent_draft/tags.rb +56 -0
  26. data/lib/tasks/concurrent_draft_extension_tasks.rake +46 -0
  27. data/public/images/admin/cancel.png +0 -0
  28. data/public/images/admin/clock.png +0 -0
  29. data/public/images/admin/page_delete.png +0 -0
  30. data/public/images/admin/page_edit.png +0 -0
  31. data/public/images/admin/page_refresh.png +0 -0
  32. data/public/images/admin/tick.png +0 -0
  33. data/public/javascripts/admin/concurrent_draft.js +72 -0
  34. data/public/stylesheets/admin/concurrent_draft.css +74 -0
  35. data/spec/controllers/admin_controller_extensions_spec.rb +184 -0
  36. data/spec/controllers/site_controller_extensions_spec.rb +31 -0
  37. data/spec/matchers/concurrent_draft_matcher.rb +11 -0
  38. data/spec/models/model_extensions_spec.rb +114 -0
  39. data/spec/models/page_extensions_spec.rb +56 -0
  40. data/spec/models/tags_spec.rb +43 -0
  41. data/spec/spec.opts +6 -0
  42. data/spec/spec_helper.rb +38 -0
  43. data/test/helpers/caching_test_helper.rb +42 -0
  44. data/test/helpers/page_part_test_helper.rb +49 -0
  45. data/test/helpers/page_test_helper.rb +77 -0
  46. data/vendor/plugins/12_hour_time/CHANGELOG +2 -0
  47. data/vendor/plugins/12_hour_time/README +16 -0
  48. data/vendor/plugins/12_hour_time/Rakefile +22 -0
  49. data/vendor/plugins/12_hour_time/init.rb +1 -0
  50. data/vendor/plugins/12_hour_time/lib/12_hour_time.rb +78 -0
  51. data/vendor/plugins/12_hour_time/test/12_hour_time_test.rb +52 -0
  52. data/vendor/plugins/12_hour_time/test/test_helper.rb +3 -0
  53. metadata +143 -0
data/config/routes.rb ADDED
@@ -0,0 +1,9 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ map.page_schedule_draft_promotion 'admin/pages/schedule_draft_promotion/:id',
3
+ :controller => 'admin/pages', :action => 'schedule_draft_promotion'
4
+ map.snippet_schedule_draft_promotion 'admin/snippet/schedule_draft_promotion/:id',
5
+ :controller => 'admin/snippets', :action => 'schedule_draft_promotion'
6
+ map.layout_schedule_draft_promotion 'admin/layout/schedule_draft_promotion/:id',
7
+ :controller => 'admin/layouts', :action => 'schedule_draft_promotion'
8
+ map.page_unpublish 'admin/pages/unpublish/:id', :controller => 'admin/pages', :action => 'unpublish'
9
+ end
@@ -0,0 +1,29 @@
1
+ class UpdateSchemata < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :pages, :draft_promotion_scheduled_at, :datetime
4
+ add_column :pages, :draft_promoted_at, :datetime
5
+ add_column :page_parts, :draft_content, :text
6
+ add_column :snippets, :draft_promotion_scheduled_at, :datetime
7
+ add_column :snippets, :draft_promoted_at, :datetime
8
+ add_column :snippets, :draft_content, :text
9
+ add_column :layouts, :draft_promotion_scheduled_at, :datetime
10
+ add_column :layouts, :draft_promoted_at, :datetime
11
+ add_column :layouts, :draft_content, :text
12
+ Page.reset_column_information
13
+ PagePart.reset_column_information
14
+ Snippet.reset_column_information
15
+ Layout.reset_column_information
16
+ end
17
+
18
+ def self.down
19
+ remove_column :page_parts, :draft_content
20
+ remove_column :pages, :draft_promotion_scheduled_at
21
+ remove_column :pages,:draft_promoted_at
22
+ remove_column :snippets, :draft_content
23
+ remove_column :snippets, :draft_promotion_scheduled_at
24
+ remove_column :snippets, :draft_promoted_at
25
+ remove_column :layouts, :draft_content
26
+ remove_column :layouts, :draft_promotion_scheduled_at
27
+ remove_column :layouts, :draft_promoted_at
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ class CreateDraftPageElements < ActiveRecord::Migration
2
+ def self.up
3
+ PagePart.update_all("draft_content = content")
4
+ Snippet.update_all("draft_content = content")
5
+ Layout.update_all("draft_content = content")
6
+ end
7
+
8
+ def self.down
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ class AddPublisherRole < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :users, :publisher, :boolean, :default => false
4
+ end
5
+
6
+ def self.down
7
+ remove_column :users, :publisher
8
+ end
9
+ end
@@ -0,0 +1,52 @@
1
+ module ConcurrentDraft::AdminControllerExtensions
2
+ def self.included(base)
3
+ base.class_eval do
4
+ helper_method :model
5
+ helper_method :model_class
6
+ public :model, :model_class
7
+ only_allow_access_to :schedule_draft_promotion, :unpublish,
8
+ :when => [:publisher, :admin],
9
+ :denied_message => "You must have publisher privileges to execute this action.",
10
+ :denied_url => {:action => 'edit'}
11
+ after_filter :check_for_promote_now, :only => [:create, :update]
12
+ end
13
+ end
14
+
15
+ def authorized_user?
16
+ (current_user.publisher? || current_user.admin?)
17
+ end
18
+
19
+ def check_for_promote_now
20
+ if params[:promote] && authorized_user?
21
+ model.promote_draft!
22
+ flash[:notice] = "The existing draft #{model_class.to_s.downcase} has been saved and promoted, and is now live."
23
+ flash.keep
24
+ end
25
+ end
26
+
27
+ def schedule_draft_promotion
28
+ self.model = model_class.find(params[:id])
29
+ case params[:commit]
30
+ when model_class.promote_now_text
31
+ model.promote_draft!
32
+ flash[:notice] = "The existing draft #{model_class.to_s.downcase} has been promoted and is now live."
33
+ when model_class.schedule_promotion_text
34
+ if model.update_attributes(params[model.class.name.underscore.intern])
35
+ flash[:notice] = "The draft #{model_class.to_s.downcase} will be promoted on #{model.draft_promotion_scheduled_at.to_s(:long_civilian)}."
36
+ else
37
+ flash[:error] = model.errors.full_messages.to_sentence
38
+ end
39
+ when model_class.cancel_promotion_text
40
+ model.cancel_promotion!
41
+ flash[:notice] = "The scheduled draft #{model_class.to_s.downcase} promotion has been cancelled."
42
+ end
43
+ redirect_to :action => "edit"
44
+ end
45
+
46
+ def unpublish
47
+ self.model = model_class.find(params[:id])
48
+ model.unpublish!
49
+ flash[:notice] = "#{model_class} has been unpublished and reset to draft mode -- no draft promotion scheduled."
50
+ redirect_to :action => "edit"
51
+ end
52
+ end
@@ -0,0 +1,39 @@
1
+ module ConcurrentDraft::HelperExtensions
2
+ def updated_stamp(model)
3
+ unless model.new_record?
4
+ updated_by = (model.updated_by || model.created_by) if model.respond_to?(:updated_by)
5
+ login = updated_by ? updated_by.login : nil
6
+ time = (model.updated_at || model.created_at)
7
+ promoted_at = model.draft_promoted_at if model.respond_to?(:draft_promoted_at)
8
+ html = %{<p style="clear: left"><small>}
9
+ if login or time
10
+ html << 'Last updated '
11
+ html << %{by #{login} } if login
12
+ html << %{at #{ timestamp(time) }} if time
13
+ html << '. '
14
+ end
15
+ if promoted_at
16
+ html << %{Last promoted at #{ timestamp(promoted_at) }.}
17
+ end
18
+ html << %{</small></p>}
19
+ html
20
+ else
21
+ %{<p class="clear">&nbsp;</p>}
22
+ end
23
+ end
24
+
25
+ def save_model_button(_model)
26
+ label = _model.new_record? ? "Create" : "Save"
27
+ submit_tag "#{label} and Exit", :class => 'button'
28
+ end
29
+
30
+ def save_model_and_continue_editing_button(_model)
31
+ label = _model.new_record? ? "Create" : "Save"
32
+ submit_tag label, :name => 'continue', :class => 'button'
33
+ end
34
+
35
+ def save_model_and_promote_button(_model)
36
+ label = _model.new_record? ? "Create" : "Save"
37
+ submit_tag "#{label} and Promote Now", :name => 'promote', :class => 'button'
38
+ end
39
+ end
@@ -0,0 +1,77 @@
1
+ module ConcurrentDraft::ModelExtensions
2
+
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.class_eval do
6
+ validate :promotion_date_in_future
7
+ if instance_methods.include?('after_initialize')
8
+ alias_method_chain :after_initialize, :drafts
9
+ else
10
+ def after_initialize
11
+ promote_on_load
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ def promote_now_text; "Promote Now" end
19
+ def schedule_promotion_text; "Schedule for promotion on -->" end
20
+ def cancel_promotion_text; "Cancel scheduled promotion" end
21
+ end
22
+
23
+ def display_name
24
+ if self.class.to_s == 'Page'
25
+ self.title
26
+ else
27
+ self.name
28
+ end
29
+ end
30
+
31
+ def promotion_date_in_future
32
+ if respond_to?(:draft_promotion_scheduled_at) && !draft_promotion_scheduled_at.blank? && draft_promotion_scheduled_at < Time.now
33
+ errors.add :draft_promotion_scheduled_at, "may not be in the past"
34
+ end
35
+ end
36
+
37
+ def promote_on_load
38
+ promote_draft! if respond_to?(:draft_promotion_scheduled_at) && draft_should_be_promoted?
39
+ end
40
+
41
+ def after_initialize_with_drafts
42
+ promote_on_load
43
+ after_initialize_without_drafts
44
+ end
45
+
46
+ def has_draft_promotion_scheduled?
47
+ !draft_promotion_scheduled_at.blank?
48
+ end
49
+
50
+ def has_draft_promoted?
51
+ !draft_promoted_at.blank? && draft_promoted_at <= Time.now
52
+ end
53
+
54
+ def cancel_promotion!
55
+ update_attribute("draft_promotion_scheduled_at", nil)
56
+ end
57
+
58
+ def draft_should_be_promoted?
59
+ has_draft_promotion_scheduled? && draft_promotion_scheduled_at <= Time.now
60
+ end
61
+
62
+ def promote_draft!
63
+ update_attributes("content" => draft_content) if respond_to?(:content) && respond_to?(:draft_content)
64
+ update_attributes("draft_promotion_scheduled_at" => nil, "draft_promoted_at" => Time.now) if respond_to?(:draft_promoted_at)
65
+ Radiant::Cache.clear if defined?(Radiant::Cache)
66
+ end
67
+
68
+ def unpublish!
69
+ update_attributes("content" => nil) if respond_to?(:content) && respond_to?(:draft_content)
70
+ update_attributes("draft_promotion_scheduled_at" => nil, "draft_promoted_at" => nil) if respond_to?(:draft_promoted_at)
71
+ end
72
+
73
+ def publishable?
74
+ has_attribute?("published_at") && has_attribute?("status_id")
75
+ end
76
+
77
+ end
@@ -0,0 +1,28 @@
1
+ module ConcurrentDraft::PageExtensions
2
+
3
+ def self.included(base)
4
+ base.class_eval do
5
+ alias_method_chain :parse_object, :drafts
6
+ end
7
+ end
8
+
9
+ def promote_draft!
10
+ parts.reload.each(&:promote_draft!)
11
+ update_attribute('status_id', Status[:published].id)
12
+ super
13
+ end
14
+
15
+ def unpublish!
16
+ parts.each(&:unpublish!)
17
+ update_attributes('published_at' => nil, 'status_id' => Status[:draft].id)
18
+ super
19
+ end
20
+
21
+ private
22
+
23
+ def parse_object_with_drafts(object)
24
+ object.content = object.draft_content unless published?
25
+ parse_object_without_drafts(object)
26
+ end
27
+
28
+ end
@@ -0,0 +1,16 @@
1
+ module ConcurrentDraft::SiteControllerExtensions
2
+ def self.included(base)
3
+ base.class_eval do
4
+ before_filter :publish_if_scheduled, :only => :show_page
5
+ end
6
+ end
7
+
8
+ def publish_if_scheduled
9
+ url = Array === params[:url] ? params[:url].join('/') : params[:url]
10
+ page = Page.find_by_url(url, false)
11
+ if page && !page.published? && page.draft_should_be_promoted?
12
+ page.update_attribute('status_id', Status[:published].id)
13
+ end
14
+ true
15
+ end
16
+ end
@@ -0,0 +1,56 @@
1
+ module ConcurrentDraft::Tags
2
+ include Radiant::Taggable
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ %w{content snippet}.each do |t|
7
+ alias_method "tag:old_#{t}", "tag:#{t}"
8
+ alias_method "tag:#{t}", "tag:concurrent_draft_#{t}"
9
+ end
10
+ end
11
+ end
12
+
13
+ tag 'concurrent_draft_content' do |tag|
14
+ page = tag.locals.page
15
+ part_name = tag_part_name(tag)
16
+ boolean_attr = proc do |attribute_name, default|
17
+ attribute = (tag.attr[attribute_name] || default).to_s
18
+ raise TagError.new(%{`#{attribute_name}' attribute of `content' tag must be set to either "true" or "false"}) unless attribute =~ /true|false/i
19
+ (attribute.downcase == 'true') ? true : false
20
+ end
21
+ inherit = boolean_attr['inherit', false]
22
+ part_page = page
23
+ if inherit
24
+ while (part_page.part(part_name).nil? and (not part_page.parent.nil?)) do
25
+ part_page = part_page.parent
26
+ end
27
+ end
28
+ contextual = boolean_attr['contextual', true]
29
+ part = part_page.part(part_name)
30
+ ### CONCURRENT DRAFTS CHANGE ###
31
+ # Show the draft content on the dev site #
32
+ part.content = part.draft_content if part && dev?(tag.globals.page.request)
33
+ ### END CONCURRENT DRAFTS CHANGE ###
34
+ tag.locals.page = part_page unless contextual
35
+ tag.globals.page.render_snippet(part) unless part.nil?
36
+ end
37
+
38
+ tag 'concurrent_draft_snippet' do |tag|
39
+ if name = tag.attr['name']
40
+ if snippet = Snippet.find_by_name(name.strip)
41
+ tag.locals.yield = tag.expand if tag.double?
42
+ ### CONCURRENT DRAFTS CHANGE ###
43
+ # Show the draft content on the dev site #
44
+ # Promote the snippet if it needs to be #
45
+ snippet.promote_draft! if snippet.draft_should_be_promoted?
46
+ snippet.content = snippet.draft_content if dev?(tag.globals.page.request)
47
+ ### END CONCURRENT DRAFTS CHANGE ###
48
+ tag.globals.page.render_snippet(snippet)
49
+ else
50
+ raise StandardTags::TagError.new("snippet not found: #{name}")
51
+ end
52
+ else
53
+ raise StandardTags::TagError.new("`snippet' tag must contain `name' attribute")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,46 @@
1
+ namespace :radiant do
2
+ namespace :extensions do
3
+ namespace :concurrent_draft do
4
+
5
+ desc "Runs the migration of the ConcurrentDraft extension"
6
+ task :migrate => :environment do
7
+ require 'radiant/extension_migrator'
8
+ if ENV["VERSION"]
9
+ ConcurrentDraftExtension.migrator.migrate(ENV["VERSION"].to_i)
10
+ else
11
+ ConcurrentDraftExtension.migrator.migrate
12
+ end
13
+ end
14
+
15
+ desc "Copies public assets of the Concurrent Draft to the instance public/ directory."
16
+ task :update => :environment do
17
+ is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
18
+ Dir[ConcurrentDraftExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
19
+ path = file.sub(ConcurrentDraftExtension.root, '')
20
+ directory = File.dirname(path)
21
+ puts "Copying #{path}..."
22
+ mkdir_p RAILS_ROOT + directory
23
+ cp file, RAILS_ROOT + path
24
+ end
25
+ end
26
+
27
+ desc "Create drafts for all snippets"
28
+ task :create_draft_snippets => :environment do
29
+ print 'copying content to draft_content for all snippets...'
30
+ Snippet.update_all('draft_content = content')
31
+ puts 'done.'
32
+ end
33
+
34
+ desc "Promote all drafts of all assets"
35
+ task :promote_all => :environment do
36
+ [Page, Snippet, Layout].each do |asset|
37
+ print "Promoting all #{asset.to_s.pluralize}..."
38
+ @user = User.find(:first) ## Admin user
39
+ asset.find(:all).each(&:promote_draft!)
40
+ asset.update_all("updated_by_id = #{@user.id}")
41
+ puts "done."
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,72 @@
1
+ Event.addBehavior({'a.popup': Popup.TriggerBehavior()});
2
+
3
+ var Draft = {};
4
+
5
+ Draft.ControlBox = Behavior.create({
6
+ initialize: function(){
7
+ document.observe('click', Element.removeClassName.curry(this.element, 'active'));
8
+ },
9
+ onclick: function(e){
10
+ e.stop();
11
+ this.element.addClassName('active');
12
+ }
13
+ });
14
+
15
+ Draft.RevertLink = Behavior.create({
16
+ initialize: function(){
17
+ this.popup = $('revert-draft-popup');
18
+ },
19
+ onclick: function(e){
20
+ e.stop();
21
+ this.element.up('.active').removeClassName('active');
22
+ $$('input[name*="[content]"]').each(this.copyData, this);
23
+ center(this.popup);
24
+ this.popup.show();
25
+ },
26
+ copyData: function(input){
27
+ // var draft_id = input.name.gsub(/content/, 'draft_content').gsub(/[\[\]]+/, '_').sub(/\_*$/, '');
28
+ var draft_id = input.id.gsub(/content/, 'draft_content');
29
+ var draft = $(draft_id);
30
+ // The following was modified to accommodate templates extension
31
+ if (draft) {
32
+ switch(draft.type){
33
+ case 'checkbox':
34
+ draft.checked = (input.value == draft.value);
35
+ break;
36
+ default:
37
+ draft.value = input.value;
38
+ break;
39
+ }
40
+ draft.fire('draft:reverted');
41
+ } else {
42
+ // for true/false radio buttons in templates extension
43
+ var draft_true = document.getElementById(draft_id + '_true');
44
+ var draft_false = document.getElementById(draft_id + '_false');
45
+ if (draft_true && draft_false && draft_true.type == 'radio' && draft_false.type == 'radio') {
46
+ if (input.value == 'true') {
47
+ draft_true.checked = true;
48
+ } else if (input.value == 'false') {
49
+ draft_false.checked = true;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ });
55
+
56
+ Draft.ScheduleLink = Behavior.create({
57
+ onclick: function(e){
58
+ e.stop();
59
+ this.element.up('.active').removeClassName('.active');
60
+ var element = $(this.element.href.split('#')[1]);
61
+ center(element);
62
+ element.show();
63
+ element.down('form').focus();
64
+ }
65
+ });
66
+
67
+
68
+ Event.addBehavior({
69
+ '#draft-controls': Draft.ControlBox,
70
+ '#draft-controls li.revert a': Draft.RevertLink,
71
+ '#draft-controls li.schedule_draft a': Draft.ScheduleLink
72
+ });