liquid_feedback_frontend

diff app/main/timeline/index.lua @ 11:77d58efe99fd

Version beta7

Important security fixes:
- Added missing HTML encoding to postal address of member
- Link to discussion URL only if it starts with http(s)://

Other bugfixes:
- Fixed wrong display of 2nd level delegating voters for an initiative
- Do not display invited initiators as initiators while voting
- Added missing translation

New features:
- Public message of the day
- Both direct and indirect supporter count is shown in tab heads
- Support shown in initiative lists

Language chooser at the login page has been added (again)
author bsw
date Fri Jan 22 12:00:00 2010 +0100 (2010-01-22)
parents 72c5e0ee7c98
children 00d1004545f1
line diff
     1.1 --- a/app/main/timeline/index.lua	Sun Jan 10 12:00:00 2010 +0100
     1.2 +++ b/app/main/timeline/index.lua	Fri Jan 22 12:00:00 2010 +0100
     1.3 @@ -1,3 +1,12 @@
     1.4 +execute.view{
     1.5 +  module = "timeline",
     1.6 +  view = "_constants"
     1.7 +}
     1.8 +
     1.9 +local options_box_count = param.get("options_box_count", atom.number) or 1
    1.10 +if options_box_count > 10 then
    1.11 +  options_box_count = 10
    1.12 +end
    1.13  
    1.14  local function format_dow(dow)
    1.15    local dows = {
    1.16 @@ -11,141 +20,331 @@
    1.17    }
    1.18    return dows[dow+1]
    1.19  end
    1.20 +slot.put_into("title", _"Timeline")
    1.21  
    1.22 -slot.put_into("title", _"Global timeline")
    1.23 +slot.select("actions", function()
    1.24 +  local setting_key = "liquidfeedback_frontend_timeline_current_options"
    1.25 +  local setting = Setting:by_pk(app.session.member.id, setting_key)
    1.26 +  local current_options = ""
    1.27 +  if setting then
    1.28 +    current_options = setting.value
    1.29 +  end
    1.30 +  local setting_maps = app.session.member:get_setting_maps_by_key("timeline_filters")
    1.31 +  for i, setting_map in ipairs(setting_maps) do
    1.32 +    local active
    1.33 +    local options_string = setting_map.value
    1.34 +    local name = setting_map.subkey
    1.35 +    if options_string == current_options then
    1.36 +      active = true
    1.37 +    end
    1.38 +    timeline_params.date = param.get("date")
    1.39 +    ui.link{
    1.40 +      attr = { class = active and "action_active" or nil },
    1.41 +      content = function()
    1.42 +        ui.image{ static = "icons/16/time.png" }
    1.43 +        slot.put(encode.html(name))
    1.44 +      end,
    1.45 +      module = 'timeline',
    1.46 +      action = 'update',
    1.47 +      params = {
    1.48 +        options_string = options_string
    1.49 +      },
    1.50 +    }
    1.51 +  end
    1.52 +  if #setting_maps > 0 then
    1.53 +    ui.link{
    1.54 +      content = function()
    1.55 +        ui.image{ static = "icons/16/wrench.png" }
    1.56 +        slot.put(_"Manage filter")
    1.57 +      end,
    1.58 +      module = "timeline",
    1.59 +      view = "list_filter",
    1.60 +    }
    1.61 +  end
    1.62 +  ui.link{
    1.63 +    content = function()
    1.64 +      ui.image{ static = "icons/16/bullet_disk.png" }
    1.65 +      slot.put(_"Save current filter")
    1.66 +    end,
    1.67 +    module = "timeline",
    1.68 +    view = "save_filter",
    1.69 +    attr = { 
    1.70 +      onclick = "el=document.getElementById('timeline_save');el.checked=true;el.form.submit();return(false);"
    1.71 +    }
    1.72 +  }
    1.73 +end)
    1.74  
    1.75 +util.help("timeline.index", _"Timeline")
    1.76  
    1.77  ui.form{
    1.78 -  attr = { class = "vertical" },
    1.79    module = "timeline",
    1.80 -  view = "index",
    1.81 -  method = "get",
    1.82 +  action = "update",
    1.83    content = function()
    1.84 -    local tmp = db:query("select EXTRACT(DOW FROM date) as dow, date FROM (SELECT (now() - (to_char(days_before, '0') || ' days')::interval)::date as date from (select generate_series(0,7) as days_before) as series) as date; ")
    1.85 -    local today = tmp[1].date
    1.86 -    for i, record in ipairs(tmp) do
    1.87 -      local content
    1.88 -      if i == 1 then
    1.89 -        content = _"Today"
    1.90 -      elseif i == 2 then
    1.91 -        content = _"Yesterday"
    1.92 -      else
    1.93 -        content = format_dow(record.dow)
    1.94 -      end
    1.95 -      ui.link{
    1.96 -        content = content,
    1.97 -        attr = { onclick = "el = document.getElementById('timeline_search_date'); el.value = '" .. tostring(record.date) .. "'; el.form.submit(); return(false);" },
    1.98 -        module = "timeline",
    1.99 -        view = "index",
   1.100 -        params = { date = record.date }
   1.101 -      }
   1.102 -      slot.put(" ")
   1.103 +
   1.104 +
   1.105 +    ui.tag{
   1.106 +      tag = "label",
   1.107 +      attr = { style = "font-size: 130%;" },
   1.108 +      content = _"Date" .. ":"
   1.109 +    }
   1.110 +    slot.put(" ")
   1.111 +    local date = param.get("date")
   1.112 +    if not date or #date == 0 then
   1.113 +      date = tostring(db:query("select now()::date as date")[1].date)
   1.114      end
   1.115 -    ui.field.hidden{
   1.116 -      attr = { id = "timeline_search_date" },
   1.117 -      name = "date",
   1.118 -      value = param.get("date") or today
   1.119 +    ui.tag{
   1.120 +      tag = "input",
   1.121 +      attr = {
   1.122 +        type = "text",
   1.123 +        id = "timeline_search_date",
   1.124 +        style = "width: 10em;",
   1.125 +        onchange = "this.form.submit();",
   1.126 +        name = "date",
   1.127 +        value = date
   1.128 +      },
   1.129 +      content = function() end
   1.130 +    }
   1.131 +
   1.132 +    ui.script{ static = "gregor.js/gregor.js" }
   1.133 +    util.gregor("timeline_search_date", "document.getElementById('timeline_search_date').form.submit();")
   1.134 +
   1.135 +
   1.136 +    ui.link{
   1.137 +      attr = { style = "margin-left: 1em; font-size: 130%; font-weight: bold;", onclick = "document.getElementById('timeline_search_date').form.submit();return(false);" },
   1.138 +      content = function()
   1.139 +        ui.image{
   1.140 +          attr = { style = "margin-right: 0.25em;" },
   1.141 +          static = "icons/16/magnifier.png"
   1.142 +        }
   1.143 +        slot.put(_"Search")
   1.144 +      end,
   1.145 +      external = "#",
   1.146 +    }
   1.147 +    local show_options = param.get("show_options", atom.boolean)
   1.148 +    ui.link{
   1.149 +      attr = { style = "margin-left: 1em; font-size: 130%;", onclick = "el=document.getElementById('timeline_show_options');el.checked=" .. tostring(not show_options) .. ";el.form.submit();return(false);" },
   1.150 +      content = function()
   1.151 +        ui.image{
   1.152 +          attr = { style = "margin-right: 0.25em;" },
   1.153 +          static = "icons/16/text_list_bullets.png"
   1.154 +        }
   1.155 +        slot.put(not show_options and _"Show filter details" or _"Hide filter details")
   1.156 +      end,
   1.157 +      external = "#",
   1.158 +    }
   1.159 +
   1.160 +    ui.field.boolean{
   1.161 +      attr = { id = "timeline_show_options", style = "display: none;", onchange="this.form.submit();" },
   1.162 +      name = "show_options",
   1.163 +      value = param.get("show_options", atom.boolean)
   1.164 +    }
   1.165 +
   1.166 +    ui.field.boolean{
   1.167 +      attr = { id = "timeline_save", style = "display: none;", onchange="this.form.submit();" },
   1.168 +      name = "save",
   1.169 +      value = false
   1.170      }
   1.171 -    ui.field.select{
   1.172 -      attr = { onchange = "this.form.submit();" },
   1.173 -      name = "per_page",
   1.174 -      label = _"Issues per page",
   1.175 -      foreign_records = {
   1.176 -        { id = "10",  name = "10"   },
   1.177 -        { id = "25",  name = "25"   },
   1.178 -        { id = "50",  name = "50"   },
   1.179 -        { id = "100", name = "100"  },
   1.180 -        { id = "250", name = "250"  },
   1.181 -        { id = "all", name = _"All" },
   1.182 +
   1.183 +    ui.container{
   1.184 +      attr = { 
   1.185 +        id = "timeline_options_boxes",
   1.186 +        class = "vertical",
   1.187 +        style = not param.get("show_options", atom.boolean) and "display: none;" or nil
   1.188        },
   1.189 -      foreign_id = "id",
   1.190 -      foreign_name = "name",
   1.191 -      value = param.get("per_page")
   1.192 -    }
   1.193 -    local initiatives_per_page = param.get("initiatives_per_page", atom.integer) or 3
   1.194 +      content = function()
   1.195 +
   1.196 +        local function option_field(event_ident, filter_ident)
   1.197 +          local param_name
   1.198 +          if not filter_ident then
   1.199 +            param_name = "option_" .. event_ident
   1.200 +          else
   1.201 +            param_name = "option_" .. event_ident .. "_" .. filter_ident
   1.202 +          end
   1.203 +          local value = param.get(param_name, atom.boolean)
   1.204 +          ui.field.boolean{
   1.205 +            attr = { id = param_name },
   1.206 +            name = param_name,
   1.207 +            value = value,
   1.208 +          }
   1.209 +        end
   1.210 +
   1.211 +        local function filter_option_fields(event_ident, filter_idents)
   1.212 +
   1.213 +          for i, filter_ident in ipairs(filter_idents) do
   1.214 +              slot.put("<td>")
   1.215 +              option_field(event_ident, filter_ident)
   1.216 +              slot.put("</td><td><div class='ui_field_label label_right'>")
   1.217 +              ui.tag{
   1.218 +                attr = { ["for"] = "option_" .. event_ident .. "_" .. filter_ident },
   1.219 +                tag = "label",
   1.220 +                content = filter_names[filter_ident]
   1.221 +              }
   1.222 +              slot.put("</div></td>")
   1.223 +          end
   1.224 +
   1.225 +        end
   1.226  
   1.227 -    ui.field.select{
   1.228 -      attr = { onchange = "this.form.submit();" },
   1.229 -      name = "initiatives_per_page",
   1.230 -      label = _"Initiatives per page",
   1.231 -      foreign_records = {
   1.232 -        { id = 1,   name = "1"  },
   1.233 -        { id = 3,   name = "3"  },
   1.234 -        { id = 5,   name = "5"  },
   1.235 -        { id = 10,  name = "10" },
   1.236 -        { id = 25,  name = "25" },
   1.237 -        { id = 50,  name = "50" },
   1.238 -      },
   1.239 -      foreign_id = "id",
   1.240 -      foreign_name = "name",
   1.241 -      value = initiatives_per_page
   1.242 +        local event_groups = {
   1.243 +          {
   1.244 +            title = _"Issue events",
   1.245 +            event_idents = {
   1.246 +              "issue_created",
   1.247 +              "issue_canceled",
   1.248 +              "issue_accepted",
   1.249 +              "issue_half_frozen",
   1.250 +              "issue_finished_without_voting",
   1.251 +              "issue_voting_started",
   1.252 +              "issue_finished_after_voting",
   1.253 +            },
   1.254 +            filter_idents = {
   1.255 +              "membership",
   1.256 +              "interested"
   1.257 +            }
   1.258 +          },
   1.259 +          {
   1.260 +            title = _"Initiative events",
   1.261 +            event_idents = {
   1.262 +              "initiative_created",
   1.263 +              "initiative_revoked",
   1.264 +              "draft_created",
   1.265 +              "suggestion_created",
   1.266 +            },
   1.267 +            filter_idents = {
   1.268 +              "membership",
   1.269 +              "interested",
   1.270 +              "supporter",
   1.271 +              "potential_supporter",
   1.272 +              "initiator"
   1.273 +            }
   1.274 +          }
   1.275 +        }
   1.276 +
   1.277 +        slot.put("<br />")
   1.278 +
   1.279 +        slot.put("<table>")
   1.280 +
   1.281 +        for i_event_group, event_group in ipairs(event_groups) do
   1.282 +          slot.put("<tr>")
   1.283 +          slot.put("<th colspan='2'>")
   1.284 +          slot.put(event_group.title)
   1.285 +          slot.put("</th><th colspan='10'>")
   1.286 +          slot.put(_"Show only events which match... (or associtated)")
   1.287 +          slot.put("</th>")
   1.288 +          slot.put("</tr>")
   1.289 +          local event_idents = event_group.event_idents
   1.290 +          for i, event_ident in ipairs(event_idents) do
   1.291 +            slot.put("<tr><td>")
   1.292 +            option_field(event_ident)
   1.293 +            slot.put("</td><td><div class='ui_field_label label_right'>")
   1.294 +            ui.tag{
   1.295 +              attr = { ["for"] = "option_" .. event_ident },
   1.296 +              tag = "label",
   1.297 +              content = event_names[event_ident]
   1.298 +            }
   1.299 +            slot.put("</div></td>")
   1.300 +            filter_option_fields(event_ident, event_group.filter_idents)
   1.301 +            slot.put("</tr>")
   1.302 +          end
   1.303 +        end
   1.304 +
   1.305 +        slot.put("</table>")
   1.306 +
   1.307 +      end
   1.308      }
   1.309    end
   1.310  }
   1.311  
   1.312  local date = param.get("date")
   1.313 -if not date then
   1.314 +if not date or #date == 0 then
   1.315    date = "today"
   1.316  end
   1.317 -local issues_selector = db:new_selector()
   1.318 -issues_selector._class = Issue
   1.319 +
   1.320 +local timeline_selector
   1.321 +
   1.322 +for event, event_name in pairs(event_names) do
   1.323 +
   1.324 +  if param.get("option_" .. event, atom.boolean) then
   1.325 +
   1.326 +    local tmp = Timeline:new_selector()
   1.327 +      :add_where{ "occurrence::date = ?", date }
   1.328 +
   1.329 +      :left_join("draft", nil, "draft.id = timeline.draft_id")
   1.330 +      :left_join("suggestion", nil, "suggestion.id = timeline.suggestion_id")
   1.331 +      :left_join("initiative", nil, "initiative.id = timeline.initiative_id or initiative.id = draft.initiative_id or initiative.id = suggestion.initiative_id")
   1.332 +      :left_join("issue", nil, "issue.id = timeline.issue_id or issue.id = initiative.issue_id")
   1.333 +      :left_join("area", nil, "area.id = issue.area_id")
   1.334 +
   1.335 +      :left_join("interest", "_interest", { "_interest.issue_id = issue.id AND _interest.member_id = ?", app.session.member.id} )
   1.336 +      :left_join("membership", "_membership", { "_membership.area_id = area.id AND _membership.member_id = ?", app.session.member.id} )
   1.337 +      :left_join("initiator", "_initiator", { "_initiator.initiative_id = initiative.id AND _initiator.member_id = ?", app.session.member.id} )
   1.338 +      :left_join("supporter", "_supporter", { "_supporter.initiative_id = initiative.id AND _supporter.member_id = ?", app.session.member.id} )
   1.339 +
   1.340 +      :add_field("(_interest.member_id NOTNULL)", "is_interested")
   1.341 +      :add_field("(_initiator.member_id NOTNULL)", "is_initiator")
   1.342 +      :add_field({"(_supporter.member_id NOTNULL) AND NOT EXISTS(SELECT NULL FROM opinion WHERE opinion.initiative_id = initiative.id AND opinion.member_id = ? AND ((opinion.degree = 2 AND NOT fulfilled) OR (opinion.degree = -2 AND fulfilled)) LIMIT 1)", app.session.member.id }, "is_supporter")
   1.343 +      :add_field({"EXISTS(SELECT NULL FROM opinion WHERE opinion.initiative_id = initiative.id AND opinion.member_id = ? AND ((opinion.degree = 2 AND NOT fulfilled) OR (opinion.degree = -2 AND fulfilled)) LIMIT 1)", app.session.member.id }, "is_potential_supporter")
   1.344 +  --    :left_join("member", nil, "member.id = timeline.member_id")
   1.345 +
   1.346 +    tmp:add_where{ "event = ?", event }
   1.347 +
   1.348 +    local filters = {}
   1.349 +    if param.get("option_" .. event .. "_membership", atom.boolean) then
   1.350 +      filters[#filters+1] = "(timeline.initiative_id ISNULL AND timeline.issue_id ISNULL AND timeline.draft_id ISNULL AND timeline.suggestion_id ISNULL) OR _membership.member_id NOTNULL"
   1.351 +    end
   1.352 +
   1.353 +    if param.get("option_" .. event .. "_supporter", atom.boolean) then
   1.354 +      filters[#filters+1] = "(timeline.initiative_id ISNULL AND timeline.issue_id ISNULL AND timeline.draft_id ISNULL AND timeline.suggestion_id ISNULL) OR ((_supporter.member_id NOTNULL) AND NOT EXISTS(SELECT NULL FROM opinion WHERE opinion.initiative_id = initiative.id AND opinion.member_id = ? AND ((opinion.degree = 2 AND NOT fulfilled) OR (opinion.degree = -2 AND fulfilled)) LIMIT 1))"
   1.355 +    end
   1.356 +
   1.357 +    if param.get("option_" .. event .. "_potential_supporter", atom.boolean) then
   1.358 +      filters[#filters+1] = "(timeline.initiative_id ISNULL AND timeline.issue_id ISNULL AND timeline.draft_id ISNULL AND timeline.suggestion_id ISNULL) OR ((_supporter.member_id NOTNULL) AND EXISTS(SELECT NULL FROM opinion WHERE opinion.initiative_id = initiative.id AND opinion.member_id = ? AND ((opinion.degree = 2 AND NOT fulfilled) OR (opinion.degree = -2 AND fulfilled)) LIMIT 1))"
   1.359 +    end
   1.360  
   1.361 -issues_selector
   1.362 -  :add_field("*")
   1.363 -  :add_where{ "sort::date = ?", date }
   1.364 -  :add_from{ "($) as issue", {
   1.365 -    Issue:new_selector()
   1.366 -      :add_field("''", "old_state")
   1.367 -      :add_field("'new'", "new_state")
   1.368 -      :add_field("created", "sort")
   1.369 -    :union(Issue:new_selector()
   1.370 -      :add_field("'new'", "old_state")
   1.371 -      :add_field("'accepted'", "new_state")
   1.372 -      :add_field("accepted", "sort")
   1.373 -      :add_where("accepted NOTNULL")
   1.374 -    ):union(Issue:new_selector()
   1.375 -      :add_field("'accepted'", "old_state")
   1.376 -      :add_field("'frozen'", "new_state")
   1.377 -      :add_field("half_frozen", "sort")
   1.378 -      :add_where("half_frozen NOTNULL")
   1.379 -    ):union(Issue:new_selector()
   1.380 -      :add_field("'frozen'", "old_state")
   1.381 -      :add_field("'voting'", "new_state")
   1.382 -      :add_field("fully_frozen", "sort")
   1.383 -      :add_where("fully_frozen NOTNULL")
   1.384 -    ):union(Issue:new_selector()
   1.385 -      :add_field("'new'", "old_state")
   1.386 -      :add_field("'cancelled'", "new_state")
   1.387 -      :add_field("closed", "sort")
   1.388 -      :add_where("closed NOTNULL AND accepted ISNULL")
   1.389 -    ):union(Issue:new_selector()
   1.390 -      :add_field("'accepted'", "old_state")
   1.391 -      :add_field("'cancelled'", "new_state")
   1.392 -      :add_field("closed", "sort")
   1.393 -      :add_where("closed NOTNULL AND half_frozen ISNULL AND accepted NOTNULL")
   1.394 -    ):union(Issue:new_selector()
   1.395 -      :add_field("'frozen'", "old_state")
   1.396 -      :add_field("'cancelled'", "new_state")
   1.397 -      :add_field("closed", "sort")
   1.398 -      :add_where("closed NOTNULL AND fully_frozen ISNULL AND half_frozen NOTNULL")
   1.399 -    ):union(Issue:new_selector()
   1.400 -      :add_field("'voting'", "old_state")
   1.401 -      :add_field("'finished'", "new_state")
   1.402 -      :add_field("closed", "sort")
   1.403 -      :add_where("closed NOTNULL AND fully_frozen NOTNULL AND half_frozen ISNULL")
   1.404 -    )
   1.405 +    if param.get("option_" .. event .. "_interested", atom.boolean) then
   1.406 +      filters[#filters+1] = "(timeline.initiative_id ISNULL AND timeline.issue_id ISNULL AND timeline.draft_id ISNULL AND timeline.suggestion_id ISNULL) OR _interest.member_id NOTNULL"
   1.407 +    end
   1.408 +
   1.409 +    if param.get("option_" .. event .. "_initiator", atom.boolean) then
   1.410 +      filters[#filters+1] = "(timeline.initiative_id ISNULL AND timeline.issue_id ISNULL AND timeline.draft_id ISNULL AND timeline.suggestion_id ISNULL) OR _initiator.member_id NOTNULL"
   1.411 +    end
   1.412 +
   1.413 +    if #filters > 0 then
   1.414 +      local filter_string = "(" .. table.concat(filters, ") OR (") .. ")"
   1.415 +      tmp:add_where{ filter_string, app.session.member.id }
   1.416 +    end
   1.417 +  
   1.418 +    if not timeline_selector then
   1.419 +      timeline_selector = tmp
   1.420 +    else
   1.421 +      timeline_selector:union_all(tmp)
   1.422 +    end
   1.423 +  end
   1.424 +end
   1.425 +
   1.426 +if timeline_selector then
   1.427 +  
   1.428 +  local initiatives_per_page = param.get("initiatives_per_page", atom.number)
   1.429 +  
   1.430 +  local outer_timeline_selector = db:new_selector()
   1.431 +  outer_timeline_selector._class = Timeline
   1.432 +  outer_timeline_selector:add_field{ "timeline.*" }
   1.433 +  outer_timeline_selector:from({"($)", { timeline_selector }}, "timeline" )
   1.434 +  outer_timeline_selector:add_order_by("occurrence DESC")
   1.435 +  
   1.436 +  slot.put("<br />")
   1.437 +  execute.view{
   1.438 +    module = "timeline",
   1.439 +    view = "_list",
   1.440 +    params = {
   1.441 +      timeline_selector = outer_timeline_selector,
   1.442 +      per_page = param.get("per_page", atom.number),
   1.443 +      event_names = event_names,
   1.444 +      initiatives_per_page = initiatives_per_page
   1.445 +    }
   1.446    }
   1.447 -}
   1.448 +
   1.449 +else
   1.450  
   1.451 -execute.view{
   1.452 -  module = "issue",
   1.453 -  view = "_list",
   1.454 -  params = {
   1.455 -    issues_selector = issues_selector,
   1.456 -    initiatives_per_page = param.get("initiatives_per_page", atom.number),
   1.457 -    initiatives_no_sort = true,
   1.458 -    no_filter = true,
   1.459 -    no_sort = true,
   1.460 -    per_page = param.get("per_page"),
   1.461 -  }
   1.462 -}
   1.463 +  slot.put(_"No events selected to list")
   1.464 +
   1.465 +end
   1.466 \ No newline at end of file

Impressum / About Us