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