radiant-scheduler-extension 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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