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,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