radiant-polls-extension 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/CHANGELOG +27 -0
  2. data/HELP.textile +71 -0
  3. data/LICENSE +21 -0
  4. data/README.textile +112 -0
  5. data/Rakefile +129 -0
  6. data/VERSION +1 -0
  7. data/app/controllers/admin/polls_controller.rb +25 -0
  8. data/app/controllers/poll_response_controller.rb +35 -0
  9. data/app/models/option.rb +22 -0
  10. data/app/models/poll.rb +67 -0
  11. data/app/views/admin/polls/_form.html.haml +67 -0
  12. data/app/views/admin/polls/_option.html.haml +9 -0
  13. data/app/views/admin/polls/edit.html.haml +5 -0
  14. data/app/views/admin/polls/index.html.haml +72 -0
  15. data/app/views/admin/polls/new.html.haml +5 -0
  16. data/app/views/admin/polls/remove.html.haml +17 -0
  17. data/config/locales/en.yml +25 -0
  18. data/config/locales/en_available_tags.yml +187 -0
  19. data/config/routes.rb +6 -0
  20. data/db/migrate/001_create_polls.rb +12 -0
  21. data/db/migrate/002_create_options.rb +14 -0
  22. data/db/migrate/003_add_start_date_to_polls.rb +9 -0
  23. data/lib/poll_process.rb +23 -0
  24. data/lib/poll_tags.rb +408 -0
  25. data/lib/tasks/polls_extension_tasks.rake +56 -0
  26. data/polls_extension.rb +51 -0
  27. data/public/images/admin/new-poll.png +0 -0
  28. data/public/images/admin/poll.png +0 -0
  29. data/public/images/admin/recycle.png +0 -0
  30. data/public/images/admin/reset.png +0 -0
  31. data/public/javascripts/admin/date_selector.js +177 -0
  32. data/public/javascripts/admin/polls.js +47 -0
  33. data/public/javascripts/poll_check.js +52 -0
  34. data/public/stylesheets/admin/polls.css +93 -0
  35. data/public/stylesheets/polls.css +9 -0
  36. data/radiant-polls-extension.gemspec +83 -0
  37. data/spec/controllers/admin/polls_controller_spec.rb +16 -0
  38. data/spec/controllers/poll_response_controller_spec.rb +149 -0
  39. data/spec/integration/page_caching_spec.rb +76 -0
  40. data/spec/lib/poll_tags_spec.rb +415 -0
  41. data/spec/models/poll_spec.rb +50 -0
  42. data/spec/spec.opts +6 -0
  43. data/spec/spec_helper.rb +42 -0
  44. metadata +141 -0
