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.
- data/CHANGELOG +27 -0
- data/HELP.textile +71 -0
- data/LICENSE +21 -0
- data/README.textile +112 -0
- data/Rakefile +129 -0
- data/VERSION +1 -0
- data/app/controllers/admin/polls_controller.rb +25 -0
- data/app/controllers/poll_response_controller.rb +35 -0
- data/app/models/option.rb +22 -0
- data/app/models/poll.rb +67 -0
- data/app/views/admin/polls/_form.html.haml +67 -0
- data/app/views/admin/polls/_option.html.haml +9 -0
- data/app/views/admin/polls/edit.html.haml +5 -0
- data/app/views/admin/polls/index.html.haml +72 -0
- data/app/views/admin/polls/new.html.haml +5 -0
- data/app/views/admin/polls/remove.html.haml +17 -0
- data/config/locales/en.yml +25 -0
- data/config/locales/en_available_tags.yml +187 -0
- data/config/routes.rb +6 -0
- data/db/migrate/001_create_polls.rb +12 -0
- data/db/migrate/002_create_options.rb +14 -0
- data/db/migrate/003_add_start_date_to_polls.rb +9 -0
- data/lib/poll_process.rb +23 -0
- data/lib/poll_tags.rb +408 -0
- data/lib/tasks/polls_extension_tasks.rake +56 -0
- data/polls_extension.rb +51 -0
- data/public/images/admin/new-poll.png +0 -0
- data/public/images/admin/poll.png +0 -0
- data/public/images/admin/recycle.png +0 -0
- data/public/images/admin/reset.png +0 -0
- data/public/javascripts/admin/date_selector.js +177 -0
- data/public/javascripts/admin/polls.js +47 -0
- data/public/javascripts/poll_check.js +52 -0
- data/public/stylesheets/admin/polls.css +93 -0
- data/public/stylesheets/polls.css +9 -0
- data/radiant-polls-extension.gemspec +83 -0
- data/spec/controllers/admin/polls_controller_spec.rb +16 -0
- data/spec/controllers/poll_response_controller_spec.rb +149 -0
- data/spec/integration/page_caching_spec.rb +76 -0
- data/spec/lib/poll_tags_spec.rb +415 -0
- data/spec/models/poll_spec.rb +50 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +42 -0
- metadata +141 -0
data/config/routes.rb
ADDED
@@ -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,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
|
data/lib/poll_process.rb
ADDED
@@ -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
|
data/lib/poll_tags.rb
ADDED
@@ -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">…</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="« Previous"]
|
345
|
+
[next_label="Next »"] [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
|