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
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("&nbsp;")
   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("&nbsp;")
   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  

Impressum / About Us