@@ -0,0 +1,56 @@
1
+ namespace :radiant do
2
+ namespace :extensions do
3
+ namespace :polls do
4
+
5
+ desc "Runs the migration of the Assets extension"
6
+ task :migrate => :environment do
7
+ require 'radiant/extension_migrator'
8
+ if ENV["VERSION"]
9
+ PollsExtension.migrator.migrate(ENV["VERSION"].to_i)
10
+ Rake::Task['db:schema:dump'].invoke
11
+ else
12
+ PollsExtension.migrator.migrate
13
+ Rake::Task['db:schema:dump'].invoke
14
+ end
15
+ end
16
+
17
+ desc "Copies public assets of the Assets to the instance public/ directory."
18
+ task :update => :environment do
19
+ is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
20
+ Dir[PollsExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
21
+ path = file.sub(PollsExtension.root, '')
22
+ directory = File.dirname(path)
23
+ puts "Copying #{path}..."
24
+ mkdir_p RAILS_ROOT + directory, :verbose => false
25
+ cp file, RAILS_ROOT + path, :verbose => false
26
+ end
27
+
28
+ unless PollsExtension.root.starts_with? RAILS_ROOT # don't need to copy vendored tasks
29
+ puts "Copying rake tasks from PollsExtension"
30
+ local_tasks_path = File.join(RAILS_ROOT, %w(lib tasks))
31
+ mkdir_p local_tasks_path, :verbose => false
32
+ Dir[File.join PollsExtension.root, %w(lib tasks *.rake)].each do |file|
33
+ cp file, local_tasks_path, :verbose => false
34
+ end
35
+ end
36
+ end
37
+
38
+ desc "Syncs all available translations for this ext to the English ext master"
39
+ task :sync => :environment do
40
+ # The main translation root, basically where English is kept
41
+ language_root = PollsExtension.get_translation_keys(language_root)
42
+ words = TranslationSupport.get_translation_keys(language_root)
43
+
44
+ Dir["#{language_root}/*.yml"].each do |filename|
45
+ next if filename.match('_available_tags')
46
+ basename = File.basename(filename, '.yml')
47
+ puts "Syncing #{basename}"
48
+ (comments, other) = TranslationSupport.read_file(filename, basename)
49
+ words.each { |k,v| other[k] ||= words[k] } # Initializing hash variable as empty if it does not exist
50
+ other.delete_if { |k,v| !words[k] } # Remove if not defined in en.yml
51
+ TranslationSupport.write_file(filename, basename, comments, other)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,51 @@
1
+ require_dependency 'application_controller'
2
+
3
+ class PollsExtension < Radiant::Extension
4
+
5
+ version "#{File.read(File.expand_path(File.dirname(__FILE__)) + '/VERSION')}"
6
+ description 'Radiant gets polls. Forked from nuex (Chase James) to use cookies.'
7
+ url 'https://github.com/avonderluft/radiant-polls-extension'
8
+
9
+ def activate
10
+
11
+ unless defined?(CacheByPage)
12
+ puts "The Radiant cache_by_page extension is recommended, to set shorter caching times on pages with polls"
13
+ end
14
+
15
+ tab 'Content' do
16
+ add_item 'Polls', '/admin/polls'
17
+ end
18
+
19
+ if Radiant::Config.table_exists?
20
+ Radiant::Config['paginate.url_route'] = '' unless Radiant::Config['paginate.url_route']
21
+ PollsExtension.const_set('UrlCache', Radiant::Config['paginate.url_route'])
22
+ end
23
+
24
+ Page.send :include, PollTags
25
+ Page.send :include, PollProcess
26
+
27
+ Radiant::AdminUI.class_eval do
28
+ attr_accessor :poll
29
+ alias_method "polls", :poll
30
+ end
31
+ admin.poll = load_default_poll_regions
32
+
33
+ end
34
+
35
+ def load_default_poll_regions
36
+ OpenStruct.new.tap do |poll|
37
+ poll.edit = Radiant::AdminUI::RegionSet.new do |edit|
38
+ edit.main.concat %w{edit_header edit_form}
39
+ edit.form_bottom.concat %w{edit_buttons edit_timestamp}
40
+ end
41
+ poll.index = Radiant::AdminUI::RegionSet.new do |index|
42
+ index.top.concat %w{}
43
+ index.thead.concat %w{name_header date_header responses_header actions_header}
44
+ index.tbody.concat %w{name_cell date_cell responses_cell actions_cell}
45
+ index.bottom.concat %w{new_button}
46
+ end
47
+ poll.new = poll.edit
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,177 @@
1
+ DateSelector = Behavior.create({
2
+ initialize: function(options) {
3
+ this.element.addClassName('date_selector');
4
+ this.calendar = null;
5
+ this.options = Object.extend(DateSelector.DEFAULTS, options || {});
6
+ this.date = this.getDate();
7
+ this._createCalendar();
8
+ },
9
+ setDate : function(value) {
10
+ this.date = value;
11
+ this.element.value = this.options.setter(this.date);
12
+
13
+ if (this.calendar)
14
+ setTimeout(this.calendar.element.hide.bind(this.calendar.element), 500);
15
+ },
16
+ _createCalendar : function() {
17
+ var calendar = $div({ 'class' : 'date_selector' });
18
+ document.body.appendChild(calendar);
19
+ calendar.setStyle({
20
+ position : 'absolute',
21
+ zIndex : '500',
22
+ top : Position.cumulativeOffset(this.element)[1] + this.element.getHeight() + 'px',
23
+ left : Position.cumulativeOffset(this.element)[0] + 'px'
24
+ });
25
+ this.calendar = new Calendar(calendar, this);
26
+ },
27
+ onclick : function(e) {
28
+ this.calendar.show();
29
+ Event.stop(e);
30
+ },
31
+ onfocus : function(e) {
32
+ this.onclick(e);
33
+ },
34
+ getDate : function() {
35
+ return this.options.getter(this.element.value) || new Date;
36
+ }
37
+ });
38
+
39
+ Calendar = Behavior.create({
40
+ initialize : function(selector) {
41
+ this.selector = selector;
42
+ this.element.hide();
43
+ Event.observe(document, 'click', this.element.hide.bind(this.element));
44
+ },
45
+ show : function() {
46
+ Calendar.instances.invoke('hide');
47
+ this.date = this.selector.getDate();
48
+ this.redraw();
49
+ this.element.show();
50
+ this.active = true;
51
+ },
52
+ hide : function() {
53
+ this.element.hide();
54
+ this.active = false;
55
+ },
56
+ redraw : function() {
57
+ var html = '<table class="calendar">' +
58
+ ' <thead>' +
59
+ ' <tr><th class="back"><a href="#">&larr;</a></th>' +
60
+ ' <th colspan="5" class="month_label">' + this._label() + '</th>' +
61
+ ' <th class="forward"><a href="#">&rarr;</a></th></tr>' +
62
+ ' <tr class="day_header">' + this._dayRows() + '</tr>' +
63
+ ' </thead>' +
64
+ ' <tbody>';
65
+ html += this._buildDateCells();
66
+ html += '</tbody></table>';
67
+ this.element.innerHTML = html;
68
+ },
69
+ onclick : function(e) {
70
+ var source = Event.element(e);
71
+ Event.stop(e);
72
+
73
+ if ($(source.parentNode).hasClassName('day')) return this._setDate(source);
74
+ if ($(source.parentNode).hasClassName('back')) return this._backMonth();
75
+ if ($(source.parentNode).hasClassName('forward')) return this._forwardMonth();
76
+ },
77
+ _setDate : function(source) {
78
+ if (source.innerHTML.strip() != '') {
79
+ this.date.setDate(parseInt(source.innerHTML));
80
+ this.selector.setDate(this.date);
81
+ this.element.select('.selected').invoke('removeClassName', 'selected');
82
+ source.parentNode.addClassName('selected');
83
+ }
84
+ },
85
+ _backMonth : function() {
86
+ this.date.setMonth(this.date.getMonth() - 1);
87
+ this.redraw();
88
+ return false;
89
+ },
90
+ _forwardMonth : function() {
91
+ this.date.setMonth(this.date.getMonth() + 1);
92
+ this.redraw();
93
+ return false;
94
+ },
95
+ _getDateFromSelector : function() {
96
+ this.date = new Date(this.selector.date.getTime());
97
+ },
98
+ _firstDay : function(month, year) {
99
+ return new Date(year, month, 1).getDay();
100
+ },
101
+ _monthLength : function(month, year) {
102
+ var length = Calendar.MONTHS[month].days;
103
+ return (month == 1 && (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0))) ? 29 : length;
104
+ },
105
+ _label : function() {
106
+ return Calendar.MONTHS[this.date.getMonth()].label + ' ' + this.date.getFullYear();
107
+ },
108
+ _dayRows : function() {
109
+ for (var i = 0, html='', day; day = Calendar.DAYS[i]; i++)
110
+ html += '<th>' + day + '</th>';
111
+ return html;
112
+ },
113
+ _buildDateCells : function() {
114
+ var month = this.date.getMonth(), year = this.date.getFullYear();
115
+ var day = 1, monthLength = this._monthLength(month, year), firstDay = this._firstDay(month, year);
116
+
117
+ for (var i = 0, html = '<tr>'; i < 9; i++) {
118
+ for (var j = 0; j <= 6; j++) {
119
+
120
+ if (day <= monthLength && (i > 0 || j >= firstDay)) {
121
+ var classes = ['day'];
122
+
123
+ if (this._compareDate(new Date, year, month, day)) classes.push('today');
124
+ if (this._compareDate(this.selector.date, year, month, day)) classes.push('selected');
125
+
126
+ html += '<td class="' + classes.join(' ') + '">' +
127
+ '<a href="#">' + day++ + '</a>' +
128
+ '</td>';
129
+ } else html += '<td></td>';
130
+ }
131
+
132
+ if (day > monthLength) break;
133
+ else html += '</tr><tr>';
134
+ }
135
+
136
+ return html + '</tr>';
137
+ },
138
+ _compareDate : function(date, year, month, day) {
139
+ return date.getFullYear() == year &&
140
+ date.getMonth() == month &&
141
+ date.getDate() == day;
142
+ }
143
+ });
144
+
145
+ DateSelector.DEFAULTS = {
146
+ setter: function(date) {
147
+ return [
148
+ date.getFullYear(),
149
+ date.getMonth() + 1,
150
+ date.getDate()
151
+ ].join('/');
152
+ },
153
+ getter: function(value) {
154
+ var parsed = Date.parse(value);
155
+
156
+ if (!isNaN(parsed)) return new Date(parsed);
157
+ else return null;
158
+ }
159
+ }
160
+
161
+ Object.extend(Calendar, {
162
+ DAYS : $w('S M T W T F S'),
163
+ MONTHS : [
164
+ { label : 'January', days : 31 },
165
+ { label : 'February', days : 28 },
166
+ { label : 'March', days : 31 },
167
+ { label : 'April', days : 30 },
168
+ { label : 'May', days : 31 },
169
+ { label : 'June', days : 30 },
170
+ { label : 'July', days : 31 },
171
+ { label : 'August', days : 31 },
172
+ { label : 'September', days : 30 },
173
+ { label : 'October', days : 31 },
174
+ { label : 'November', days : 30 },
175
+ { label : 'December', days : 31 }
176
+ ]
177
+ });
@@ -0,0 +1,47 @@
1
+ // poll javascript
2
+
3
+ // add an option field
4
+
5
+ function add_option(container_id){
6
+ var container = $(container_id);
7
+ var template = new Template('<p class="option" id="option_#{id}"><input type="text" name="poll[option_attributes][][title]" size="30" /> <a href="#" onclick="Element.remove(\'option_#{id}\')">Cancel</a></p>');
8
+ new Insertion.Bottom(container, template.evaluate({id: new Date().getTime()}));
9
+ $(container).childElements().last().childElements()[0].activate();
10
+ }
11
+
12
+
13
+ // delete an option
14
+
15
+ function delete_option(id, container_id){
16
+ if(confirm("Really delete this option?")){
17
+ var container = $(container_id);
18
+
19
+ // mark option for deletion
20
+ ($("option_"+id).down('.should_destroy') || $("option_"+id).next('.error-with-field').next('.should_destroy')).value = 1
21
+
22
+ // hide the field
23
+ var elem = Element.hide("option_"+id);
24
+ var next_elem = elem.nextSiblings()[0];
25
+ if (next_elem.hasClassName('error-with-field')) {
26
+ next_elem.hide();
27
+ }
28
+
29
+ // don't redisplay the deletion notice if its already there
30
+ if(typeof $('options').previous('.title').down('#options-deleted') == 'undefined')
31
+ {
32
+ new Insertion.After('options-title', '<p class="option" id="options-deleted">Removed options will be deleted when you Save this poll.</p>');
33
+ }
34
+ new Effect.Highlight('options-deleted');
35
+ }
36
+ }
37
+
38
+
39
+ // rearrange the page contents (works around problems with error messages)
40
+
41
+ function initialize_page_view() {
42
+ $('options').descendants().findAll(function(obj) {
43
+ return obj.hasClassName('delete');
44
+ }).each(function(obj) {
45
+ obj.nextSiblings()[0].insert({ after:obj.remove() });
46
+ });
47
+ }
@@ -0,0 +1,52 @@
1
+ // In order to cache pages with poll functionality we put the logic in the client.
2
+ // In other words, rather than having the server check the cookie status
3
+ // to see if the user has taken the poll and then decide to send either the questions or results,
4
+ // we should send both options to the client. Then, using the following javascript on the browser,
5
+ // check the cookie and display either the poll options or the poll results.
6
+
7
+ // Read a cookie
8
+ function readCookie(name) {
9
+ var nameEQ = name + "=";
10
+ var ca = document.cookie.split(';');
11
+ for(var i=0;i < ca.length;i++) {
12
+ var c = ca[i];
13
+ while (c.charAt(0)==' ') c = c.substring(1,c.length);
14
+ if (c.indexOf(nameEQ) == 0) {
15
+ var a=c.substring(nameEQ.length,c.length);
16
+ /* alert(a);*/
17
+ return a;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ // Client-side Poll processing - check for presence of poll
24
+ // If present, read cookie to see if it has been submitted by this user
25
+ function checkForPoll () {
26
+ var poll = document.getElementById('poll');
27
+ if (poll!=null) {
28
+ var pollDivs = poll.getElementsByTagName("div");
29
+ var pollName = pollDivs[1].id;
30
+ var submittedDiv = document.getElementById(pollName + "_submitted");
31
+ var unsubmittedDiv = document.getElementById(pollName + "_unsubmitted");
32
+ theCookie=readCookie(pollName);
33
+ if (theCookie!=null) {
34
+ unsubmittedDiv.parentNode.removeChild(unsubmittedDiv);
35
+ submittedDiv.style.display = "inline";
36
+ } else {
37
+ submittedDiv.parentNode.removeChild(submittedDiv);
38
+ unsubmittedDiv.style.display = "inline";
39
+ }
40
+ }
41
+ }
42
+
43
+ // ensure radio button selected
44
+ function checkRadioSelection (frmId, rbGroupName) {
45
+ var radios = document.getElementById(frmId).elements[rbGroupName];
46
+ for (var i=0; i <radios.length; i++) {
47
+ if (radios[i].checked) return true;
48
+ }
49
+ alert("Please make a selection first...");
50
+ return false;
51
+ }
52
+
@@ -0,0 +1,93 @@
1
+ #content #polls.index {
2
+ border: none;
3
+ border-collapse: collapse;
4
+ }
5
+ #content #polls.index .responses {
6
+ padding-right: 2em;
7
+ text-align: right;
8
+ }
9
+ #content #polls.index .reset {
10
+ padding-left: 0;
11
+ width: 140px;
12
+ }
13
+ #content #polls.index .reset a {
14
+ background: url("../../images/admin/polls/recycle.png") no-repeat scroll 4px 0 transparent;
15
+ color: black;
16
+ font-size: 80%;
17
+ padding: 0 0 0 20px;
18
+ text-decoration: none;
19
+ }
20
+ #content #polls.index th.modify {
21
+ text-align: center;
22
+ }
23
+ #content #polls.index .node .poll,
24
+ #content #polls.index .node .responses {
25
+ font-size: 115%;
26
+ font-weight: bold;
27
+ }
28
+ #content #polls.index .node .responses .none {
29
+ color: #666;
30
+ }
31
+ #content #polls.index .node .poll a,
32
+ #content #polls.index .node .poll a:visited,
33
+ #content #polls.index .node .responses a,
34
+ #content #polls.index .node .responses a:visited {
35
+ color: black;
36
+ text-decoration: none;
37
+ }
38
+
39
+ #content #poll_form_area .title {
40
+ margin-top: 1em;
41
+ }
42
+ #content #poll_form_area .title label {
43
+ font-size: 120%;
44
+ }
45
+
46
+ #options p.option {
47
+ margin-bottom: 1em;
48
+ }
49
+ #options .error-with-field {
50
+ margin-top: -1em;
51
+ }
52
+ #options .delete {
53
+ margin: 0 0.5em;
54
+ }
55
+ #content .form-area p#add-option {
56
+ margin-top: 1em;
57
+ }
58
+
59
+ #poll_start_date {
60
+ position: relative;
61
+ }
62
+ table.calendar {
63
+ background: white;
64
+ border: 1px solid black;
65
+ }
66
+ table.calendar td {
67
+ background: white;
68
+ border: 1px solid #eee;
69
+ padding: 2px;
70
+ text-align: right;
71
+ }
72
+ table.calendar td.today a {
73
+ color: red !important;
74
+ }
75
+ table.calendar td.back, table.calendar td.forward {
76
+ background: #ddd;
77
+ }
78
+ table.calendar th {
79
+ background: #eee;
80
+ }
81
+ table.calendar a {
82
+ color: black;
83
+ text-decoration: none;
84
+ }
85
+ table.calendar tbody tr:last-child td {
86
+ border-bottom: 1px solid black;
87
+ }
88
+ table.calendar tbody tr td:first-child {
89
+ border-left: 1px solid black;
90
+ }
91
+ table.calendar tbody tr td:last-child {
92
+ border-right: 1px solid black;
93
+ }