liquid_feedback_frontend
diff app/main/vote/list.lua @ 19:00d1004545f1
Dynamic interface using XMLHttpRequests, and many other changes
Bugfixes:
- Only allow voting on admitted initiatives
- Repaired issue search
- Don't display delegations for closed issues on member page
- Don't show revoke link in initiative, when issue is already half_frozen
- Localization for voting JavaScript
- Display author of suggestions
Disclosure of voting data after voting is finished:
- Possibility to inspect every ballot including preferences
- Show number of voters preferring one initiative to another initiative
Interface behaviour changes:
- Reversed default order of drafts
- Default order of suggestions changed
- Show new drafts of initiatives only once per day in timeline
Accessibility:
- Barrier-free voting implemented
- POST links are now accessible without JavaScript
- Changed gray for unsatisfied supporters in bar graph to a lighter gray
Other interface improvements:
- Optical enhancements
- Dynamic interface using XMLHttpRequests
- Show usage terms in about section
- Show own membership in area listing
- Show uninformed supporters greyed out and marked with yellow question mark
- Warning box in non-admitted initiatives
- When voted, don't display voting notice and change label of voting link
- Show object counts in more tabulator heads
- Enlarged member statement input field
Miscellaneous:
- Code cleanup
- Added README file containing installation instructions
- Use new WebMCP function ui.filters{...} instead of own ui.filter and ui.order functions
Bugfixes:
- Only allow voting on admitted initiatives
- Repaired issue search
- Don't display delegations for closed issues on member page
- Don't show revoke link in initiative, when issue is already half_frozen
- Localization for voting JavaScript
- Display author of suggestions
Disclosure of voting data after voting is finished:
- Possibility to inspect every ballot including preferences
- Show number of voters preferring one initiative to another initiative
Interface behaviour changes:
- Reversed default order of drafts
- Default order of suggestions changed
- Show new drafts of initiatives only once per day in timeline
Accessibility:
- Barrier-free voting implemented
- POST links are now accessible without JavaScript
- Changed gray for unsatisfied supporters in bar graph to a lighter gray
Other interface improvements:
- Optical enhancements
- Dynamic interface using XMLHttpRequests
- Show usage terms in about section
- Show own membership in area listing
- Show uninformed supporters greyed out and marked with yellow question mark
- Warning box in non-admitted initiatives
- When voted, don't display voting notice and change label of voting link
- Show object counts in more tabulator heads
- Enlarged member statement input field
Miscellaneous:
- Code cleanup
- Added README file containing installation instructions
- Use new WebMCP function ui.filters{...} instead of own ui.filter and ui.order functions
| author | bsw/jbe | 
|---|---|
| date | Sat Feb 20 22:10:31 2010 +0100 (2010-02-20) | 
| parents | 77d58efe99fd | 
| children | 3036f2732b83 | 
   line diff
