radiant-scheduler-extension 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # Radiant Scheduler Extension
2
+
3
+ [![Build Status](https://secure.travis-ci.org/radiant/radiant-scheduler-extension.png?branch=master)](http://travis-ci.org/radiant/radiant-scheduler-extension)
4
+
5
+ Created by: Sean Cribbs, September 2007
6
+
7
+ The Scheduler extension creates publish and expiration dates (or
8
+ appearance and disappearance) that are configurable by the content
9
+ editor. These may be set in the "meta" area of the page editing
10
+ screen, and include a calendar-style date picker, thanks to Dan Webb's
11
+ wonderful LowPro library (and his date_selector behavior). These
12
+ dates only affect what may be found from the 'live' site. All pages
13
+ are accessible when in 'dev' or 'preview' mode.
14
+
15
+ ## Acknowledgments
16
+
17
+ Thanks to Digital Pulp, Inc. for funding the initial development of this
18
+ extension as part of the Redken.com project.
data/Rakefile ADDED
@@ -0,0 +1,109 @@
1
+ # Determine where the RSpec plugin is by loading the boot
2
+ unless defined? RADIANT_ROOT
3
+ ENV["RAILS_ENV"] = "test"
4
+ case
5
+ when ENV["RADIANT_ENV_FILE"]
6
+ require File.dirname(ENV["RADIANT_ENV_FILE"]) + "/boot"
7
+ when File.dirname(__FILE__) =~ %r{vendor/radiant/vendor/extensions}
8
+ require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../")}/config/boot"
9
+ else
10
+ require "#{File.expand_path(File.dirname(__FILE__) + "/../../../")}/config/boot"
11
+ end
12
+ end
13
+
14
+ require 'rake'
15
+ require 'rdoc/task'
16
+ require 'rake/testtask'
17
+
18
+ rspec_base = File.expand_path(RADIANT_ROOT + '/vendor/plugins/rspec/lib')
19
+ $LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
20
+ require 'spec/rake/spectask'
21
+ require 'cucumber'
22
+ require 'cucumber/rake/task'
23
+
24
+ # Cleanup the RADIANT_ROOT constant so specs will load the environment
25
+ Object.send(:remove_const, :RADIANT_ROOT)
26
+
27
+ extension_root = File.expand_path(File.dirname(__FILE__))
28
+
29
+ task :default => [:spec, :features]
30
+ task :stats => "spec:statsetup"
31
+
32
+ desc "Run all specs in spec directory"
33
+ Spec::Rake::SpecTask.new(:spec) do |t|
34
+ t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
35
+ t.spec_files = FileList['spec/**/*_spec.rb']
36
+ end
37
+
38
+ task :features => 'spec:integration'
39
+
40
+ namespace :spec do
41
+ desc "Run all specs in spec directory with RCov"
42
+ Spec::Rake::SpecTask.new(:rcov) do |t|
43
+ t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
44
+ t.spec_files = FileList['spec/**/*_spec.rb']
45
+ t.rcov = true
46
+ t.rcov_opts = ['--exclude', 'spec', '--rails']
47
+ end
48
+
49
+ desc "Print Specdoc for all specs"
50
+ Spec::Rake::SpecTask.new(:doc) do |t|
51
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
52
+ t.spec_files = FileList['spec/**/*_spec.rb']
53
+ end
54
+
55
+ [:models, :controllers, :views, :helpers].each do |sub|
56
+ desc "Run the specs under spec/#{sub}"
57
+ Spec::Rake::SpecTask.new(sub) do |t|
58
+ t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
59
+ t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"]
60
+ end
61
+ end
62
+
63
+ desc "Run the Cucumber features"
64
+ Cucumber::Rake::Task.new(:integration) do |t|
65
+ t.fork = true
66
+ t.cucumber_opts = ['--format', (ENV['CUCUMBER_FORMAT'] || 'pretty')]
67
+ # t.feature_pattern = "#{extension_root}/features/**/*.feature"
68
+ t.profile = "default"
69
+ end
70
+
71
+ # Setup specs for stats
72
+ task :statsetup do
73
+ require 'code_statistics'
74
+ ::STATS_DIRECTORIES << %w(Model\ specs spec/models)
75
+ ::STATS_DIRECTORIES << %w(View\ specs spec/views)
76
+ ::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers)
77
+ ::STATS_DIRECTORIES << %w(Helper\ specs spec/views)
78
+ ::CodeStatistics::TEST_TYPES << "Model specs"
79
+ ::CodeStatistics::TEST_TYPES << "View specs"
80
+ ::CodeStatistics::TEST_TYPES << "Controller specs"
81
+ ::CodeStatistics::TEST_TYPES << "Helper specs"
82
+ ::STATS_DIRECTORIES.delete_if {|a| a[0] =~ /test/}
83
+ end
84
+
85
+ namespace :db do
86
+ namespace :fixtures do
87
+ desc "Load fixtures (from spec/fixtures) into the current environment's database. Load specific fixtures using FIXTURES=x,y"
88
+ task :load => :environment do
89
+ require 'active_record/fixtures'
90
+ ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
91
+ (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'spec', 'fixtures', '*.{yml,csv}'))).each do |fixture_file|
92
+ Fixtures.create_fixtures('spec/fixtures', File.basename(fixture_file, '.*'))
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ desc 'Generate documentation for the scheduler extension.'
100
+ RDoc::Task.new(:rdoc) do |rdoc|
101
+ rdoc.rdoc_dir = 'rdoc'
102
+ rdoc.title = 'SchedulerExtension'
103
+ rdoc.options << '--line-numbers' << '--inline-source'
104
+ rdoc.rdoc_files.include('README')
105
+ rdoc.rdoc_files.include('lib/**/*.rb')
106
+ end
107
+
108
+ # Load any custom rakefiles for extension
109
+ Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f }
@@ -0,0 +1,48 @@
1
+ %tr
2
+ %td{:class=>"label"} Scheduler
3
+ %td{:class => "field", :style => "text-align: center"}
4
+ %label{:for => "page_appears_on", :style => "padding: 0 10px;"} Publish date
5
+ = text_field "page", "appears_on", :size => 15
6
+ %label{:for => "page_expires_on", :style => "padding: 0 10px;"} Expiration date
7
+ = text_field "page", "expires_on", :size => 15
8
+ - include_javascript 'lowpro'
9
+ - include_javascript 'date_selector'
10
+ - content_for :page_scripts do
11
+ :plain
12
+ Event.addBehavior({
13
+ '#page_appears_on, #page_expires_on': DateSelector
14
+ });
15
+ - content_for :page_css do
16
+ :sass
17
+ #page_expires_on, #page_appears_on
18
+ :position relative
19
+ table.calendar
20
+ :background white
21
+ :border 1px solid black
22
+ td
23
+ :padding 2px
24
+ :border 1px solid #eee
25
+ :background white
26
+ :text-align right
27
+ &.today
28
+ a
29
+ color: red
30
+ &.selected
31
+ :background #ddd
32
+ :font-weight bold
33
+ :border-color black
34
+ :border-width 2px
35
+ .back, .forward
36
+ :background #ddd
37
+ th
38
+ :background #eee
39
+ a
40
+ :text-decoration none
41
+ :color black
42
+ tbody
43
+ tr:last-child td
44
+ :border-bottom 1px solid black
45
+ td:first-child
46
+ :border-left 1px solid black
47
+ td:last-child
48
+ :border-right 1px solid black
data/cucumber.yml ADDED
@@ -0,0 +1 @@
1
+ default: --format progress features --tags ~@proposed,~@in_progress
@@ -0,0 +1,11 @@
1
+ class AddScheduleFields < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :pages, :appears_on, :date
4
+ add_column :pages, :expires_on, :date
5
+ end
6
+
7
+ def self.down
8
+ remove_column :pages, :appears_on
9
+ remove_column :pages, :expires_on
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ # Sets up the Rails environment for Cucumber
2
+ ENV["RAILS_ENV"] = "test"
3
+ # Extension root
4
+ extension_env = File.expand_path(File.dirname(__FILE__) + '/../../../../../config/environment')
5
+ require extension_env+'.rb'
6
+
7
+ Dir.glob(File.join(RADIANT_ROOT, "features", "**", "*.rb")).each {|step| require step}
8
+
9
+ Cucumber::Rails::World.class_eval do
10
+ include Dataset
11
+ datasets_directory "#{RADIANT_ROOT}/spec/datasets"
12
+ Dataset::Resolver.default = Dataset::DirectoryResolver.new("#{RADIANT_ROOT}/spec/datasets", File.dirname(__FILE__) + '/../../spec/datasets', File.dirname(__FILE__) + '/../datasets')
13
+ self.datasets_database_dump_path = "#{Rails.root}/tmp/dataset"
14
+
15
+ # dataset :scheduler
16
+ end
@@ -0,0 +1,14 @@
1
+ def path_to(page_name)
2
+ case page_name
3
+
4
+ when /the homepage/i
5
+ root_path
6
+
7
+ when /login/i
8
+ login_path
9
+ # Add more page name => path mappings here
10
+
11
+ else
12
+ raise "Can't find mapping from \"#{page_name}\" to a path."
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module RadiantSchedulerExtension
2
+ VERSION = "1.0.0"
3
+ SUMMARY = "Scheduler extension for Radiant CMS"
4
+ DESCRIPTION = "Allows setting of appearance and expiration dates for pages."
5
+ URL = "https://github.com/radiant/radiant-scheduler-extension"
6
+ AUTHORS = ["Sean Cribbs"]
7
+ EMAIL = ["sean@basho.com"]
8
+ end
@@ -0,0 +1,10 @@
1
+ module Scheduler::ControllerExtensions
2
+ def self.included(base)
3
+ base.class_eval { around_filter :filter_with_scheduler, :if => :live? }
4
+ end
5
+
6
+ protected
7
+ def filter_with_scheduler
8
+ Page.with_published_only { yield }
9
+ end
10
+ end
@@ -0,0 +1,58 @@
1
+ module Scheduler::PageExtensions
2
+ include Radiant::Taggable
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ class << base
7
+ alias_method_chain :find_by_path, :scheduling
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def find_by_path_with_scheduling(url, live=true)
13
+ if live
14
+ self.with_published_only do
15
+ find_by_path_without_scheduling(url, live)
16
+ end
17
+ else
18
+ find_by_path_without_scheduling(url, live)
19
+ end
20
+ end
21
+
22
+ def with_published_only
23
+ if @with_published
24
+ yield
25
+ else
26
+ @with_published = true
27
+ result = with_scope(:find => {:conditions => ["(appears_on IS NULL OR appears_on <= ?) AND (expires_on IS NULL OR expires_on > ?)", Date.today, Date.today]}) do
28
+ yield
29
+ end
30
+ @with_published = false
31
+ result
32
+ end
33
+ end
34
+ end
35
+
36
+ def visible?
37
+ published? && appeared? && !expired?
38
+ end
39
+
40
+ def appeared?
41
+ appears_on.blank? || appears_on <= Date.today
42
+ end
43
+
44
+ def expired?
45
+ !expires_on.blank? && self.expires_on < Date.today
46
+ end
47
+
48
+ tag 'children' do |tag|
49
+ tag.locals.children = tag.locals.page.children
50
+ if dev?(tag.globals.page.request)
51
+ tag.expand
52
+ else
53
+ Page.with_published_only do
54
+ tag.expand
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,28 @@
1
+ namespace :radiant do
2
+ namespace :extensions do
3
+ namespace :scheduler do
4
+
5
+ desc "Runs the migration of the Scheduler extension"
6
+ task :migrate => :environment do
7
+ require 'radiant/extension_migrator'
8
+ if ENV["VERSION"]
9
+ SchedulerExtension.migrator.migrate(ENV["VERSION"].to_i)
10
+ else
11
+ SchedulerExtension.migrator.migrate
12
+ end
13
+ end
14
+
15
+ desc "Copies public assets of the Scheduler to the instance public/ directory."
16
+ task :update => :environment do
17
+ is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
18
+ puts "Copying assets from SchedulerExtension"
19
+ Dir[SchedulerExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
20
+ path = file.sub(SchedulerExtension.root, '')
21
+ directory = File.dirname(path)
22
+ mkdir_p RAILS_ROOT + directory, :verbose => false
23
+ cp file, RAILS_ROOT + path, :verbose => false
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,181 @@
1
+
2
+ /* http://svn.danwebb.net/external/lowpro/trunk/behaviours/date_selector.js */
3
+ DateSelector = Behavior.create({
4
+ initialize: function(options) {
5
+ this.element.addClassName('date_selector');
6
+ this.calendar = null;
7
+ this.options = Object.extend(DateSelector.DEFAULTS, options || {});
8
+ this.date = this.getDate();
9
+ this._createCalendar();
10
+ },
11
+ setDate : function(value) {
12
+ this.date = value;
13
+ this.element.value = this.options.setter(this.date);
14
+
15
+ if (this.calendar)
16
+ setTimeout(this.calendar.element.hide.bind(this.calendar.element), 500);
17
+ },
18
+ _createCalendar : function() {
19
+ var calendar = $div({ 'class' : 'date_selector' });
20
+ document.body.appendChild(calendar);
21
+ calendar.setStyle({
22
+ position : 'absolute',
23
+ zIndex : '500'
24
+ });
25
+ this.calendar = new Calendar(calendar, this);
26
+ },
27
+ onclick : function(e) {
28
+ this.calendar.element.setStyle({
29
+ top : Position.cumulativeOffset(this.element)[1] + this.element.getHeight() + 'px',
30
+ left : Position.cumulativeOffset(this.element)[0] + 'px'
31
+ });
32
+ this.calendar.show();
33
+ Event.stop(e);
34
+ },
35
+ onfocus : function(e) {
36
+ this.onclick(e);
37
+ },
38
+ getDate : function() {
39
+ return this.options.getter(this.element.value) || new Date;
40
+ }
41
+ });
42
+
43
+ Calendar = Behavior.create({
44
+ initialize : function(selector) {
45
+ this.selector = selector;
46
+ this.element.hide();
47
+ Event.observe(document, 'click', this.element.hide.bind(this.element));
48
+ },
49
+ show : function() {
50
+ Calendar.instances.invoke('hide');
51
+ this.date = this.selector.getDate();
52
+ this.redraw();
53
+ this.element.show();
54
+ this.active = true;
55
+ },
56
+ hide : function() {
57
+ this.element.hide();
58
+ this.active = false;
59
+ },
60
+ redraw : function() {
61
+ var html = '<table class="calendar">' +
62
+ ' <thead>' +
63
+ ' <tr><th class="back"><a href="#">&larr;</a></th>' +
64
+ ' <th colspan="5" class="month_label">' + this._label() + '</th>' +
65
+ ' <th class="forward"><a href="#">&rarr;</a></th></tr>' +
66
+ ' <tr class="day_header">' + this._dayRows() + '</tr>' +
67
+ ' </thead>' +
68
+ ' <tbody>';
69
+ html += this._buildDateCells();
70
+ html += '</tbody></table>';
71
+ this.element.innerHTML = html;
72
+ },
73
+ onclick : function(e) {
74
+ var source = Event.element(e);
75
+ Event.stop(e);
76
+
77
+ if ($(source.parentNode).hasClassName('day')) return this._setDate(source);
78
+ if ($(source.parentNode).hasClassName('back')) return this._backMonth();
79
+ if ($(source.parentNode).hasClassName('forward')) return this._forwardMonth();
80
+ },
81
+ _setDate : function(source) {
82
+ if (source.innerHTML.strip() != '') {
83
+ this.date.setDate(parseInt(source.innerHTML));
84
+ this.selector.setDate(this.date);
85
+ this.element.getElementsByClassName('selected').invoke('removeClassName', 'selected');
86
+ source.parentNode.addClassName('selected');
87
+ }
88
+ },
89
+ _backMonth : function() {
90
+ this.date.setMonth(this.date.getMonth() - 1);
91
+ this.redraw();
92
+ return false;
93
+ },
94
+ _forwardMonth : function() {
95
+ this.date.setMonth(this.date.getMonth() + 1);
96
+ this.redraw();
97
+ return false;
98
+ },
99
+ _getDateFromSelector : function() {
100
+ this.date = new Date(this.selector.date.getTime());
101
+ },
102
+ _firstDay : function(month, year) {
103
+ return new Date(year, month, 1).getDay();
104
+ },
105
+ _monthLength : function(month, year) {
106
+ var length = Calendar.MONTHS[month].days;
107
+ return (month == 1 && (year % 4 == 0) && (year % 100 != 0)) ? 29 : length;
108
+ },
109
+ _label : function() {
110
+ return Calendar.MONTHS[this.date.getMonth()].label + ' ' + this.date.getFullYear();
111
+ },
112
+ _dayRows : function() {
113
+ for (var i = 0, html='', day; day = Calendar.DAYS[i]; i++)
114
+ html += '<th>' + day + '</th>';
115
+ return html;
116
+ },
117
+ _buildDateCells : function() {
118
+ var month = this.date.getMonth(), year = this.date.getFullYear();
119
+ var day = 1, monthLength = this._monthLength(month, year), firstDay = this._firstDay(month, year);
120
+
121
+ for (var i = 0, html = '<tr>'; i < 9; i++) {
122
+ for (var j = 0; j <= 6; j++) {
123
+
124
+ if (day <= monthLength && (i > 0 || j >= firstDay)) {
125
+ var classes = ['day'];
126
+
127
+ if (this._compareDate(new Date, year, month, day)) classes.push('today');
128
+ if (this._compareDate(this.selector.date, year, month, day)) classes.push('selected');
129
+
130
+ html += '<td class="' + classes.join(' ') + '">' +
131
+ '<a href="#">' + day++ + '</a>' +
132
+ '</td>';
133
+ } else html += '<td></td>';
134
+ }
135
+
136
+ if (day > monthLength) break;
137
+ else html += '</tr><tr>';
138
+ }
139
+
140
+ return html + '</tr>';
141
+ },
142
+ _compareDate : function(date, year, month, day) {
143
+ return date.getFullYear() == year &&
144
+ date.getMonth() == month &&
145
+ date.getDate() == day;
146
+ }
147
+ });
148
+
149
+ DateSelector.DEFAULTS = {
150
+ setter: function(date) {
151
+ return [
152
+ date.getFullYear(),
153
+ date.getMonth() + 1,
154
+ date.getDate()
155
+ ].join('/');
156
+ },
157
+ getter: function(value) {
158
+ var parsed = Date.parse(value);
159
+
160
+ if (!isNaN(parsed)) return new Date(parsed);
161
+ else return null;
162
+ }
163
+ }
164
+
165
+ Object.extend(Calendar, {
166
+ DAYS : $w('S M T W T F S'),
167
+ MONTHS : [
168
+ { label : 'January', days : 31 },
169
+ { label : 'February', days : 28 },
170
+ { label : 'March', days : 31 },
171
+ { label : 'April', days : 30 },
172
+ { label : 'May', days : 31 },
173
+ { label : 'June', days : 30 },
174
+ { label : 'July', days : 31 },
175
+ { label : 'August', days : 31 },
176
+ { label : 'September', days : 30 },
177
+ { label : 'October', days : 31 },
178
+ { label : 'November', days : 30 },
179
+ { label : 'December', days : 31 }
180
+ ]
181
+ });
@@ -0,0 +1,338 @@
1
+ LowPro = {};
2
+ LowPro.Version = '0.5';
3
+ LowPro.CompatibleWithPrototype = '1.6';
4
+
5
+ if (Prototype.Version.indexOf(LowPro.CompatibleWithPrototype) != 0 && window.console && window.console.warn)
6
+ console.warn("This version of Low Pro is tested with Prototype " + LowPro.CompatibleWithPrototype +
7
+ " it may not work as expected with this version (" + Prototype.Version + ")");
8
+
9
+ if (!Element.addMethods)
10
+ Element.addMethods = function(o) { Object.extend(Element.Methods, o) };
11
+
12
+ // Simple utility methods for working with the DOM
13
+ DOM = {};
14
+
15
+ // DOMBuilder for prototype
16
+ DOM.Builder = {
17
+ tagFunc : function(tag) {
18
+ return function() {
19
+ var attrs, children;
20
+ if (arguments.length>0) {
21
+ if (arguments[0].constructor == Object) {
22
+ attrs = arguments[0];
23
+ children = Array.prototype.slice.call(arguments, 1);
24
+ } else {
25
+ children = arguments;
26
+ };
27
+ children = $A(children).flatten()
28
+ }
29
+ return DOM.Builder.create(tag, attrs, children);
30
+ };
31
+ },
32
+ create : function(tag, attrs, children) {
33
+ attrs = attrs || {}; children = children || []; tag = tag.toLowerCase();
34
+ var el = new Element(tag, attrs);
35
+
36
+ for (var i=0; i<children.length; i++) {
37
+ if (typeof children[i] == 'string')
38
+ children[i] = document.createTextNode(children[i]);
39
+ el.appendChild(children[i]);
40
+ }
41
+ return $(el);
42
+ }
43
+ };
44
+
45
+ // Automatically create node builders as $tagName.
46
+ (function() {
47
+ var els = ("p|div|span|strong|em|img|table|tr|td|th|thead|tbody|tfoot|pre|code|" +
48
+ "h1|h2|h3|h4|h5|h6|ul|ol|li|form|input|textarea|legend|fieldset|" +
49
+ "select|option|blockquote|cite|br|hr|dd|dl|dt|address|a|button|abbr|acronym|" +
50
+ "script|link|style|bdo|ins|del|object|param|col|colgroup|optgroup|caption|" +
51
+ "label|dfn|kbd|samp|var").split("|");
52
+ var el, i=0;
53
+ while (el = els[i++])
54
+ window['$' + el] = DOM.Builder.tagFunc(el);
55
+ })();
56
+
57
+ DOM.Builder.fromHTML = function(html) {
58
+ var root;
59
+ if (!(root = arguments.callee._root))
60
+ root = arguments.callee._root = document.createElement('div');
61
+ root.innerHTML = html;
62
+ return root.childNodes[0];
63
+ };
64
+
65
+
66
+
67
+ // Wraps the 1.6 contentloaded event for backwards compatibility
68
+ //
69
+ // Usage:
70
+ //
71
+ // Event.onReady(callbackFunction);
72
+ Object.extend(Event, {
73
+ onReady : function(f) {
74
+ if (document.body) f();
75
+ else document.observe('dom:loaded', f);
76
+ }
77
+ });
78
+
79
+ // Based on event:Selectors by Justin Palmer
80
+ // http://encytemedia.com/event-selectors/
81
+ //
82
+ // Usage:
83
+ //
84
+ // Event.addBehavior({
85
+ // "selector:event" : function(event) { /* event handler. this refers to the element. */ },
86
+ // "selector" : function() { /* runs function on dom ready. this refers to the element. */ }
87
+ // ...
88
+ // });
89
+ //
90
+ // Multiple calls will add to exisiting rules. Event.addBehavior.reassignAfterAjax and
91
+ // Event.addBehavior.autoTrigger can be adjusted to needs.
92
+ Event.addBehavior = function(rules) {
93
+ var ab = this.addBehavior;
94
+ Object.extend(ab.rules, rules);
95
+
96
+ if (!ab.responderApplied) {
97
+ Ajax.Responders.register({
98
+ onComplete : function() {
99
+ if (Event.addBehavior.reassignAfterAjax)
100
+ setTimeout(function() { ab.reload() }, 10);
101
+ }
102
+ });
103
+ ab.responderApplied = true;
104
+ }
105
+
106
+ if (ab.autoTrigger) {
107
+ this.onReady(ab.load.bind(ab, rules));
108
+ }
109
+
110
+ };
111
+
112
+ Event.delegate = function(rules) {
113
+ return function(e) {
114
+ var element = $(e.element());
115
+ for (var selector in rules)
116
+ if (element.match(selector)) return rules[selector].apply(this, $A(arguments));
117
+ }
118
+ }
119
+
120
+ Object.extend(Event.addBehavior, {
121
+ rules : {}, cache : [],
122
+ reassignAfterAjax : false,
123
+ autoTrigger : true,
124
+
125
+ load : function(rules) {
126
+ for (var selector in rules) {
127
+ var observer = rules[selector];
128
+ var sels = selector.split(',');
129
+ sels.each(function(sel) {
130
+ var parts = sel.split(/:(?=[a-z]+$)/), css = parts[0], event = parts[1];
131
+ $$(css).each(function(element) {
132
+ if (event) {
133
+ var wrappedObserver = Event.addBehavior._wrapObserver(observer);
134
+ $(element).observe(event, wrappedObserver);
135
+ Event.addBehavior.cache.push([element, event, wrappedObserver]);
136
+ } else {
137
+ if (!element.$$assigned || !element.$$assigned.include(observer)) {
138
+ if (observer.attach) observer.attach(element);
139
+
140
+ else observer.call($(element));
141
+ element.$$assigned = element.$$assigned || [];
142
+ element.$$assigned.push(observer);
143
+ }
144
+ }
145
+ });
146
+ });
147
+ }
148
+ },
149
+
150
+ unload : function() {
151
+ this.cache.each(function(c) {
152
+ Event.stopObserving.apply(Event, c);
153
+ });
154
+ this.cache = [];
155
+ },
156
+
157
+ reload: function() {
158
+ var ab = Event.addBehavior;
159
+ ab.unload();
160
+ ab.load(ab.rules);
161
+ },
162
+
163
+ _wrapObserver: function(observer) {
164
+ return function(event) {
165
+ if (observer.call(this, event) === false) event.stop();
166
+ }
167
+ }
168
+
169
+ });
170
+
171
+ Event.observe(window, 'unload', Event.addBehavior.unload.bind(Event.addBehavior));
172
+
173
+ // A silly Prototype style shortcut for the reckless
174
+ $$$ = Event.addBehavior.bind(Event);
175
+
176
+ // Behaviors can be bound to elements to provide an object orientated way of controlling elements
177
+ // and their behavior. Use Behavior.create() to make a new behavior class then use attach() to
178
+ // glue it to an element. Each element then gets it's own instance of the behavior and any
179
+ // methods called onxxx are bound to the relevent event.
180
+ //
181
+ // Usage:
182
+ //
183
+ // var MyBehavior = Behavior.create({
184
+ // onmouseover : function() { this.element.addClassName('bong') }
185
+ // });
186
+ //
187
+ // Event.addBehavior({ 'a.rollover' : MyBehavior });
188
+ //
189
+ // If you need to pass additional values to initialize use:
190
+ //
191
+ // Event.addBehavior({ 'a.rollover' : MyBehavior(10, { thing : 15 }) })
192
+ //
193
+ // You can also use the attach() method. If you specify extra arguments to attach they get passed to initialize.
194
+ //
195
+ // MyBehavior.attach(el, values, to, init);
196
+ //
197
+ // Finally, the rawest method is using the new constructor normally:
198
+ // var draggable = new Draggable(element, init, vals);
199
+ //
200
+ // Each behaviour has a collection of all its instances in Behavior.instances
201
+ //
202
+ var Behavior = {
203
+ create: function() {
204
+ var parent = null, properties = $A(arguments);
205
+ if (Object.isFunction(properties[0]))
206
+ parent = properties.shift();
207
+
208
+ var behavior = function() {
209
+ if (!this.initialize) {
210
+ var args = $A(arguments);
211
+
212
+ return function() {
213
+ var initArgs = [this].concat(args);
214
+ behavior.attach.apply(behavior, initArgs);
215
+ };
216
+ } else {
217
+ var args = (arguments.length == 2 && arguments[1] instanceof Array) ?
218
+ arguments[1] : Array.prototype.slice.call(arguments, 1);
219
+
220
+ this.element = $(arguments[0]);
221
+ this.initialize.apply(this, args);
222
+ behavior._bindEvents(this);
223
+ behavior.instances.push(this);
224
+ }
225
+ };
226
+
227
+ Object.extend(behavior, Class.Methods);
228
+ Object.extend(behavior, Behavior.Methods);
229
+ behavior.superclass = parent;
230
+ behavior.subclasses = [];
231
+ behavior.instances = [];
232
+
233
+ if (parent) {
234
+ var subclass = function() { };
235
+ subclass.prototype = parent.prototype;
236
+ behavior.prototype = new subclass;
237
+ parent.subclasses.push(behavior);
238
+ }
239
+
240
+ for (var i = 0; i < properties.length; i++)
241
+ behavior.addMethods(properties[i]);
242
+
243
+ if (!behavior.prototype.initialize)
244
+ behavior.prototype.initialize = Prototype.emptyFunction;
245
+
246
+ behavior.prototype.constructor = behavior;
247
+
248
+ return behavior;
249
+ },
250
+ Methods : {
251
+ attach : function(element) {
252
+ return new this(element, Array.prototype.slice.call(arguments, 1));
253
+ },
254
+ _bindEvents : function(bound) {
255
+ for (var member in bound) {
256
+ var matches = member.match(/^on(.+)/);
257
+ if (matches && typeof bound[member] == 'function')
258
+ bound.element.observe(matches[1], Event.addBehavior._wrapObserver(bound[member].bindAsEventListener(bound)));
259
+ }
260
+ }
261
+ }
262
+ };
263
+
264
+
265
+
266
+ Remote = Behavior.create({
267
+ initialize: function(options) {
268
+ if (this.element.nodeName == 'FORM') new Remote.Form(this.element, options);
269
+ else new Remote.Link(this.element, options);
270
+ }
271
+ });
272
+
273
+ Remote.Base = {
274
+ initialize : function(options) {
275
+ this.options = Object.extend({
276
+ evaluateScripts : true
277
+ }, options || {});
278
+
279
+ this._bindCallbacks();
280
+ },
281
+ _makeRequest : function(options) {
282
+ if (options.update) new Ajax.Updater(options.update, options.url, options);
283
+ else new Ajax.Request(options.url, options);
284
+ return false;
285
+ },
286
+ _bindCallbacks: function() {
287
+ $w('onCreate onComplete onException onFailure onInteractive onLoading onLoaded onSuccess').each(function(cb) {
288
+ if (Object.isFunction(this.options[cb]))
289
+ this.options[cb] = this.options[cb].bind(this);
290
+ }.bind(this));
291
+ }
292
+ }
293
+
294
+ Remote.Link = Behavior.create(Remote.Base, {
295
+ onclick : function() {
296
+ var options = Object.extend({ url : this.element.href, method : 'get' }, this.options);
297
+ return this._makeRequest(options);
298
+ }
299
+ });
300
+
301
+
302
+ Remote.Form = Behavior.create(Remote.Base, {
303
+ onclick : function(e) {
304
+ var sourceElement = e.element();
305
+
306
+ if (['input', 'button'].include(sourceElement.nodeName.toLowerCase()) &&
307
+ sourceElement.type == 'submit')
308
+ this._submitButton = sourceElement;
309
+ },
310
+ onsubmit : function() {
311
+ var options = Object.extend({
312
+ url : this.element.action,
313
+ method : this.element.method || 'get',
314
+ parameters : this.element.serialize({ submit: this._submitButton.name })
315
+ }, this.options);
316
+ this._submitButton = null;
317
+ return this._makeRequest(options);
318
+ }
319
+ });
320
+
321
+ Observed = Behavior.create({
322
+ initialize : function(callback, options) {
323
+ this.callback = callback.bind(this);
324
+ this.options = options || {};
325
+ this.observer = (this.element.nodeName == 'FORM') ? this._observeForm() : this._observeField();
326
+ },
327
+ stop: function() {
328
+ this.observer.stop();
329
+ },
330
+ _observeForm: function() {
331
+ return (this.options.frequency) ? new Form.Observer(this.element, this.options.frequency, this.callback) :
332
+ new Form.EventObserver(this.element, this.callback);
333
+ },
334
+ _observeField: function() {
335
+ return (this.options.frequency) ? new Form.Element.Observer(this.element, this.options.frequency, this.callback) :
336
+ new Form.Element.EventObserver(this.element, this.callback);
337
+ }
338
+ });
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "radiant-scheduler-extension"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "radiant-scheduler-extension"
7
+ s.version = RadiantSchedulerExtension::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = RadiantSchedulerExtension::AUTHORS
10
+ s.email = RadiantSchedulerExtension::EMAIL
11
+ s.homepage = RadiantSchedulerExtension::URL
12
+ s.summary = RadiantSchedulerExtension::SUMMARY
13
+ s.description = RadiantSchedulerExtension::DESCRIPTION
14
+
15
+ # Define gem dependencies here.
16
+ # Don't include a dependency on radiant itself: it causes problems when radiant is in vendor/radiant.
17
+ # s.add_dependency "something", "~> 1.0.0"
18
+ # s.add_dependency "radiant-some-extension", "~> 1.0.0"
19
+
20
+ ignores = if File.exist?('.gitignore')
21
+ File.read('.gitignore').split("\n").inject([]) {|a,p| a + Dir[p] }
22
+ else
23
+ []
24
+ end
25
+ s.files = Dir['**/*'] - ignores
26
+ s.test_files = Dir['test/**/*','spec/**/*','features/**/*'] - ignores
27
+ # s.executables = Dir['bin/*'] - ignores
28
+ s.require_paths = ["lib"]
29
+ end
@@ -0,0 +1,16 @@
1
+ # Uncomment this if you reference any of your controllers in activate
2
+ require_dependency 'application_controller'
3
+
4
+ require 'radiant-scheduler-extension'
5
+
6
+ class SchedulerExtension < Radiant::Extension
7
+ version RadiantSchedulerExtension::VERSION
8
+ description RadiantSchedulerExtension::DESCRIPTION
9
+ url RadiantSchedulerExtension::URL
10
+
11
+ def activate
12
+ Page.send :include, Scheduler::PageExtensions
13
+ SiteController.send :include, Scheduler::ControllerExtensions
14
+ admin.pages.edit.add :extended_metadata, "edit_scheduler_meta"
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+
3
+ cd ~
4
+ git clone git://github.com/radiant/radiant.git
5
+ cd ~/radiant
6
+ if [[ $RADIANT_VERSION != "master" ]]
7
+ then
8
+ git checkout -b $RADIANT_VERSION $RADIANT_VERSION
9
+ fi
10
+ cp -r ~/builds/*/radiant-scheduler-extension vendor/extensions/scheduler
11
+ bundle install
12
+
13
+ case $DB in
14
+ "mysql" )
15
+ mysql -e 'create database radiant_test;'
16
+ cp spec/ci/database.mysql.yml config/database.yml;;
17
+ "postgres" )
18
+ psql -c 'create database radiant_test;' -U postgres
19
+ cp spec/ci/database.postgresql.yml config/database.yml;;
20
+ esac
21
+
22
+ bundle exec rake db:migrate
23
+ bundle exec rake db:migrate:extensions
data/spec/ci/script ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+
3
+ cd ~/radiant
4
+ bundle exec rake spec:extensions EXT=scheduler
@@ -0,0 +1,17 @@
1
+ require File.expand_path '../../spec_helper', __FILE__
2
+
3
+ describe "Scheduler::ControllerExtensions", :type => :controller do
4
+ dataset :pages_with_scheduling
5
+ controller_name :site
6
+
7
+ it "should not render invisible pages in live mode" do
8
+ get :show_page, :url => ['expired']
9
+ response.should_not be_success
10
+ end
11
+
12
+ it "should render invisible pages in dev mode" do
13
+ request.host = "dev.example.com"
14
+ get :show_page, :url => ['expired']
15
+ response.should be_success
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ class PagesWithSchedulingDataset < Dataset::Base
2
+ uses :pages
3
+
4
+ def load
5
+ create_page "Expired", :appears_on => 2.days.ago.to_date, :expires_on => Date.yesterday
6
+ create_page "Expired blank start", :expires_on => Date.yesterday
7
+ create_page "Blank schedule"
8
+ create_page "Visible blank start", :expires_on => Date.tomorrow
9
+ create_page "Visible", :appears_on => Date.yesterday, :expires_on => Date.tomorrow
10
+ create_page "Visible blank end", :appears_on => Date.yesterday
11
+ create_page "Unappeared blank end", :appears_on => Date.tomorrow
12
+ create_page "Unappeared", :appears_on => Date.tomorrow, :expires_on => 2.days.from_now.to_date
13
+ end
14
+ end
@@ -0,0 +1,123 @@
1
+ require File.expand_path '../../spec_helper', __FILE__
2
+
3
+ describe "Scheduler::PageExtensions" do
4
+ dataset :pages_with_scheduling
5
+
6
+ describe "finding pages" do
7
+ it "should wrap the find_by_path method" do
8
+ Page.should respond_to(:find_by_path_with_scheduling)
9
+ end
10
+
11
+ it "should find visible pages in both modes" do
12
+ [true, false].each do |live|
13
+ [:blank_schedule, :visible, :visible_blank_start, :visible_blank_end].each do |page|
14
+ Page.find_by_path(pages(page).url, live).should == pages(page)
15
+ end
16
+ end
17
+ end
18
+
19
+ it "should not find pages scheduled outside the window when live" do
20
+ [:expired, :expired_blank_start, :unappeared, :unappeared_blank_end].each do |page|
21
+ Page.find_by_path(pages(page).url).should_not == pages(page)
22
+ end
23
+ end
24
+
25
+ it "should find pages scheduled outside the window when dev" do
26
+ [:expired, :expired_blank_start, :unappeared, :unappeared_blank_end].each do |page|
27
+ Page.find_by_path(pages(page).url, false).should == pages(page)
28
+ end
29
+ end
30
+ end
31
+
32
+ describe "scheduling interrogators" do
33
+ before :each do
34
+ @page = Page.new
35
+ end
36
+
37
+ describe "appeared?" do
38
+ it "should be true when the page has no limits" do
39
+ @page.should be_appeared
40
+ end
41
+
42
+ it "should be true when the page has no start date" do
43
+ @page.appears_on = nil
44
+ @page.expires_on = Date.tomorrow
45
+ @page.should be_appeared
46
+ end
47
+
48
+ it "should be true when the start date is in the past" do
49
+ @page.appears_on = Date.yesterday
50
+ @page.expires_on = Date.tomorrow
51
+ @page.should be_appeared
52
+ end
53
+
54
+ it "should be false when the start date is in the future" do
55
+ @page.appears_on = Date.tomorrow
56
+ @page.should_not be_appeared
57
+ end
58
+ end
59
+
60
+ describe "expired?" do
61
+ it "should be false when the page has no limits" do
62
+ @page.should_not be_expired
63
+ end
64
+
65
+ it "should be false when the page has no end date" do
66
+ @page.appears_on = Date.yesterday
67
+ @page.expires_on = nil
68
+ @page.should_not be_expired
69
+ end
70
+
71
+ it "should be false when the end date is in the future" do
72
+ @page.expires_on = Date.tomorrow
73
+ @page.should_not be_expired
74
+ end
75
+
76
+ it "should be true when the end date is in the past" do
77
+ @page.expires_on = Date.yesterday
78
+ @page.should be_expired
79
+ end
80
+ end
81
+
82
+ describe "visible?" do
83
+ before :each do
84
+ @page.stub!(:published?).and_return(true)
85
+ end
86
+
87
+ it "should be true when the page is appeared and not expired" do
88
+ @page.stub!(:appeared?).and_return(true)
89
+ @page.stub!(:expired?).and_return(false)
90
+ @page.should be_visible
91
+ end
92
+ it "should be false when the page has not appeared" do
93
+ @page.stub!(:appeared?).and_return(false)
94
+ @page.stub!(:expired?).and_return(false)
95
+ @page.should_not be_visible
96
+ end
97
+ it "should be false when the page has expired" do
98
+ @page.stub!(:appeared?).and_return(true)
99
+ @page.stub!(:expired?).and_return(true)
100
+ @page.should_not be_visible
101
+ end
102
+ it "should be false when the page is unpublished" do
103
+ @page.stub!(:published?).and_return(false)
104
+ @page.should_not be_visible
105
+ end
106
+ end
107
+ end
108
+
109
+ describe "<r:children>" do
110
+ before :each do
111
+ raise "dev site configured" if Radiant::Config['dev.site']
112
+ end
113
+
114
+ it "should render only visible children in live mode" do
115
+ pages(:home).should render("<r:children:each><r:title /> </r:children:each>").matching(%r{^((?!expired)(?!unappeared).)*$}i)
116
+ end
117
+
118
+ it "should render all children in dev mode" do
119
+ pages(:home).should render("<r:children:each><r:title /> </r:children:each>").matching(%r{expired}i).on("dev.example.com")
120
+ pages(:home).should render("<r:children:each><r:title /> </r:children:each>").matching(%r{unappeared}i).on("dev.example.com")
121
+ end
122
+ end
123
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,6 @@
1
+ --colour
2
+ --format
3
+ progress
4
+ --loadby
5
+ mtime
6
+ --reverse
@@ -0,0 +1,36 @@
1
+ unless defined? RADIANT_ROOT
2
+ ENV["RAILS_ENV"] = "test"
3
+ case
4
+ when ENV["RADIANT_ENV_FILE"]
5
+ require ENV["RADIANT_ENV_FILE"]
6
+ when File.dirname(__FILE__) =~ %r{vendor/radiant/vendor/extensions}
7
+ require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../../")}/config/environment"
8
+ else
9
+ require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../")}/config/environment"
10
+ end
11
+ end
12
+ require "#{RADIANT_ROOT}/spec/spec_helper"
13
+
14
+ Dataset::Resolver.default << (File.dirname(__FILE__) + "/datasets")
15
+
16
+ if File.directory?(File.dirname(__FILE__) + "/matchers")
17
+ Dir[File.dirname(__FILE__) + "/matchers/*.rb"].each {|file| require file }
18
+ end
19
+
20
+ Spec::Runner.configure do |config|
21
+ # config.use_transactional_fixtures = true
22
+ # config.use_instantiated_fixtures = false
23
+ # config.fixture_path = RAILS_ROOT + '/spec/fixtures'
24
+
25
+ # You can declare fixtures for each behaviour like this:
26
+ # describe "...." do
27
+ # fixtures :table_a, :table_b
28
+ #
29
+ # Alternatively, if you prefer to declare them only once, you can
30
+ # do so here, like so ...
31
+ #
32
+ # config.global_fixtures = :table_a, :table_b
33
+ #
34
+ # If you declare global fixtures, be aware that they will be declared
35
+ # for all of your examples, even those that don't use them.
36
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: radiant-scheduler-extension
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Sean Cribbs
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-03-29 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Allows setting of appearance and expiration dates for pages.
23
+ email:
24
+ - sean@basho.com
25
+ executables: []
26
+
27
+ extensions: []
28
+
29
+ extra_rdoc_files: []
30
+
31
+ files:
32
+ - app/views/admin/pages/_edit_scheduler_meta.html.haml
33
+ - cucumber.yml
34
+ - db/migrate/001_add_schedule_fields.rb
35
+ - features/support/env.rb
36
+ - features/support/paths.rb
37
+ - lib/radiant-scheduler-extension.rb
38
+ - lib/scheduler/controller_extensions.rb
39
+ - lib/scheduler/page_extensions.rb
40
+ - lib/tasks/scheduler_extension_tasks.rake
41
+ - public/javascripts/date_selector.js
42
+ - public/javascripts/lowpro.js
43
+ - radiant-scheduler-extension.gemspec
44
+ - Rakefile
45
+ - README.md
46
+ - scheduler_extension.rb
47
+ - spec/ci/before_script
48
+ - spec/ci/script
49
+ - spec/controllers/controller_extensions_spec.rb
50
+ - spec/datasets/pages_with_scheduling_dataset.rb
51
+ - spec/models/page_extensions_spec.rb
52
+ - spec/spec.opts
53
+ - spec/spec_helper.rb
54
+ has_rdoc: true
55
+ homepage: https://github.com/radiant/radiant-scheduler-extension
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options: []
60
+
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 3
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 3
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 1.3.9.3
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: Scheduler extension for Radiant CMS
88
+ test_files:
89
+ - spec/ci/before_script
90
+ - spec/ci/script
91
+ - spec/controllers/controller_extensions_spec.rb
92
+ - spec/datasets/pages_with_scheduling_dataset.rb
93
+ - spec/models/page_extensions_spec.rb
94
+ - spec/spec.opts
95
+ - spec/spec_helper.rb
96
+ - features/support/env.rb
97
+ - features/support/paths.rb