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,6 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ map.namespace :admin, :member => { :clear_responses => :post } do |admin|
3
+ admin.resources :polls
4
+ end
5
+ map.resources :poll_response, :only => [ :index, :create ], :path_prefix => '/pages/:page_id', :controller => 'poll_response'
6
+ end
@@ -0,0 +1,12 @@
1
+ class CreatePolls < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :polls do |t|
4
+ t.string :title
5
+ t.integer :response_count
6
+ end
7
+ end
8
+
9
+ def self.down
10
+ drop_table :polls
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ class CreateOptions < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :options do |t|
4
+ t.integer :poll_id
5
+ t.string :title
6
+ t.integer :response_count, :integer
7
+ t.integer :should_destroy
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ drop_table :options
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ class AddStartDateToPolls < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :polls, :start_date, :date
4
+ end
5
+
6
+ def self.down
7
+ remove_column :polls, :start_date
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module PollProcess
2
+ def self.included(base)
3
+ base.class_eval {
4
+ alias_method_chain :process, :poll
5
+ attr_accessor :submitted_polls
6
+ }
7
+ end
8
+
9
+ def poll_cookies(cookie_hash)
10
+ result = []
11
+ cookie_hash.to_a.select { |c| c[0].starts_with?('poll_') }.each do |c|
12
+ result << c[0].gsub('poll_','').to_i
13
+ end
14
+ result
15
+ end
16
+
17
+ def process_with_poll(request, response)
18
+ # check the cookies and set the polls that have been submitted
19
+ self.submitted_polls = poll_cookies(request.cookies)
20
+ process_without_poll(request, response)
21
+ end
22
+
23
+ end
@@ -0,0 +1,408 @@
1
+ # -*- coding: utf-8 -*-
2
+ module PollTags
3
+ include Radiant::Taggable
4
+ include WillPaginate::ViewHelpers
5
+
6
+ class RadiantLinkRenderer < WillPaginate::LinkRenderer
7
+ include ActionView::Helpers::TagHelper
8
+
9
+ def initialize(tag)
10
+ @tag = tag
11
+ end
12
+
13
+ def page_link(page, text, attributes = {})
14
+ attributes = tag_options(attributes)
15
+ @paginate_url_route = @paginate_url_route.blank? ? PollsExtension::UrlCache : @paginate_url_route
16
+ %Q{<a href="#{@tag.locals.page.url}#{@paginate_url_route}#{page}"#{attributes}>#{text}</a>}
17
+ end
18
+
19
+ def gap_marker
20
+ '<span class="gap">&#8230;</span>'
21
+ end
22
+
23
+ def page_span(page, text, attributes = {})
24
+ attributes = tag_options(attributes)
25
+ "<span#{attributes}>#{text}</span>"
26
+ end
27
+ end
28
+
29
+ class TagError < StandardError; end
30
+
31
+ ##
32
+ ## Individual polls
33
+ ##
34
+
35
+ desc %{
36
+ Selects the active poll.
37
+
38
+ *Usage:*
39
+ <pre><code><r:poll [title="poll_title"]>...</r:poll></code></pre>
40
+ }
41
+ tag 'poll' do |tag|
42
+ options = tag.attr.dup
43
+ if Poll.count > 0
44
+ tag.locals.poll = find_poll(tag, options)
45
+ tag.expand.blank? ? tag.expand : Poll.marker + tag.expand
46
+ else
47
+ 'No polls found'
48
+ end
49
+ end
50
+
51
+ desc %{
52
+ Expands inner tags if the poll has not been submitted yet.
53
+
54
+ *Usage:*
55
+ <pre><code><r:poll:unless_submitted>...</r:poll:unless_submitted></code></pre>
56
+
57
+ }
58
+ tag 'poll:unless_submitted' do |tag|
59
+ options = tag.attr.dup
60
+ poll = tag.locals.poll = find_poll(tag, options)
61
+ tag.expand unless tag.locals.page.submitted_polls && tag.locals.page.submitted_polls.include?(poll.id)
62
+ end
63
+
64
+ desc %{
65
+ Expands inner tags if the poll has been submitted.
66
+
67
+ *Usage:*
68
+ <pre><code><r:poll:if_submitted>...</r:poll:if_submitted></code></pre>
69
+
70
+ }
71
+ tag 'poll:if_submitted' do |tag|
72
+ options = tag.attr.dup
73
+ poll = tag.locals.poll = find_poll(tag, options)
74
+ tag.expand if tag.locals.page.submitted_polls && tag.locals.page.submitted_polls.include?(poll.id)
75
+ end
76
+
77
+ desc %{
78
+ Shows the poll id.
79
+
80
+ *Usage:*
81
+ <pre><code><r:poll [title="My Poll"]><r:id /></r:poll></code></pre>
82
+ }
83
+ tag 'poll:id' do |tag|
84
+ tag.locals.poll.id
85
+ end
86
+
87
+ desc %{
88
+ Shows the poll title.
89
+
90
+ *Usage:*
91
+ <pre><code><r:poll [title="My Poll"]><r:title /></r:poll></code></pre>
92
+ }
93
+ tag 'poll:title' do |tag|
94
+ tag.locals.poll.title
95
+ end
96
+
97
+ desc %{
98
+ Renders a poll survey form.
99
+
100
+ *Usage:*
101
+ <pre><code><r:poll [title="My Poll"]><r:form>...</r:form></r:poll></code></pre>
102
+ }
103
+ tag 'poll:form' do |tag|
104
+ options = tag.attr.dup
105
+ tag.locals.poll = find_poll(tag, options)
106
+ poll = tag.locals.poll
107
+ result = %Q{
108
+ <form action="/pages/#{tag.locals.page.id}/poll_response" method="post" id="poll_form">
109
+ <div>
110
+ #{tag.expand}
111
+ <input type="hidden" name="poll_id" value="#{tag.locals.poll.id}" />
112
+ </div>
113
+ </form>
114
+ }
115
+ end
116
+
117
+ desc %{
118
+ A collection of options, optionally sorted by response_count in
119
+ ASCending, DESCending, or RANDom order. If no order is specified
120
+ the result will be in whatever order is returned by the SQL query.
121
+
122
+ *Usage:*
123
+ <pre><code><r:poll:options [order="asc|desc|rand"]>...</r:poll:options></code></pre>
124
+ }
125
+ tag 'poll:options' do |tag|
126
+ tag.locals.sort_order = case tag.attr['order']
127
+ when /^asc/i
128
+ lambda { |a,b| a.response_count <=> b.response_count }
129
+ when /^desc/i
130
+ lambda { |a,b| b.response_count <=> a.response_count }
131
+ when /^rand/i
132
+ lambda { rand(3) - 1 }
133
+ else
134
+ nil
135
+ end
136
+ tag.expand
137
+ end
138
+
139
+ desc %{
140
+ Iterate through each poll option.
141
+
142
+ *Usage:*
143
+ <pre><code><r:poll:options:each>...</r:poll:options:each></code></pre>
144
+ }
145
+ tag 'poll:options:each' do |tag|
146
+ result = []
147
+ options = tag.locals.sort_order.nil? ? tag.locals.poll.options : tag.locals.poll.options.sort(&tag.locals.sort_order)
148
+ options.each do |option|
149
+ tag.locals.option = option
150
+ result << tag.expand
151
+ end
152
+ result
153
+ end
154
+
155
+ desc %{
156
+ Render inner content if the current contextual option is the first option.
157
+
158
+ *Usage:*
159
+ <pre><code><r:poll:options:each:if_first>...</r:poll:options:each:if_first></code></pre>
160
+ }
161
+ tag 'poll:options:each:if_first' do |tag|
162
+ options = tag.locals.sort_order.nil? ? tag.locals.poll.options : tag.locals.poll.options.sort(&tag.locals.sort_order)
163
+ if options.first == tag.locals.option
164
+ tag.expand
165
+ end
166
+ end
167
+
168
+ desc %{
169
+ Render inner content if the current contextual option is the last option.
170
+
171
+ *Usage:*
172
+ <pre><code><r:poll:options:each:if_last>...</r:poll:options:each:if_last></code></pre>
173
+ }
174
+ tag 'poll:options:each:if_last' do |tag|
175
+ options = tag.locals.sort_order.nil? ? tag.locals.poll.options : tag.locals.poll.options.sort(&tag.locals.sort_order)
176
+ if options.last == tag.locals.option
177
+ tag.expand
178
+ end
179
+ end
180
+
181
+ desc %{
182
+ Render inner content unless the current contextual option is the first option.
183
+
184
+ *Usage:*
185
+ <pre><code><r:poll:options:each:unless_first>...</r:poll:options:each:unless_first></code></pre>
186
+ }
187
+ tag 'poll:options:each:unless_first' do |tag|
188
+ options = tag.locals.sort_order.nil? ? tag.locals.poll.options : tag.locals.poll.options.sort(&tag.locals.sort_order)
189
+ unless options.first == tag.locals.option
190
+ tag.expand
191
+ end
192
+ end
193
+
194
+ desc %{
195
+ Render inner content unless the current contextual option is the last option.
196
+
197
+ *Usage:*
198
+ <pre><code><r:poll:options:each:unless_last>...</r:poll:options:each:unless_last></code></pre>
199
+ }
200
+ tag 'poll:options:each:unless_last' do |tag|
201
+ options = tag.locals.sort_order.nil? ? tag.locals.poll.options : tag.locals.poll.options.sort(&tag.locals.sort_order)
202
+ unless options.last == tag.locals.option
203
+ tag.expand
204
+ end
205
+ end
206
+
207
+ desc %{
208
+ Show the poll option radio button input type.
209
+
210
+ *Usage:*
211
+ <pre><code><r:poll:form><r:options:each><r:input /><r:title /></r:options:each></r:poll:form></code></pre>
212
+ }
213
+ tag 'poll:options:input' do |tag|
214
+ option = tag.locals.option
215
+ %{<input type="radio" name="response_id" value="#{option.id}" />}
216
+ end
217
+
218
+ desc %{
219
+ Show the poll option title.
220
+
221
+ *Usage:*
222
+ <pre><code><r:poll:options:each><r:title /></r:poll:options:each></code></pre>
223
+ }
224
+ tag 'poll:options:title' do |tag|
225
+ tag.locals.option.title
226
+ end
227
+
228
+ desc %{
229
+ Show a poll form submit button.
230
+
231
+ *Usage:*
232
+ <pre><code><r:poll:form><r:options:each><r:input /><r:title /></r:options:each><r:submit [value="Go!"] /></r:poll:form></code></pre>
233
+ }
234
+ tag 'poll:submit' do |tag|
235
+ value = tag.attr['value'] || 'Submit'
236
+ %{<input type="submit" name="poll[submit]" value="#{value}" />}
237
+ end
238
+
239
+ desc %{
240
+ Show percentage of responses for a particular option.
241
+
242
+ *Usage:*
243
+ <pre><code><r:poll:options:each><r:title /> <r:percent_responses /></r:poll:options:each></code></pre>
244
+ }
245
+ tag 'options:percent_responses' do |tag|
246
+ tag.locals.option.response_percentage
247
+ end
248
+
249
+ desc %{
250
+ Show count of responses for a particular option.
251
+
252
+ *Usage:*
253
+ <pre><code><r:poll:options:each><r:title /> <r:number_responded /></r:poll:options:each></code></pre>
254
+ }
255
+ tag 'options:number_responses' do |tag|
256
+ tag.locals.option.response_count
257
+ end
258
+
259
+ desc %{
260
+ Show total number of visitors that responded.
261
+
262
+ *Usage:*
263
+ <pre><code><r:poll:total_responses /></code></pre>
264
+ }
265
+ tag 'poll:total_responses' do |tag|
266
+ tag.locals.poll.response_count
267
+ end
268
+
269
+ tag 'poll_test' do |tag|
270
+ "#{tag.locals.page.request.cookies.inspect}"
271
+ end
272
+
273
+ ##
274
+ ## Collections of polls
275
+ ##
276
+
277
+ desc %{
278
+ Selects all polls.
279
+
280
+ By default, polls are sorted in ascending order by title and limited to 10 per page;
281
+ the current poll and any future polls are excluded. To include the current poll, set
282
+ show_current = "true". Any polls where @attribute@ is null are also excluded, so if
283
+ you have a set of polls that include polls that do not have a defined start date, then
284
+ when specifying @by="start_date"@ only those polls with start dates will be shown.
285
+
286
+ *Usage:*
287
+ <pre><code><r:polls [per_page="10"] [by="attribute"] [order="asc|desc"] [show_current="true|false"] /></code></pre>
288
+ }
289
+ tag 'polls' do |tag|
290
+ if Poll.count > 0
291
+ options = find_options(tag)
292
+ tag.locals.paginated_polls = Poll.paginate(options)
293
+ tag.expand.blank? ? tag.expand : Poll.marker + tag.expand
294
+ else
295
+ 'No polls found'
296
+ end
297
+ end
298
+
299
+ desc %{
300
+ Loops through each poll and renders the contents.
301
+
302
+ *Usage:*
303
+ <pre><code><r:polls:each>...</r:polls:each></code></pre>
304
+ }
305
+ tag 'polls:each' do |tag|
306
+ result = []
307
+
308
+ tag.locals.paginated_polls.each do |poll|
309
+ tag.locals.poll = poll
310
+ result << tag.expand
311
+ end
312
+
313
+ result
314
+ end
315
+
316
+ desc %{
317
+ Creates the context for a single poll.
318
+
319
+ *Usage:*
320
+ <pre><code><r:polls:each:poll>...</r:polls:each:poll></code></pre>
321
+ }
322
+ tag 'polls:each:poll' do |tag|
323
+ tag.expand
324
+ end
325
+
326
+ desc %{
327
+ Renders pagination links with will_paginate
328
+ The following optional attributes may be controlled:
329
+
330
+ * id - the id to apply to the containing @<div>@
331
+ * class - the class to apply to the containing @<div>@
332
+ * previous_label - default: "« Previous"
333
+ * prev_label - deprecated variant of previous_label
334
+ * next_label - default: "Next »"
335
+ * inner_window - how many links are shown around the current page (default: 4)
336
+ * outer_window - how many links are around the first and the last page (default: 1)
337
+ * separator - string separator for page HTML elements (default: single space)
338
+ * page_links - when false, only previous/next links are rendered (default: true)
339
+ * container - when false, pagination links are not wrapped in a containing @<div>@ (default: true)
340
+
341
+ *Usage:*
342
+
343
+ <pre><code><r:polls>
344
+ <r:pages [id=""] [class="pagination"] [previous_label="&laquo; Previous"]
345
+ [next_label="Next &raquo;"] [inner_window="4"] [outer_window="1"]
346
+ [separator=" "] [page_links="true"] [container="true"]/>
347
+ </r:polls></code></pre>
348
+ }
349
+ tag 'polls:pages' do |tag|
350
+ renderer = RadiantLinkRenderer.new(tag)
351
+
352
+ options = {}
353
+
354
+ [:id, :class, :previous_label, :prev_label, :next_label, :inner_window, :outer_window, :separator].each do |a|
355
+ options[a] = tag.attr[a.to_s] unless tag.attr[a.to_s].blank?
356
+ end
357
+ options[:page_links] = false if 'false' == tag.attr['page_links']
358
+ options[:container] = false if 'false' == tag.attr['container']
359
+
360
+ will_paginate tag.locals.paginated_polls, options.merge(:renderer => renderer)
361
+ end
362
+
363
+ private
364
+
365
+ def find_poll(tag, options)
366
+ unless title = options.delete('title') or id = options.delete('id') or tag.locals.poll
367
+ current_poll = Poll.find_current
368
+ raise TagError, "'title' attribute required" unless current_poll
369
+ end
370
+ current_poll || tag.locals.poll || Poll.find_by_title(title) || Poll.find(id)
371
+ end
372
+
373
+ def find_options(tag)
374
+ options = {}
375
+
376
+ options[:page] = tag.attr['page'] || @request.path[/^#{Regexp.quote(tag.locals.page.url)}#{Regexp.quote(PollsExtension::UrlCache)}(\d+)\/?$/, 1]
377
+ options[:per_page] = (tag.attr['per_page'] || 10).to_i
378
+ raise TagError.new('the per_page attribute of the polls tag must be a positive integer') unless options[:per_page] > 0
379
+ by = tag.attr['by'] || 'title'
380
+ order = tag.attr['order'] || 'asc'
381
+ order_string = ''
382
+ if Poll.new.attributes.keys.include?(by)
383
+ order_string << by
384
+ else
385
+ raise TagError.new('the by attribute of the polls tag must specify a valid field name')
386
+ end
387
+ if order =~ /^(a|de)sc$/i
388
+ order_string << " #{order.upcase}"
389
+ else
390
+ raise TagError.new('the order attribute of the polls tag must be either "asc" or "desc"')
391
+ end
392
+ options[:order] = order_string
393
+
394
+ # Exclude polls with null `by' values as well as any future polls and,
395
+ # if it exists, the current poll unless it is specifically included.
396
+ options[:conditions] = [ "#{by} IS NOT NULL AND COALESCE(start_date,?) <= ?",
397
+ Date.civil(1900,1,1), Date.today ]
398
+ unless tag.attr['show_current'] == 'true'
399
+ if current_poll = Poll.find_current
400
+ options[:conditions].first << ' AND id <> ?'
401
+ options[:conditions] << current_poll.id
402
+ end
403
+ end
404
+
405
+ options
406
+ end
407
+
408
+ end