1.1 --- a/app/main/vote/list.lua Tue Feb 02 00:31:06 2010 +0100 1.2 +++ b/app/main/vote/list.lua Sat Feb 20 22:10:31 2010 +0100 1.3 @@ -1,26 +1,70 @@ 1.4 +local issue = Issue:by_id(param.get("issue_id"), atom.integer) 1.5 + 1.6 +local member_id = param.get("member_id", atom.integer) 1.7 +local member 1.8 + 1.9 +local readonly = false 1.10 +if member_id then 1.11 + if not issue.closed then 1.12 + error("access denied") 1.13 + end 1.14 + member = Member:by_id(member_id) 1.15 + readonly = true 1.16 +end 1.17 + 1.18 +if member then 1.19 + slot.put_into("title", _("Ballot of '#{member_name}' for issue ##{issue_id}", { 1.20 + member_name = member.name, 1.21 + issue_id = issue.id 1.22 + })) 1.23 +else 1.24 + member = app.session.member 1.25 + slot.put_into("title", _"Voting") 1.26 + 1.27 + slot.select("actions", function() 1.28 + ui.link{ 1.29 + content = function() 1.30 + ui.image{ static = "icons/16/cancel.png" } 1.31 + slot.put(_"Cancel") 1.32 + end, 1.33 + module = "issue", 1.34 + view = "show", 1.35 + id = issue.id 1.36 + } 1.37 + end) 1.38 + 1.39 +end 1.40 + 1.41 + 1.42 local warning_text = _"Some JavaScript based functions (voting in particular) will not work.\nFor this beta, please use a current version of Firefox, Safari, Opera(?), Konqueror or another (more) standard compliant browser.\nAlternative access without JavaScript will be available soon." 1.43 1.44 ui.script{ static = "js/browser_warning.js" } 1.45 ui.script{ script = "checkBrowser(" .. encode.json(_"Your web browser is not fully supported yet." .. " " .. warning_text:gsub("\n", "\n\n")) .. ");" } 1.46 1.47 -ui.tag{ 1.48 - tag = "noscript", 1.49 - content = function() 1.50 - slot.put(_"JavaScript is disabled or not available." .. " " .. encode.html_newlines(warning_text)) 1.51 + 1.52 +local tempvoting_string = param.get("scoring") 1.53 + 1.54 +local tempvotings = {} 1.55 +if tempvoting_string then 1.56 + for match in tempvoting_string:gmatch("([^;]+)") do 1.57 + for initiative_id, grade in match:gmatch("([^:;]+):([^:;]+)") do 1.58 + tempvotings[tonumber(initiative_id)] = tonumber(grade) 1.59 + end 1.60 end 1.61 -} 1.62 - 1.63 +end 1.64 1.65 -local issue = Issue:by_id(param.get("issue_id"), atom.integer) 1.66 - 1.67 -local initiatives = issue.initiatives 1.68 +local initiatives = issue:get_reference_selector("initiatives"):add_where("initiative.admitted"):exec() 1.69 1.70 local min_grade = -1; 1.71 local max_grade = 1; 1.72 1.73 for i, initiative in ipairs(initiatives) do 1.74 -- TODO performance 1.75 - initiative.vote = Vote:by_pk(initiative.id, app.session.member.id) 1.76 + initiative.vote = Vote:by_pk(initiative.id, member.id) 1.77 + if tempvotings[initiative.id] then 1.78 + initiative.vote = {} 1.79 + initiative.vote.grade = tempvotings[initiative.id] 1.80 + end 1.81 if initiative.vote then 1.82 if initiative.vote.grade > max_grade then 1.83 max_grade = initiative.vote.grade 1.84 @@ -41,28 +85,59 @@ 1.85 end 1.86 end 1.87 1.88 -slot.put_into("title", _"Voting") 1.89 +local approval_count, disapproval_count = 0, 0 1.90 +for i = min_grade, -1 do 1.91 + if #sections[i] > 0 then 1.92 + disapproval_count = disapproval_count + 1 1.93 + end 1.94 +end 1.95 +local approval_count = 0 1.96 +for i = 1, max_grade do 1.97 + if #sections[i] > 0 then 1.98 + approval_count = approval_count + 1 1.99 + end 1.100 +end 1.101 1.102 -slot.select("actions", function() 1.103 - ui.link{ 1.104 - content = function() 1.105 - ui.image{ static = "icons/16/cancel.png" } 1.106 - slot.put(_"Cancel") 1.107 - end, 1.108 - module = "issue", 1.109 - view = "show", 1.110 - id = issue.id 1.111 - } 1.112 -end) 1.113 - 1.114 -util.help("vote.list", _"Voting") 1.115 1.116 1.117 -slot.put('<script src="' .. request.get_relative_baseurl() .. 'static/js/dragdrop.js"></script>') 1.118 -slot.put('<script src="' .. request.get_relative_baseurl() .. 'static/js/voting.js"></script>') 1.119 +if not readonly then 1.120 + util.help("vote.list", _"Voting") 1.121 + slot.put('<script src="' .. request.get_relative_baseurl() .. 'static/js/dragdrop.js"></script>') 1.122 + slot.put('<script src="' .. request.get_relative_baseurl() .. 'static/js/voting.js"></script>') 1.123 +end 1.124 + 1.125 +ui.script{ 1.126 + script = function() 1.127 + slot.put( 1.128 + "voting_text_approval_single = ", encode.json(_"Approval [single entry]"), ";\n", 1.129 + "voting_text_approval_multi = ", encode.json(_"Approval [many entries]"), ";\n", 1.130 + "voting_text_first_preference_single = ", encode.json(_"Approval (first preference) [single entry]"), ";\n", 1.131 + "voting_text_first_preference_multi = ", encode.json(_"Approval (first preference) [many entries]"), ";\n", 1.132 + "voting_text_second_preference_single = ", encode.json(_"Approval (second preference) [single entry]"), ";\n", 1.133 + "voting_text_second_preference_multi = ", encode.json(_"Approval (second preference) [many entries]"), ";\n", 1.134 + "voting_text_third_preference_single = ", encode.json(_"Approval (third preference) [single entry]"), ";\n", 1.135 + "voting_text_third_preference_multi = ", encode.json(_"Approval (third preference) [many entries]"), ";\n", 1.136 + "voting_text_numeric_preference_single = ", encode.json(_"Approval (#th preference) [single entry]"), ";\n", 1.137 + "voting_text_numeric_preference_multi = ", encode.json(_"Approval (#th preference) [many entries]"), ";\n", 1.138 + "voting_text_abstention_single = ", encode.json(_"Abstention [single entry]"), ";\n", 1.139 + "voting_text_abstention_multi = ", encode.json(_"Abstention [many entries]"), ";\n", 1.140 + "voting_text_disapproval_above_one_single = ", encode.json(_"Disapproval (prefer to lower block) [single entry]"), ";\n", 1.141 + "voting_text_disapproval_above_one_multi = ", encode.json(_"Disapproval (prefer to lower block) [many entries]"), ";\n", 1.142 + "voting_text_disapproval_above_many_single = ", encode.json(_"Disapproval (prefer to lower blocks) [single entry]"), ";\n", 1.143 + "voting_text_disapproval_above_many_multi = ", encode.json(_"Disapproval (prefer to lower blocks) [many entries]"), ";\n", 1.144 + "voting_text_disapproval_above_last_single = ", encode.json(_"Disapproval (prefer to last block) [single entry]"), ";\n", 1.145 + "voting_text_disapproval_above_last_multi = ", encode.json(_"Disapproval (prefer to last block) [many entries]"), ";\n", 1.146 + "voting_text_disapproval_single = ", encode.json(_"Disapproval [single entry]"), ";\n", 1.147 + "voting_text_disapproval_multi = ", encode.json(_"Disapproval [many entries]"), ";\n" 1.148 + ) 1.149 + end 1.150 +} 1.151 1.152 ui.form{ 1.153 - attr = { id = "voting_form" }, 1.154 + attr = { 1.155 + id = "voting_form", 1.156 + class = readonly and "voting_form_readonly" or "voting_form_active" 1.157 + }, 1.158 module = "vote", 1.159 action = "update", 1.160 params = { issue_id = issue.id }, 1.161 @@ -75,21 +150,42 @@ 1.162 } 1.163 }, 1.164 content = function() 1.165 - slot.put('<input type="hidden" name="scoring" value=""/>') 1.166 - -- TODO abstrahieren 1.167 - ui.tag{ 1.168 - tag = "input", 1.169 - attr = { 1.170 - type = "button", 1.171 - class = "voting_done", 1.172 - value = _"Finish voting" 1.173 + if not readonly then 1.174 + local scoring = param.get("scoring") 1.175 + if not scoring then 1.176 + for i, initiative in ipairs(initiatives) do 1.177 + local vote = initiative.vote 1.178 + if vote then 1.179 + tempvotings[initiative.id] = vote.grade 1.180 + end 1.181 + end 1.182 + local tempvotings_list = {} 1.183 + for key, val in pairs(tempvotings) do 1.184 + tempvotings_list[#tempvotings_list+1] = tostring(key) .. ":" .. tostring(val) 1.185 + end 1.186 + if #tempvotings_list > 0 then 1.187 + scoring = table.concat(tempvotings_list, ";") 1.188 + else 1.189 + scoring = "" 1.190 + end 1.191 + end 1.192 + slot.put('<input type="hidden" name="scoring" value="' .. scoring .. '"/>') 1.193 + -- TODO abstrahieren 1.194 + ui.tag{ 1.195 + tag = "input", 1.196 + attr = { 1.197 + type = "button", 1.198 + class = "voting_done", 1.199 + value = _"Finish voting" 1.200 + } 1.201 } 1.202 - } 1.203 + end 1.204 ui.container{ 1.205 attr = { id = "voting" }, 1.206 content = function() 1.207 + local approval_index, disapproval_index = 0, 0 1.208 for grade = max_grade, min_grade, -1 do 1.209 - local section = sections[grade] 1.210 + local entries = sections[grade] 1.211 local class 1.212 if grade > 0 then 1.213 class = "approval" 1.214 @@ -98,75 +194,199 @@ 1.215 else 1.216 class = "abstention" 1.217 end 1.218 - ui.container{ 1.219 - attr = { class = class }, 1.220 - content = function() 1.221 - slot.put('<div class="cathead"></div>') 1.222 - for i, initiative in ipairs(section) do 1.223 - ui.container{ 1.224 - attr = { 1.225 - class = "movable", 1.226 - id = "entry_" .. tostring(initiative.id) 1.227 - }, 1.228 - content = function() 1.229 - local initiators_selector = initiative:get_reference_selector("initiating_members") 1.230 - :add_where("accepted") 1.231 - local initiators = initiators_selector:exec() 1.232 - local initiator_names = {} 1.233 - for i, initiator in ipairs(initiators) do 1.234 - initiator_names[#initiator_names+1] = initiator.name 1.235 + if 1.236 + #entries > 0 or 1.237 + (grade == 1 and not approval_used) or 1.238 + (grade == -1 and not disapproval_used) or 1.239 + grade == 0 1.240 + then 1.241 + ui.container{ 1.242 + attr = { class = class }, 1.243 + content = function() 1.244 + local heading 1.245 + if class == "approval" then 1.246 + approval_used = true 1.247 + approval_index = approval_index + 1 1.248 + if approval_count > 1 then 1.249 + if approval_index == 1 then 1.250 + if #entries == 1 then 1.251 + heading = _"Approval (first preference) [single entry]" 1.252 + else 1.253 + heading = _"Approval (first preference) [many entries]" 1.254 + end 1.255 + elseif approval_index == 2 then 1.256 + if #entries == 1 then 1.257 + heading = _"Approval (second preference) [single entry]" 1.258 + else 1.259 + heading = _"Approval (second preference) [many entries]" 1.260 + end 1.261 + elseif approval_index == 3 then 1.262 + if #entries == 1 then 1.263 + heading = _"Approval (third preference) [single entry]" 1.264 + else 1.265 + heading = _"Approval (third preference) [many entries]" 1.266 + end 1.267 + else 1.268 + if #entries == 1 then 1.269 + heading = _"Approval (#th preference) [single entry]" 1.270 + else 1.271 + heading = _"Approval (#th preference) [many entries]" 1.272 + end 1.273 + end 1.274 + else 1.275 + if #entries == 1 then 1.276 + heading = _"Approval [single entry]" 1.277 + else 1.278 + heading = _"Approval [many entries]" 1.279 + end 1.280 + end 1.281 + elseif class == "abstention" then 1.282 + if #entries == 1 then 1.283 + heading = _"Abstention [single entry]" 1.284 + else 1.285 + heading = _"Abstention [many entries]" 1.286 + end 1.287 + elseif class == "disapproval" then 1.288 + disapproval_used = true 1.289 + disapproval_index = disapproval_index + 1 1.290 + if disapproval_count > disapproval_index + 1 then 1.291 + if #entries == 1 then 1.292 + heading = _"Disapproval (prefer to lower blocks) [single entry]" 1.293 + else 1.294 + heading = _"Disapproval (prefer to lower blocks) [many entries]" 1.295 + end 1.296 + elseif disapproval_count == 2 and disapproval_index == 1 then 1.297 + if #entries == 1 then 1.298 + heading = _"Disapproval (prefer to lower block) [single entry]" 1.299 + else 1.300 + heading = _"Disapproval (prefer to lower block) [many entries]" 1.301 + end 1.302 + elseif disapproval_index == disapproval_count - 1 then 1.303 + if #entries == 1 then 1.304 + heading = _"Disapproval (prefer to last block) [single entry]" 1.305 + else 1.306 + heading = _"Disapproval (prefer to last block) [many entries]" 1.307 + end 1.308 + else 1.309 + if #entries == 1 then 1.310 + heading = _"Disapproval [single entry]" 1.311 + else 1.312 + heading = _"Disapproval [many entries]" 1.313 end 1.314 - local initiator_names_string = table.concat(initiator_names, ", ") 1.315 - ui.container{ 1.316 - attr = { style = "float: right;" }, 1.317 - content = function() 1.318 - ui.link{ 1.319 - attr = { class = "clickable" }, 1.320 - content = _"Show", 1.321 - module = "initiative", 1.322 - view = "show", 1.323 - id = initiative.id 1.324 + end 1.325 + end 1.326 + ui.tag { 1.327 + tag = "div", 1.328 + attr = { class = "cathead" }, 1.329 + content = heading 1.330 + } 1.331 + for i, initiative in ipairs(entries) do 1.332 + ui.container{ 1.333 + attr = { 1.334 + class = "movable", 1.335 + id = "entry_" .. tostring(initiative.id) 1.336 + }, 1.337 + content = function() 1.338 + local initiators_selector = initiative:get_reference_selector("initiating_members") 1.339 + :add_where("accepted") 1.340 + local initiators = initiators_selector:exec() 1.341 + local initiator_names = {} 1.342 + for i, initiator in ipairs(initiators) do 1.343 + initiator_names[#initiator_names+1] = initiator.name 1.344 + end 1.345 + local initiator_names_string = table.concat(initiator_names, ", ") 1.346 + ui.container{ 1.347 + attr = { style = "float: right;" }, 1.348 + content = function() 1.349 + ui.link{ 1.350 + attr = { class = "clickable" }, 1.351 + content = _"Show", 1.352 + module = "initiative", 1.353 + view = "show", 1.354 + id = initiative.id 1.355 + } 1.356 + slot.put(" ") 1.357 + ui.link{ 1.358 + attr = { class = "clickable", target = "_blank" }, 1.359 + content = _"(new window)", 1.360 + module = "initiative", 1.361 + view = "show", 1.362 + id = initiative.id 1.363 + } 1.364 + if not readonly then 1.365 + slot.put(" ") 1.366 + ui.image{ attr = { class = "grabber" }, static = "icons/grabber.png" } 1.367 + end 1.368 + end 1.369 + } 1.370 + if not readonly then 1.371 + ui.container{ 1.372 + attr = { style = "float: left;" }, 1.373 + content = function() 1.374 + ui.tag{ 1.375 + tag = "input", 1.376 + attr = { 1.377 + onclick = "voting_moveUp(this.parentNode.parentNode); return(false);", 1.378 + name = "move_up", 1.379 + value = initiative.id, 1.380 + class = not disabled and "clickable" or nil, 1.381 + type = "image", 1.382 + src = encode.url{ static = "icons/move_up.png" }, 1.383 + alt = _"Move up" 1.384 + } 1.385 + } 1.386 + slot.put(" ") 1.387 + ui.tag{ 1.388 + tag = "input", 1.389 + attr = { 1.390 + onclick = "voting_moveDown(this.parentNode.parentNode); return(false);", 1.391 + name = "move_down", 1.392 + value = initiative.id, 1.393 + class = not disabled and "clickable" or nil, 1.394 + type = "image", 1.395 + src = encode.url{ static = "icons/move_down.png" }, 1.396 + alt = _"Move down" 1.397 + } 1.398 + } 1.399 + slot.put(" ") 1.400 + end 1.401 } 1.402 - slot.put(" ") 1.403 - ui.link{ 1.404 - attr = { class = "clickable", target = "_blank" }, 1.405 - content = _"(new window)", 1.406 - module = "initiative", 1.407 - view = "show", 1.408 - id = initiative.id 1.409 - } 1.410 - slot.put(" ") 1.411 - ui.image{ attr = { class = "grabber" }, static = "icons/grabber.png" } 1.412 end 1.413 - } 1.414 - slot.put(encode.html(initiative.shortened_name)) 1.415 - if #initiators > 1 then 1.416 ui.container{ 1.417 - attr = { style = "font-size: 80%;" }, 1.418 - content = _"Initiators" .. ": " .. initiator_names_string 1.419 - } 1.420 - else 1.421 - ui.container{ 1.422 - attr = { style = "font-size: 80%;" }, 1.423 - content = _"Initiator" .. ": " .. initiator_names_string 1.424 + content = function() 1.425 + slot.put(encode.html(initiative.shortened_name)) 1.426 + if #initiators > 1 then 1.427 + ui.container{ 1.428 + attr = { style = "font-size: 80%;" }, 1.429 + content = _"Initiators" .. ": " .. initiator_names_string 1.430 + } 1.431 + else 1.432 + ui.container{ 1.433 + attr = { style = "font-size: 80%;" }, 1.434 + content = _"Initiator" .. ": " .. initiator_names_string 1.435 + } 1.436 + end 1.437 + end 1.438 } 1.439 end 1.440 - end 1.441 - } 1.442 + } 1.443 + end 1.444 end 1.445 - end 1.446 - } 1.447 + } 1.448 + end 1.449 end 1.450 end 1.451 } 1.452 - ui.tag{ 1.453 - tag = "input", 1.454 - attr = { 1.455 - type = "button", 1.456 - class = "voting_done", 1.457 - value = _"Finish voting" 1.458 + if not readonly then 1.459 + ui.tag{ 1.460 + tag = "input", 1.461 + attr = { 1.462 + type = "button", 1.463 + class = "voting_done", 1.464 + value = _"Finish voting" 1.465 + } 1.466 } 1.467 - } 1.468 + end 1.469 end 1.470 } 1.471