radiant-polls-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.
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
+ }