Anonymous avatar Anonymous committed 72c5e0e

Version beta6

Bugfixes:
- Security fix: Every user was able to change the discussion URL of an initiative
- Creation of new issues in areas without default policies is now possible
- Members can now be sorted in different ways
- No error when trying to compare a draft with itself
- Added missing local statement to variable initialization in app/main/delegation/new.lua
- CSS flaw in initiative action bar fixed

New features:
- Possiblity to invite other users to become initiator
- Revokation of initiatives implemented
- Number of suggestions, supporters, etc. is shown on corresponding tabs of initiative view
- Members can now be sorted by account creation (default sorting is "newest first")
- Configuration option to create an automatic discussion link for all issues
- First draft of global timeline feature (not accessible via link yet)
- Custom stylesheet URL for users marked as developers

In area listing the number of closed issues is shown too

Renamed "author" field of initiative to "last author"

Removed wrongly included file app/main/member/_show_thumb.lua.orig in the distribution

Help texts updated

Comments (0)

Files changed (48)

app/main/_filter_view/34_stylesheet.lua

+local value
+if app.session.member then
+  local setting_key = "liquidfeedback_frontend_stylesheet_url"
+  local setting = Setting:by_pk(app.session.member.id, setting_key)
+  value = setting and setting.value
+end
+
+if value then
+  slot.put_into("stylesheet_url", value)
+else
+  slot.put_into("stylesheet_url", config.absolute_base_url .. "static/style.css")
+end
+
+execute.inner()

app/main/_layout/default.html

     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
     <title><!-- WEBMCP SLOTNODIV app_name --></title>
     <link rel="stylesheet" type="text/css" media="screen" href="__BASEURL__/static/trace.css" />
-    <link rel="stylesheet" type="text/css" media="screen" href="__BASEURL__/static/style.css" />
+    <link rel="stylesheet" type="text/css" media="screen" href="<!-- WEBMCP SLOTNODIV stylesheet_url -->" />
     <!-- WEBMCP SLOTNODIV html_head -->
   </head>
   <body>

app/main/area/_list.lua

   :add_field("(SELECT COUNT(*) FROM issue WHERE issue.area_id = area.id AND issue.half_frozen NOTNULL AND issue.fully_frozen ISNULL AND issue.closed ISNULL)", "issues_frozen_count")
   :add_field("(SELECT COUNT(*) FROM issue WHERE issue.area_id = area.id AND issue.fully_frozen NOTNULL AND issue.closed ISNULL)", "issues_voting_count")
   :add_field({ "(SELECT COUNT(*) FROM issue LEFT JOIN direct_voter ON direct_voter.issue_id = issue.id AND direct_voter.member_id = ? WHERE issue.area_id = area.id AND issue.fully_frozen NOTNULL AND issue.closed ISNULL AND direct_voter.member_id ISNULL)", app.session.member.id }, "issues_to_vote_count")
+  :add_field("(SELECT COUNT(*) FROM issue WHERE issue.area_id = area.id AND issue.fully_frozen NOTNULL AND issue.closed NOTNULL)", "issues_finished_count")
+  :add_field("(SELECT COUNT(*) FROM issue WHERE issue.area_id = area.id AND issue.fully_frozen ISNULL AND issue.closed NOTNULL)", "issues_cancelled_count")
 
 ui.order{
   name = name,
               params = { filter = "frozen", filter_voting = "not_voted" }
             }
           end
-        }
+        },
+        {
+          label = _"Finished",
+          field_attr = { style = "text-align: right;" },
+          content = function(record)
+            ui.link{
+              text = tostring(record.issues_finished_count),
+              module = "area",
+              view = "show",
+              id = record.id,
+              params = { filter = "finished", issue_list = "newest" }
+            }
+          end
+        },
+        {
+          label = _"Cancelled",
+          field_attr = { style = "text-align: right;" },
+          content = function(record)
+            ui.link{
+              text = tostring(record.issues_cancelled_count),
+              module = "area",
+              view = "show",
+              id = record.id,
+              params = { filter = "cancelled", issue_list = "newest" }
+            }
+          end
+        },
       }
     }
   end

app/main/delegation/new.lua

     }
   },
   content = function()
-    records = {
+    local records = {
       {
         id = "-1",
         name = _"No delegation"

app/main/draft/_action/add.lua

   return false
 end
 
-if Initiator:by_pk(initiative.id, app.session.member.id) then
-  local draft = Draft:new()
-  draft.author_id = app.session.member.id
-  draft.initiative_id = initiative.id
-  local formatting_engine = param.get("formatting_engine")
-  local formatting_engine_valid = false
-  for fe, dummy in pairs(config.formatting_engine_executeables) do
-    if formatting_engine == fe then
-      formatting_engine_valid = true
-    end
+local initiator = Initiator:by_pk(initiative.id, app.session.member.id)
+if not initiator or not initiator.accepted then
+  error("access denied")
+end
+
+local draft = Draft:new()
+draft.author_id = app.session.member.id
+draft.initiative_id = initiative.id
+local formatting_engine = param.get("formatting_engine")
+local formatting_engine_valid = false
+for fe, dummy in pairs(config.formatting_engine_executeables) do
+  if formatting_engine == fe then
+    formatting_engine_valid = true
   end
-  if not formatting_engine_valid then
-    error("invalid formatting engine!")
-  end
-  draft.formatting_engine = formatting_engine
-  draft.content = param.get("content")
-  draft:save()
+end
+if not formatting_engine_valid then
+  error("invalid formatting engine!")
+end
+draft.formatting_engine = formatting_engine
+draft.content = param.get("content")
+draft:save()
 
-  slot.put_into("notice", _"New draft has been added to initiative")
+slot.put_into("notice", _"New draft has been added to initiative")
 
-else
-  error('access denied')
-end

app/main/draft/_show.lua

   readonly = true,
   content = function()
 
-    ui.field.text{ label = _"Author", name = "author_name" }
+    ui.field.text{ label = _"Last author", name = "author_name" }
     ui.field.timestamp{ label = _"Created at", name = "created" }
     ui.container{
       attr = { class = "draft_content wiki" },

app/main/draft/diff.lua

 local old_draft_id = param.get("old_draft_id", atom.integer)
 local new_draft_id = param.get("new_draft_id", atom.integer)
 
+if not old_draft_id or not new_draft_id then
+  slot.put( _"Please choose two versions of the draft to compare")
+  return
+end
+
+if old_draft_id == new_draft_id then
+  slot.put( _"Please choose two different versions of the draft to compare")
+  return
+end
+
 if old_draft_id > new_draft_id then
   local tmp = old_draft_id
   old_draft_id = new_draft_id

app/main/index/index.lua

 slot.select("title", function()
-  execute.view{
-    module = "member_image",
-    view = "_show",
-    params = {
-      member = app.session.member, 
-      image_type = "avatar"
+  if app.session.member then
+    execute.view{
+      module = "member_image",
+      view = "_show",
+      params = {
+        member = app.session.member,
+        image_type = "avatar"
+      }
     }
-  }
+  end
 end)
 
 slot.select("title", function()
 
 slot.select("actions", function()
 
-  ui.link{
-    content = function()
-        ui.image{ static = "icons/16/application_form.png" }
-        slot.put(_"Edit my profile")
-    end,
-    module = "member",
-    view = "edit"
-  }
-
-  ui.link{
-    content = function()
-        ui.image{ static = "icons/16/user_gray.png" }
-        slot.put(_"Upload images")
-    end,
-    module = "member",
-    view = "edit_images"
-  }
-
-  execute.view{
-    module = "delegation",
-    view = "_show_box"
-  }
-
-  ui.link{
-    content = function()
-        ui.image{ static = "icons/16/wrench.png" }
-        slot.put(_"Settings")
-    end,
-    module = "member",
-    view = "settings"
-  }
-
-  if config.download_dir then
+  if app.session.member then
     ui.link{
       content = function()
-          ui.image{ static = "icons/16/database_save.png" }
-          slot.put(_"Download")
+          ui.image{ static = "icons/16/application_form.png" }
+          slot.put(_"Edit my profile")
       end,
-      module = "index",
-      view = "download"
+      module = "member",
+      view = "edit"
     }
-  end 
+  
+    ui.link{
+      content = function()
+          ui.image{ static = "icons/16/user_gray.png" }
+          slot.put(_"Upload images")
+      end,
+      module = "member",
+      view = "edit_images"
+    }
+  
+    execute.view{
+      module = "delegation",
+      view = "_show_box"
+    }
+  
+    ui.link{
+      content = function()
+          ui.image{ static = "icons/16/wrench.png" }
+          slot.put(_"Settings")
+      end,
+      module = "member",
+      view = "settings"
+    }
+  
+    if config.download_dir then
+      ui.link{
+        content = function()
+            ui.image{ static = "icons/16/database_save.png" }
+            slot.put(_"Download")
+        end,
+        module = "index",
+        view = "download"
+      }
+    end 
+  end
 end)
 
 local lang = locale.get("lang")
 
 util.help("index.index", _"Home")
 
-
-local selector = Area:new_selector()
-  :reset_fields()
-  :add_field("area.id", nil, { "grouped" })
-  :add_field("area.name", nil, { "grouped" })
-  :add_field("membership.member_id NOTNULL", "is_member", { "grouped" })
-  :add_field("count(issue.id)", "issues_to_vote_count")
-  :add_field("count(interest.member_id)", "interested_issues_to_vote_count")
-  :join("issue", nil, "issue.area_id = area.id AND issue.fully_frozen NOTNULL AND issue.closed ISNULL")
-  :left_join("direct_voter", nil, { "direct_voter.issue_id = issue.id AND direct_voter.member_id = ?", app.session.member.id })
-  :add_where{ "direct_voter.member_id ISNULL" }
-  :left_join("interest", nil, { "interest.issue_id = issue.id AND interest.member_id = ?", app.session.member.id })
-  :left_join("membership", nil, { "membership.area_id = area.id AND membership.member_id = ? ", app.session.member.id })
-
 local areas = {}
-for i, area in ipairs(selector:exec()) do
-  if area.is_member or area.interested_issues_to_vote_count > 0 then
-    areas[#areas+1] = area
+if app.session.member then
+  local selector = Area:new_selector()
+    :reset_fields()
+    :add_field("area.id", nil, { "grouped" })
+    :add_field("area.name", nil, { "grouped" })
+    :add_field("membership.member_id NOTNULL", "is_member", { "grouped" })
+    :add_field("count(issue.id)", "issues_to_vote_count")
+    :add_field("count(interest.member_id)", "interested_issues_to_vote_count")
+    :join("issue", nil, "issue.area_id = area.id AND issue.fully_frozen NOTNULL AND issue.closed ISNULL")
+    :left_join("direct_voter", nil, { "direct_voter.issue_id = issue.id AND direct_voter.member_id = ?", app.session.member.id })
+    :add_where{ "direct_voter.member_id ISNULL" }
+    :left_join("interest", nil, { "interest.issue_id = issue.id AND interest.member_id = ?", app.session.member.id })
+    :left_join("membership", nil, { "membership.area_id = area.id AND membership.member_id = ? ", app.session.member.id })
+  
+  for i, area in ipairs(selector:exec()) do
+    if area.is_member or area.interested_issues_to_vote_count > 0 then
+      areas[#areas+1] = area
+    end
   end
 end
 
   }
 end
 
-execute.view{
-  module = "member",
-  view = "_show",
-  params = { member = app.session.member }
-}
+local initiatives_selector = Initiative:new_selector()
+  :join("initiator", nil, { "initiator.initiative_id = initiative.id AND initiator.member_id = ? AND initiator.accepted ISNULL", app.session.member.id })
 
+if initiatives_selector:count() > 0 then
+  ui.container{
+    attr = { style = "font-weight: bold;" },
+    content = _"Initiatives that invited you to become initiator:"
+  }
+
+  execute.view{
+    module = "initiative",
+    view = "_list",
+    params = { initiatives_selector = initiatives_selector }
+  }
+end
+
+
+if app.session.member then
+  execute.view{
+    module = "member",
+    view = "_show",
+    params = { member = app.session.member }
+  }
+end

app/main/initiative/_action/accept_invitation.lua

+local initiator = Initiator:by_pk(param.get_id(), app.session.member.id)
+
+if not initiator then
+  slot.put_into("error", _"Sorry, but you are currently not invited")
+  return
+end
+
+-- TODO important m1 selectors returning result _SET_!
+local issue = initiator.initiative:get_reference_selector("issue"):for_share():single_object_mode():exec()
+
+if issue.closed then
+  slot.put_into("error", _"This issue is already closed.")
+  return false
+elseif issue.half_frozen then 
+  slot.put_into("error", _"This issue is already frozen.")
+  return false
+end
+
+if initiator.initiative.revoked then
+  slot.put_into("error", _"This initiative is revoked")
+  return false
+end
+
+if initiator.accepted then
+  slot.put_into("error", _"You are already initator")
+  return
+end
+
+initiator.accepted = true
+initiator:save()
+
+slot.put_into("notice", _"You are now initiator of this initiative")

app/main/initiative/_action/add_initiator.lua

+local initiative = Initiative:by_id(param.get("initiative_id"))
+local member = Member:by_id(param.get("member_id"))
+
+if not member then
+  slot.put_into("error", _"Please choose a member")
+  return false
+end
+
+local initiator = Initiator:by_pk(initiative.id, app.session.member.id)
+if not initiator or initiator.accepted ~= true then
+  error("access denied")
+end
+
+-- TODO important m1 selectors returning result _SET_!
+local issue = initiative:get_reference_selector("issue"):for_share():single_object_mode():exec()
+
+if issue.closed then
+  slot.put_into("error", _"This issue is already closed.")
+  return false
+elseif issue.half_frozen then 
+  slot.put_into("error", _"This issue is already frozen.")
+  return false
+end
+
+if initiative.revoked then
+  slot.put_into("error", _"This initiative is revoked")
+  return false
+end
+
+local initiator = Initiator:by_pk(initiative.id, member.id)
+if initiator then
+  if initiator.accepted == true then
+    slot.put_into("error", _"This member is already initiator of this initiative")
+  elseif initiator.accepted == false then
+    slot.put_into("error", _"This member has rejected to become initiator of this initiative")
+  elseif initiator.accepted == nil then
+    slot.put_into("error", _"This member is already invited to become initiator of this initiative")
+  end
+  return false
+end
+
+local initiator = Initiator:new()
+initiator.initiative_id = initiative.id
+initiator.member_id = member.id
+initiator.accepted = nil
+initiator:save()
+
+slot.put_into("notice", _"Member is now invited to be initiator")
+

app/main/initiative/_action/add_support.lua

   return false
 end
 
+if initiative.revoked then
+  slot.put_into("error", _"This initiative is revoked")
+  return false
+end
+
 local member = app.session.member
 
 local supporter = Supporter:by_pk(initiative.id, member.id)

app/main/initiative/_action/create.lua

   area = Area:new_selector():add_where{"id=?",area_id}:single_object_mode():exec()
 end
 
+local policy_id = param.get("policy_id", atom.integer)
+
+if policy_id == -1 then
+  slot.put_into("error", _"Please choose a policy")
+  return false
+end
+
 local name = param.get("name")
 
 local name = util.trim(name)
 local initiative = Initiative:new()
 
 if not issue then
-  local policy_id = param.get("policy_id", atom.integer)
   if not area:get_reference_selector("allowed_policies")
     :add_where{ "policy.id = ?", policy_id }
     :optional_object_mode()
 local initiator = Initiator:new()
 initiator.initiative_id = initiative.id
 initiator.member_id = app.session.member.id
+initiator.accepted = true
 initiator:save()
 
 local supporter = Supporter:new()

app/main/initiative/_action/reject_initiator_invitation.lua

+local initiative = Initiative:by_id(param.get("initiative_id"))
+local initiator = Initiator:by_pk(initiative.id, app.session.member.id)
+
+-- TODO important m1 selectors returning result _SET_!
+local issue = initiative:get_reference_selector("issue"):for_share():single_object_mode():exec()
+
+if issue.closed then
+  slot.put_into("error", _"This issue is already closed.")
+  return false
+elseif issue.half_frozen then 
+  slot.put_into("error", _"This issue is already frozen.")
+  return false
+end
+
+if initiator.accepted ~= nil then
+  error("access denied")
+end
+
+initiator.accepted = false
+initiator:save()
+
+slot.put_into("notice", _"Invitation has been refused")
+
+

app/main/initiative/_action/remove_initiator.lua

+local initiative = Initiative:by_id(param.get("initiative_id"))
+local initiator = Initiator:by_pk(initiative.id, app.session.member.id)
+
+-- TODO important m1 selectors returning result _SET_!
+local issue = initiative:get_reference_selector("issue"):for_share():single_object_mode():exec()
+
+if issue.closed then
+  slot.put_into("error", _"This issue is already closed.")
+  return false
+elseif issue.half_frozen then 
+  slot.put_into("error", _"This issue is already frozen.")
+  return false
+end
+
+if initiative.revoked then
+  slot.put_into("error", _"This initiative is revoked")
+  return false
+end
+
+local initiator_todelete = Initiator:by_pk(initiative.id, param.get("member_id", atom.integer))
+
+if not (initiator and initiator.accepted) and not (initiator.member_id == initiator_todelete.member_id) then
+  error("access denied")
+end
+
+if initiator_todelete.accepted == false and initiator.member_id ~= initiator_todelete.member_id then
+  error("access denied")
+end
+
+local initiators = initiative
+  :get_reference_selector("initiators")
+  :add_where("accepted")
+  :for_update()
+  :exec()
+
+if #initiators > 1 or initiator_todelete.accepted ~= true then
+  initiator_todelete:destroy()
+  slot.put_into("notice", _"Member has been removed from initiators")
+else
+  slot.put_into("error", _"Can't remove last initiator")
+  return false
+end
+
+

app/main/initiative/_action/revoke.lua

+local initiative = Initiative:by_id(param.get_id())
+
+local initiator = Initiator:by_pk(initiative.id, app.session.member.id)
+if not initiator or initiator.accepted ~= true then
+  error("access denied")
+end
+
+-- TODO important m1 selectors returning result _SET_!
+local issue = initiative:get_reference_selector("issue"):for_share():single_object_mode():exec()
+
+if issue.closed then
+  slot.put_into("error", _"This issue is already closed.")
+  return false
+elseif issue.half_frozen then 
+  slot.put_into("error", _"This issue is already frozen.")
+  return false
+end
+
+if initiative.revoked then
+  slot.put_into("error", _"This initiative is already revoked")
+  return false
+end
+
+local suggested_initiative_id = param.get("suggested_initiative_id", atom.integer)
+
+if suggested_initiative_id ~= -1 then
+  local suggested_initiative = Initiative:by_id(suggested_initiative_id)
+  if not suggested_initiative then
+    error("object not found")
+  end
+  if initiative.id == suggested_initiative.id then
+    slot.put_into("error", _"You can't suggest the initiative you are revoking")
+    return false
+  end
+  initiative.suggested_initiative_id = suggested_initiative.id
+end
+
+if not param.get("are_you_sure", atom.boolean) then
+  slot.put_into("error", _"You have to mark 'Are you sure' to revoke!")
+  return false
+end
+
+initiative.revoked = "now"
+initiative:save()
+
+slot.put_into("notice", _"Initiative is revoked now")
+

app/main/initiative/_action/update.lua

 local initiative = Initiative:by_id(param.get_id())
+
+local initiator = Initiator:by_pk(initiative.id, app.session.member.id)
+if not initiator or not initiator.accepted then
+  error("access denied")
+end
+
+-- TODO important m1 selectors returning result _SET_!
+local issue = initiative:get_reference_selector("issue"):for_share():single_object_mode():exec()
+
+if issue.closed then
+  slot.put_into("error", _"This issue is already closed.")
+  return false
+elseif issue.half_frozen then 
+  slot.put_into("error", _"This issue is already frozen.")
+  return false
+end
+
+if initiative.revoked then
+  slot.put_into("error", _"This initiative is revoked")
+  return false
+end
+
 param.update(initiative, "discussion_url")
 initiative:save()
 

app/main/initiative/_list.lua

   name = "issue_" .. tostring(issue.id) ..  "_initiative_list"
 end
 
-ui.order{
+ui_order = ui.order
+
+if param.get("no_sort", atom.boolean) then
+  ui_order = function(args) args.content() end
+  if issue.ranks_available then
+    initiatives_selector:add_order_by("initiative.rank, initiative.admitted DESC, vote_ratio(initiative.positive_votes, initiative.negative_votes) DESC, initiative.id")
+  else
+    initiatives_selector:add_order_by("initiative.supporter_count::float / issue.population::float DESC, initiative.id")
+  end
+end
+
+ui_order{
   name = name,
   selector = initiatives_selector,
   options = order_options,
   content = function()
     ui.paginate{
       selector = initiatives_selector,
+      per_page = param.get("per_page", atom.number),
       content = function()
         local initiatives = initiatives_selector:exec()
         local columns = {}
         }
         columns[#columns+1] = {
           content = function(record)
+            local link_class
+            if record.revoked then
+              link_class = "revoked"
+            end
             ui.link{
+              attr = { class = link_class },
               content = function()
                 local name
                 if record.name_highlighted then

app/main/initiative/add_initiator.lua

+local initiative = Initiative:by_id(param.get("initiative_id"))
+
+slot.put_into("title", _"Invite an initiator to initiative")
+
+slot.select("actions", function()
+  ui.link{
+    content = function()
+        ui.image{ static = "icons/16/cancel.png" }
+        slot.put(_"Cancel")
+    end,
+    module = "initiative",
+    view = "show",
+    id = initiative.id,
+    params = {
+      tab = "initiators"
+    }
+  }
+end)
+
+util.help("initiative.add_initiator", _"Invite an initiator to initiative")
+
+ui.form{
+  attr = { class = "vertical" },
+  module = "initiative",
+  action = "add_initiator",
+  params = {
+    initiative_id = initiative.id,
+  },
+  routing = {
+    ok = {
+      mode = "redirect",
+      module = "initiative",
+      view = "show",
+      id = initiative.id,
+      params = {
+        tab = "initiators",
+      }
+    }
+  },
+  content = function()
+    local records = {
+      {
+        id = "-1",
+        name = _"Choose member"
+      }
+    }
+    local contact_members = app.session.member:get_reference_selector("saved_members"):add_order_by("name"):exec()
+    for i, record in ipairs(contact_members) do
+      records[#records+1] = record
+    end
+    ui.field.select{
+      label = _"Member",
+      name = "member_id",
+      foreign_records = records,
+      foreign_id = "id",
+      foreign_name = "name"
+    }
+    ui.submit{ text = _"Save" }
+  end
+}

app/main/initiative/new.lua

     if issue_id then
       ui.field.text{ label = _"Issue",  value = issue_id }
     else
-      local value
+      tmp = { { id = -1, name = _"Please choose a policy" } }
+      for i, allowed_policy in ipairs(area.allowed_policies) do
+        tmp[#tmp+1] = allowed_policy
+      end
       ui.field.select{
         label = _"Policy",
         name = "policy_id",
-        foreign_records = area.allowed_policies,
+        foreign_records = tmp,
         foreign_id = "id",
         foreign_name = "name",
-        value = area.default_policy.id
+        value = (area.default_policy or {}).id
       }
     end
     ui.field.text{ label = _"Name", name = "name" }

app/main/initiative/remove_initiator.lua

+local initiative = Initiative:by_id(param.get("initiative_id"))
+
+local initiator = Initiator:by_pk(initiative.id, app.session.member.id)
+if not initiator or initiator.accepted ~= true then
+  error("access denied")
+end
+
+slot.put_into("title", _"Remove initiator from initiative")
+
+slot.select("actions", function()
+  ui.link{
+    content = function()
+        ui.image{ static = "icons/16/cancel.png" }
+        slot.put(_"Cancel")
+    end,
+    module = "initiative",
+    view = "show",
+    id = initiative.id,
+    params = {
+      tab = "initiators"
+    }
+  }
+end)
+
+util.help("initiative.remove_initiator", _"Remove initiator from initiative")
+
+ui.form{
+  attr = { class = "vertical" },
+  module = "initiative",
+  action = "remove_initiator",
+  params = {
+    initiative_id = initiative.id,
+  },
+  routing = {
+    ok = {
+      mode = "redirect",
+      module = "initiative",
+      view = "show",
+      id = initiative.id,
+      params = {
+        tab = "initiators",
+      }
+    }
+  },
+  content = function()
+    local records = {
+      {
+        id = "-1",
+        name = _"Choose initiator"
+      }
+    }
+    local members = initiative:get_reference_selector("initiating_members"):add_where("accepted OR accepted ISNULL"):exec()
+    for i, record in ipairs(members) do
+      records[#records+1] = record
+    end
+    ui.field.select{
+      label = _"Member",
+      name = "member_id",
+      foreign_records = records,
+      foreign_id = "id",
+      foreign_name = "name"
+    }
+    ui.submit{ text = _"Save" }
+  end
+}

app/main/initiative/revoke.lua

+local initiative = Initiative:by_id(param.get_id())
+
+slot.put_into("title", _"Revoke initiative")
+
+slot.select("actions", function()
+  ui.link{
+    content = function()
+        ui.image{ static = "icons/16/cancel.png" }
+        slot.put(_"Cancel")
+    end,
+    module = "initiative",
+    view = "show",
+    id = initiative.id,
+    params = {
+      tab = "initiators"
+    }
+  }
+end)
+
+util.help("initiative.revoke")
+
+ui.form{
+  attr = { class = "vertical" },
+  module = "initiative",
+  action = "revoke",
+  id = initiative.id,
+  routing = {
+    ok = {
+      mode = "redirect",
+      module = "initiative",
+      view = "show",
+      id = initiative.id
+    }
+  },
+  content = function()
+    local initiatives = app.session.member
+      :get_reference_selector("supported_initiatives")
+      :join("issue", nil, "issue.id = initiative.issue_id")
+      :add_field("'Issue #' || issue.id || ': ' || initiative.name", "myname")
+      :exec()
+
+    local tmp = { { id = -1, myname = _"Suggest no initiative" }}
+    for i, initiative in ipairs(initiatives) do
+      tmp[#tmp+1] = initiative
+    end
+    ui.field.select{
+      label = _"Suggested initiative",
+      name = "suggested_initiative_id",
+      foreign_records = tmp,
+      foreign_id = "id",
+      foreign_name = "myname",
+      value = param.get("suggested_initiative_id", atom.integer)
+    }
+    slot.put("")
+    ui.field.boolean{
+      label = _"Are you sure?",
+      name = "are_you_sure",
+    }
+
+    ui.submit{ text = _"Revoke initiative" }
+  end
+}

app/main/initiative/show.lua

   params = { issue = initiative.issue }
 }
 
+if initiative.revoked then
+  ui.container{
+    attr = { class = "revoked_info" },
+    content = function()
+      slot.put(_("This initiative has been revoked at #{revoked}", { revoked = format.timestamp(initiative.revoked) }))
+      local suggested_initiative = initiative.suggested_initiative
+      if suggested_initiative then
+        slot.put("<br /><br />")
+        slot.put(_("The initiators suggest to support the following initiative:"))
+        slot.put("<br />")
+        ui.link{
+          content = _("Issue ##{id}", { id = suggested_initiative.issue.id } ) .. ": " .. encode.html(suggested_initiative.name),
+          module = "initiative",
+          view = "show",
+          id = suggested_initiative.id
+        }
+      end
+    end
+  }
+end
+
 local initiator = Initiator:by_pk(initiative.id, app.session.member.id)
 
 --slot.put_into("html_head", '<link rel="alternate" type="application/rss+xml" title="RSS" href="../show/' .. tostring(initiative.id) .. '.rss" />')
 
-execute.view{
-  module = "supporter",
-  view = "_show_box",
-  params = { initiative = initiative }
-}
-
-slot.put_into("sub_title", encode.html(_"Initiative: '#{name}'":gsub("#{name}", initiative.shortened_name) ))
 
 slot.select("actions", function()
   if not initiative.issue.fully_frozen and not initiative.issue.closed then
   end
 end)
 
+slot.put_into("sub_title", encode.html(_"Initiative: '#{name}'":gsub("#{name}", initiative.shortened_name) ))
+
+slot.select("support", function()
+  ui.container{
+    attr = { class = "actions" },
+    content = function()
+      execute.view{
+        module = "supporter",
+        view = "_show_box",
+        params = { initiative = initiative }
+      }
+      if initiator and initiator.accepted and not initiative.issue.fully_frozen and not initiative.issue.closed and not initiative.revoked then
+        ui.link{
+          attr = { class = "action", style = "float: left;" },
+          content = function()
+            ui.image{ static = "icons/16/script_delete.png" }
+            slot.put(_"Revoke initiative")
+          end,
+          module = "initiative",
+          view = "revoke",
+          id = initiative.id
+        }
+      end
+    end
+  }
+end)
 
 util.help("initiative.show")
 
+if initiator and initiator.accepted == nil then
+  ui.container{
+    attr = { class = "initiator_invite_info" },
+    content = function()
+      slot.put(_"You are invited to become initiator of this initiative.")
+      slot.put(" ")
+      ui.link{
+        content = function()
+          ui.image{ static = "icons/16/tick.png" }
+          slot.put(_"Accept invitation")
+        end,
+        module = "initiative",
+        action = "accept_invitation",
+        id = initiative.id,
+        routing = {
+          default = {
+            mode = "redirect",
+            module = request.get_module(),
+            view = request.get_view(),
+            id = param.get_id_cgi(),
+            params = param.get_all_cgi()
+          }
+        }
+      }
+      slot.put(" ")
+      ui.link{
+        content = function()
+          ui.image{ static = "icons/16/cross.png" }
+          slot.put(_"Refuse invitation")
+        end,
+        module = "initiative",
+        action = "reject_initiator_invitation",
+        params = {
+          initiative_id = initiative.id,
+          member_id = app.session.member.id
+        },
+        routing = {
+          default = {
+            mode = "redirect",
+            module = request.get_module(),
+            view = request.get_view(),
+            id = param.get_id_cgi(),
+            params = param.get_all_cgi()
+          }
+        }
+      }
+    end
+  }
+  slot.put("<br />")
+end
 
-ui.container{
-  attr = { class = "vertical" },
-  content = function()
-    ui.container{
-      attr = { class = "ui_field_label" },
-      content = _"Discussion URL"
-    }
-    ui.tag{
-      tag = "span",
-      content = function()
-        if initiative.discussion_url and #initiative.discussion_url > 0 then
-          ui.link{
-            attr = {
-              class = "actions",
-              target = "_blank",
-              title = initiative.discussion_url
-            },
-            content = function()
-              slot.put(encode.html(initiative.discussion_url))
-            end,
-            external = initiative.discussion_url
-          }
+if (initiative.discussion_url and #initiative.discussion_url > 0)
+  or (initiator and initiator.accepted and not initiative.issue.half_frozen and not initiative.issue.closed and not initiative.revoked) then
+  ui.container{
+    attr = { class = "vertical" },
+    content = function()
+      ui.container{
+        attr = { class = "ui_field_label" },
+        content = _"Discussion with initiators"
+      }
+      ui.tag{
+        tag = "span",
+        content = function()
+          if initiative.discussion_url and #initiative.discussion_url > 0 then
+            ui.link{
+              attr = {
+                class = "actions",
+                target = "_blank",
+                title = initiative.discussion_url
+              },
+              content = function()
+                slot.put(encode.html(initiative.discussion_url))
+              end,
+              external = initiative.discussion_url
+            }
+          end
+          slot.put(" ")
+          if initiator and initiator.accepted and not initiative.issue.half_frozen and not initiative.issue.closed and not initiative.revoked then
+            ui.link{
+              attr = { class = "actions" },
+              content = _"(change URL)",
+              module = "initiative",
+              view = "edit",
+              id = initiative.id
+            }
+          end
         end
-        slot.put(" ")
-        if initiator then
-          ui.link{
-            attr = { class = "actions" },
-            content = _"(change URL)",
-            module = "initiative",
-            view = "edit",
-            id = initiative.id
-          }
-        end
-      end
-    }
-  end
-}
-
+      }
+    end
+  }
+end
 
 
 ui.container{
     name = "current_draft",
     label = current_draft_name,
     content = function()
-      if initiator then
+      if initiator and initiator.accepted and not initiative.issue.half_frozen and not initiative.issue.closed and not initiative.revoked then
         ui.link{
           content = function()
             ui.image{ static = "icons/16/script_add.png" }
   }
 end
 
+local suggestion_count = initiative:get_reference_selector("suggestions"):count()
+
 tabs[#tabs+1] = {
   name = "suggestion",
-  label = _"Suggestions",
+  label = _"Suggestions" .. " (" .. tostring(suggestion_count) .. ")",
   content = function()
     execute.view{
       module = "suggestion",
       }
     }
     slot.put("<br />")
-    if not initiative.issue.fully_frozen and not initiative.issue.closed then
+    if not initiative.issue.fully_frozen and not initiative.issue.closed and not initiative.revoked then
       ui.link{
         content = function()
           ui.image{ static = "icons/16/comment_add.png" }
   end
 }
 
+local members_selector =  initiative:get_reference_selector("supporting_members_snapshot")
+          :join("issue", nil, "issue.id = direct_supporter_snapshot.issue_id")
+          :join("direct_interest_snapshot", nil, "direct_interest_snapshot.event = issue.latest_snapshot_event AND direct_interest_snapshot.issue_id = issue.id AND direct_interest_snapshot.member_id = member.id")
+          :add_field("direct_interest_snapshot.weight")
+          :add_where("direct_supporter_snapshot.event = issue.latest_snapshot_event")
+          :add_where("direct_supporter_snapshot.satisfied")
+
+local satisfied_supporter_count = members_selector:count()
+
 tabs[#tabs+1] = {
   name = "satisfied_supporter",
-  label = _"Supporter",
+  label = _"Supporter" .. " (" .. tostring(satisfied_supporter_count) .. ")",
   content = function()
     execute.view{
       module = "member",
       view = "_list",
       params = {
         initiative = initiative,
-        members_selector =  initiative:get_reference_selector("supporting_members_snapshot")
-          :join("issue", nil, "issue.id = direct_supporter_snapshot.issue_id")
-          :join("direct_interest_snapshot", nil, "direct_interest_snapshot.event = issue.latest_snapshot_event AND direct_interest_snapshot.issue_id = issue.id AND direct_interest_snapshot.member_id = member.id")
-          :add_field("direct_interest_snapshot.weight")
-          :add_where("direct_supporter_snapshot.event = issue.latest_snapshot_event")
-          :add_where("direct_supporter_snapshot.satisfied")
+        members_selector = members_selector
       }
     }
   end
 }
 
+local members_selector = initiative:get_reference_selector("supporting_members_snapshot")
+          :join("issue", nil, "issue.id = direct_supporter_snapshot.issue_id")
+          :join("direct_interest_snapshot", nil, "direct_interest_snapshot.event = issue.latest_snapshot_event AND direct_interest_snapshot.issue_id = issue.id AND direct_interest_snapshot.member_id = member.id")
+          :add_field("direct_interest_snapshot.weight")
+          :add_where("direct_supporter_snapshot.event = issue.latest_snapshot_event")
+          :add_where("NOT direct_supporter_snapshot.satisfied")
+
+local potential_supporter_count = members_selector:count()
+
 tabs[#tabs+1] = {
   name = "supporter",
-  label = _"Potential supporter",
+  label = _"Potential supporter" .. " (" .. tostring(potential_supporter_count) .. ")",
   content = function()
     execute.view{
       module = "member",
       view = "_list",
       params = {
         initiative = initiative,
-        members_selector =  initiative:get_reference_selector("supporting_members_snapshot")
-          :join("issue", nil, "issue.id = direct_supporter_snapshot.issue_id")
-          :join("direct_interest_snapshot", nil, "direct_interest_snapshot.event = issue.latest_snapshot_event AND direct_interest_snapshot.issue_id = issue.id AND direct_interest_snapshot.member_id = member.id")
-          :add_field("direct_interest_snapshot.weight")
-          :add_where("direct_supporter_snapshot.event = issue.latest_snapshot_event")
-          :add_where("NOT direct_supporter_snapshot.satisfied")
+        members_selector = members_selector
       }
     }
   end
 }
 
+local initiator_count = initiative:get_reference_selector("initiators"):add_where("accepted"):count()
+
 tabs[#tabs+1] = {
   name = "initiators",
-  label = _"Initiators",
+  label = _"Initiators" .. " (" .. tostring(initiator_count) .. ")",
   content = function()
-    execute.view{ module = "member", view = "_list", params = { members_selector = initiative:get_reference_selector("initiating_members") } }
+     if initiator and initiator.accepted and not initiative.issue.fully_frozen and not initiative.issue.closed and not initiative.revoked then
+      ui.link{
+        attr = { class = "action" },
+        content = function()
+          ui.image{ static = "icons/16/user_add.png" }
+          slot.put(_"Invite initiator")
+        end,
+        module = "initiative",
+        view = "add_initiator",
+        params = { initiative_id = initiative.id }
+      }
+      if initiator_count > 1 then
+        ui.link{
+          content = function()
+            ui.image{ static = "icons/16/user_delete.png" }
+            slot.put(_"Remove initiator")
+          end,
+          module = "initiative",
+          view = "remove_initiator",
+          params = { initiative_id = initiative.id }
+        }
+      end
+    end
+    if initiator and initiator.accepted == false then
+        ui.link{
+          content = function()
+            ui.image{ static = "icons/16/user_delete.png" }
+            slot.put(_"Cancel refuse of invitation")
+          end,
+          module = "initiative",
+          action = "remove_initiator",
+          params = {
+            initiative_id = initiative.id,
+            member_id = app.session.member.id
+          },
+          routing = {
+            ok = {
+              mode = "redirect",
+              module = "initiative",
+              view = "show",
+              id = initiative.id
+            }
+          }
+        }
+    end
+    local members_selector = initiative:get_reference_selector("initiating_members")
+      :add_field("initiator.accepted", "accepted")
+    if not (initiator and initiator.accepted) then
+      members_selector:add_where("accepted")
+    end
+    execute.view{
+      module = "member",
+      view = "_list",
+      params = {
+        members_selector = members_selector,
+        initiator = initiator
+      }
+    }
   end
 }
 
+local drafts_count = initiative:get_reference_selector("drafts"):count()
+
 tabs[#tabs+1] = {
   name = "drafts",
-  label = _"Old drafts",
+  label = _"Draft history" .. " (" .. tostring(drafts_count) .. ")",
   content = function()
     execute.view{ module = "draft", view = "_list", params = { drafts = initiative.drafts } }
   end
           label = _"Created at",
           value = format.timestamp(initiative.created)
         }
-        ui.field.date{ label = _"Revoked at", name = "revoked" }
+--         ui.field.date{ label = _"Revoked at", name = "revoked" }
         ui.field.boolean{ label = _"Admitted", name = "admitted" }
       end
     }

app/main/issue/_list.lua

   ui_filter = function(args) args.content() end
 end
 
+if param.get("no_filter", atom.boolean) then
+  ui_filter = function(args) args.content() end
+end
+
 local filter_voting = false
 ui_filter{
   selector = issues_selector,
     if not filter_voting then
       ui_filter = function(args) args.content() end
     end
+    if param.get("no_filter", atom.boolean) then
+      ui_filter = function(args) args.content() end
+    end
     ui_filter{
       selector = issues_selector,
       name = "filter_voting",
             },
           },
           content = function()
-
-            ui.order{
+            local ui_order = ui.order
+            if param.get("no_sort", atom.boolean) then
+              ui_order = function(args) args.content() end
+            end
+            ui_order{
               name = "issue_list",
               selector = issues_selector,
               options = {
                 }
               },
               content = function()
-                ui.paginate{
+                local ui_paginate = ui.paginate
+                if param.get("per_page") == "all" then
+                  ui_paginate = function(args) args.content() end
+                end
+                ui_paginate{
+                  per_page = tonumber(param.get("per_page")),
                   selector = issues_selector,
                   content = function()
                     local highlight_string = param.get("highlight_string", "string")
                               slot.put("<br />")
                             end
                             ui.link{
-                              text = _"Issue ##{id}":gsub("#{id}", tostring(record.id)),
+                              text = _("Issue ##{id}", { id = tostring(record.id) }),
                               module = "issue",
                               view = "show",
                               id = record.id
                             end
                             slot.put("<br />")
                             slot.put("<br />")
+                            if record.old_state then
+                              ui.field.text{ value = format.time(record.sort) }
+                              ui.field.text{ value = Issue:get_state_name_for_state(record.old_state) .. " > " .. Issue:get_state_name_for_state(record.new_state) }
+                            else
+                            end
                           end
                         },
                         {
                                 issue = record,
                                 initiatives_selector = initiatives_selector,
                                 highlight_string = highlight_string,
-                                limit = 3
+                                limit = 3,
+                                per_page = param.get("initiatives_per_page", atom.number),
+                                no_sort = param.get("initiatives_no_sort", atom.boolean)
                               }
                             }
                           end

app/main/issue/_show_head.lua

   end
 --]]
 
+  if config.issue_discussion_url_func then
+    local url = config.issue_discussion_url_func(issue)
+    ui.link{
+      attr = { target = "_blank" },
+      external = url,
+      content = function()
+        ui.image{ static = "icons/16/comments.png" }
+        slot.put(_"Discussion on issue")
+      end,
+    }
+  end
 end)
 
 

app/main/member/_action/update_stylesheet_url.lua

+
+local setting_key = "liquidfeedback_frontend_developer_features"
+local setting = Setting:by_pk(app.session.member.id, setting_key)
+
+if not setting then
+  error("access denied")
+end
+
+local stylesheet_url = util.trim(param.get("stylesheet_url"))
+local setting_key = "liquidfeedback_frontend_stylesheet_url"
+local setting = Setting:by_pk(app.session.member.id, setting_key)
+
+if stylesheet_url and #stylesheet_url > 0 then
+  if not setting then
+    setting = Setting:new()
+    setting.member_id = app.session.member.id
+    setting.key = setting_key
+  end
+  setting.value = stylesheet_url
+  setting:save()
+elseif setting then
+  setting:destroy()
+end
+
+slot.put_into("notice", _"Stylesheet URL has been updated")

app/main/member/_list.lua

 local initiative = param.get("initiative", "table")
 local issue = param.get("issue", "table")
 local trustee = param.get("trustee", "table")
+local initiator = param.get("initiator", "table")
 
 local options = {
   {
+    name = "newest",
+    label = _"Newest",
+    order_by = "created DESC, id DESC"
+  },
+  {
+    name = "oldest",
+    label = _"Oldest",
+    order_by = "created, id"
+  },
+  {
     name = "name",
     label = _"A-Z",
     order_by = "name"
               execute.view{
                 module = "member",
                 view = "_show_thumb",
-                params = { member = member, initiative = initiative, issue = issue, trustee = trustee }
+                params = {
+                  member = member,
+                  initiative = initiative,
+                  issue = issue,
+                  trustee = trustee,
+                  initiator = initiator
+                }
               }
             end
 ---]]

app/main/member/_show.lua

       execute.view{
         module = "initiative",
         view = "_list",
-        params = { initiatives_selector = member:get_reference_selector("initiated_initiatives") }
+        params = { initiatives_selector = member:get_reference_selector("initiated_initiatives"):add_where("initiator.accepted = true") }
       }
     end
   },

app/main/member/_show_thumb.lua

+local initiator = param.get("initiator", "table")
 local member = param.get("member", "table")
 
 local issue = param.get("issue", "table")
   name = encode.html(member.name)
 end
 
+local container_class = "member_thumb"
+if initiator and member.accepted ~= true then
+  container_class = container_class .. " not_accepted"
+end
+
 ui.container{
-  attr = { class = "member_thumb" },
+  attr = { class = container_class },
   content = function()
     ui.container{
       attr = { class = "flags" },
         else
           slot.put("&nbsp;")
         end
+        if initiator and initiator.accepted then
+          if member.accepted == nil then
+            slot.put(_"Invited")
+          elseif member.accepted == false then
+            slot.put(_"Rejected")
+          end
+        end
         if member.grade then
           ui.container{
             content = function()

app/main/member/_show_thumb.lua.orig

-local member = param.get("member", "table")
-
-local issue = param.get("issue", "table")
-local initiative = param.get("initiative", "table")
-local trustee = param.get("trustee", "table")
-
-local name
-if member.name_highlighted then
-  name = encode.highlight(member.name_highlighted)
-else
-  name = encode.html(member.name)
-end
-
-ui.container{
-  attr = { class = "member_thumb" },
-  content = function()
-    ui.container{
-      attr = { class = "flags" },
-      content = function()
-        if (issue or initiative) and member.weight > 1 then
-          local module
-          if issue then
-            module = "interest"
-          elseif initiative then
-            module = "supporter"
-          end
-          ui.link{
-            attr = { title = _"Number of incoming delegations, follow link to see more details" },
-            content = _("+ #{weight}", { weight = member.weight - 1 }),
-            module = module,
-            view = "show_incoming",
-            params = { 
-              member_id = member.id, 
-              initiative_id = initiative and initiative.id or nil,
-              issue_id = issue and issue.id or nil
-            }
-          }
-        end
-        -- TODO performance
-        local contact = Contact:by_pk(app.session.member.id, member.id)
-        if contact then
-          ui.image{
-            attr = { 
-              alt   = _"You have saved this member as contact",
-              title = _"You have saved this member as contact"
-            },
-            static = "icons/16/bullet_disk.png"
-          }
-        end
-      end
-    }
-
-    ui.link{
-      attr = { title = _"Show member" },
-      module = "member",
-      view = "show",
-      id = member.id,
-      content = function()
-        execute.view{
-          module = "member_image",
-          view = "_show",
-          params = {
-            member = member,
-            image_type = "avatar",
-            show_dummy = true
-          }
-        }
-      end
-    }
-
-    ui.link{
-      attr = { title = _"Show member" },
-      module = "member",
-      view = "show",
-      id = member.id,
-      content = function()
-        ui.container{
-          attr = { class = "member_name" },
-          content = function()
-            slot.put(name)
-          end
-        }
-      end
-    }
-  end
-}

app/main/member/developer_settings.lua

+slot.put_into("title", _"Developer features")
+
+slot.select("actions", function()
+  ui.link{
+    content = function()
+        ui.image{ static = "icons/16/cancel.png" }
+        slot.put(_"Cancel")
+    end,
+    module = "member",
+    view = "settings"
+  }
+end)
+
+ui.form{
+  attr = { class = "vertical" },
+  module = "member",
+  action = "update_stylesheet_url",
+  routing = {
+    ok = {
+      mode = "redirect",
+      module = "index",
+      view = "index"
+    }
+  },
+  content = function()
+    local setting_key = "liquidfeedback_frontend_stylesheet_url"
+    local setting = Setting:by_pk(app.session.member.id, setting_key)
+    local value = setting and setting.value
+    ui.field.text{ 
+      label = _"Stylesheet URL",
+      name = "stylesheet_url",
+      value = value
+    }
+    ui.submit{ value = _"Set URL" }
+  end
+}

app/main/member/list.lua

 execute.view{
   module = "member",
   view = "_list",
-  params = { members_selector = Member:new_selector():add_order_by("name") }
+  params = { members_selector = Member:new_selector() }
 }

app/main/member/settings.lua

     module = "index",
     view = "index"
   }
+
+  local setting_key = "liquidfeedback_frontend_developer_features"
+  local setting = Setting:by_pk(app.session.member.id, setting_key)
+
+  if setting then
+    ui.link{
+      content = function()
+          ui.image{ static = "icons/16/wrench.png" }
+          slot.put(_"Developer features")
+      end,
+      module = "member",
+      view = "developer_settings"
+    }
+  end
 end)
 
 ui.heading{ content = _"Change your name" }
     ui.field.password{ label = _"Repeat new password", name = "new_password2" }
     ui.submit{ value = _"Change password" }
   end
-}
+}

app/main/supporter/_show_box.lua

   local initiative = param.get("initiative", "table")
   local supporter = Supporter:by_pk(initiative.id, app.session.member.id)
 
-  ui.container{
-    attr = { class = "actions" },
-    content = function()
-      if not initiative.issue.fully_frozen and not initiative.issue.closed then
-        if supporter then
-          if not supporter:has_critical_opinion() then
-            ui.container{
-              attr = {
-                class = "head head_supporter",
-                style = "cursor: pointer;",
-                onclick = "document.getElementById('support_content').style.display = 'block';"
-              },
+  if not initiative.issue.fully_frozen and not initiative.issue.closed then
+    if supporter then
+      if not supporter:has_critical_opinion() then
+        ui.container{
+          attr = {
+            class = "head head_supporter",
+            style = "cursor: pointer;",
+            onclick = "document.getElementById('support_content').style.display = 'block';"
+          },
+          content = function()
+            ui.image{
+              static = "icons/16/thumb_up_green.png"
+            }
+            slot.put(_"Your are supporter")
+            ui.image{
+              static = "icons/16/dropdown.png"
+            }
+          end
+        }
+      else
+        ui.container{
+          attr = {
+            class = "head head_potential_supporter",
+            style = "cursor: pointer;",
+            onclick = "document.getElementById('support_content').style.display = 'block';"
+          },
+          content = function()
+            ui.image{
+              static = "icons/16/thumb_up.png"
+            }
+            slot.put(_"Your are potential supporter")
+            ui.image{
+              static = "icons/16/dropdown.png"
+            }
+          end
+        }
+      end
+      ui.container{
+        attr = { class = "content", id = "support_content" },
+        content = function()
+          ui.container{
+            attr = {
+              class = "close",
+              style = "cursor: pointer;",
+              onclick = "document.getElementById('support_content').style.display = 'none';"
+            },
+            content = function()
+              ui.image{ static = "icons/16/cross.png" }
+            end
+          }
+          if supporter then
+            ui.link{
               content = function()
-                ui.image{
-                  static = "icons/16/thumb_up_green.png"
+                ui.image{ static = "icons/16/thumb_down_red.png" }
+                slot.put(_"Remove my support from this initiative")
+              end,
+              module = "initiative",
+              action = "remove_support",
+              id = initiative.id,
+              routing = {
+                default = {
+                  mode = "redirect",
+                  module = request.get_module(),
+                  view = request.get_view(),
+                  id = param.get_id_cgi(),
+                  params = param.get_all_cgi()
                 }
-                slot.put(_"Your are supporter")
-                ui.image{
-                  static = "icons/16/dropdown.png"
-                }
-              end
+              }
             }
           else
-            ui.container{
-              attr = {
-                class = "head head_potential_supporter",
-                style = "cursor: pointer;",
-                onclick = "document.getElementById('support_content').style.display = 'block';"
-              },
-              content = function()
-                ui.image{
-                  static = "icons/16/thumb_up.png"
-                }
-                slot.put(_"Your are potential supporter")
-                ui.image{
-                  static = "icons/16/dropdown.png"
-                }
-              end
-            }
           end
-          ui.container{
-            attr = { class = "content", id = "support_content" },
-            content = function()
-              ui.container{
-                attr = {
-                  class = "close",
-                  style = "cursor: pointer;",
-                  onclick = "document.getElementById('support_content').style.display = 'none';"
-                },
-                content = function()
-                  ui.image{ static = "icons/16/cross.png" }
-                end
-              }
-              if supporter then
-                ui.link{
-                  content = function()
-                    ui.image{ static = "icons/16/thumb_down_red.png" }
-                    slot.put(_"Remove my support from this initiative")
-                  end,
-                  module = "initiative",
-                  action = "remove_support",
-                  id = initiative.id,
-                  routing = {
-                    default = {
-                      mode = "redirect",
-                      module = request.get_module(),
-                      view = request.get_view(),
-                      id = param.get_id_cgi(),
-                      params = param.get_all_cgi()
-                    }
-                  }
-                }
-              else
-              end
-            end
-          }
-        else
-          ui.link{
-            content = function()
-              ui.image{ static = "icons/16/thumb_up_green.png" }
-              slot.put(_"Support this initiative")
-            end,
-            module = "initiative",
-            action = "add_support",
-            id = initiative.id,
-            routing = {
-              default = {
-                mode = "redirect",
-                module = request.get_module(),
-                view = request.get_view(),
-                id = param.get_id_cgi(),
-                params = param.get_all_cgi()
-              }
+        end
+      }
+    else
+      if not initiative.revoked then
+        ui.link{
+          content = function()
+            ui.image{ static = "icons/16/thumb_up_green.png" }
+            slot.put(_"Support this initiative")
+          end,
+          module = "initiative",
+          action = "add_support",
+          id = initiative.id,
+          routing = {
+            default = {
+              mode = "redirect",
+              module = request.get_module(),
+              view = request.get_view(),
+              id = param.get_id_cgi(),
+              params = param.get_all_cgi()
             }
           }
-        end
+        }
       end
     end
-  }
+  end
+
 end)

app/main/timeline/index.lua

+
+local function format_dow(dow)
+  local dows = {
+    _"Monday",
+    _"Tuesday",
+    _"Wednesday",
+    _"Thursday",
+    _"Friday",
+    _"Saturday",
+    _"Sunday"
+  }
+  return dows[dow+1]
+end
+
+slot.put_into("title", _"Global timeline")
+
+
+ui.form{
+  attr = { class = "vertical" },
+  module = "timeline",
+  view = "index",
+  method = "get",
+  content = function()
+    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; ")
+    local today = tmp[1].date
+    for i, record in ipairs(tmp) do
+      local content
+      if i == 1 then
+        content = _"Today"
+      elseif i == 2 then
+        content = _"Yesterday"
+      else
+        content = format_dow(record.dow)
+      end
+      ui.link{
+        content = content,
+        attr = { onclick = "el = document.getElementById('timeline_search_date'); el.value = '" .. tostring(record.date) .. "'; el.form.submit(); return(false);" },
+        module = "timeline",
+        view = "index",
+        params = { date = record.date }
+      }
+      slot.put(" ")
+    end
+    ui.field.hidden{
+      attr = { id = "timeline_search_date" },
+      name = "date",
+      value = param.get("date") or today
+    }
+    ui.field.select{
+      attr = { onchange = "this.form.submit();" },
+      name = "per_page",
+      label = _"Issues per page",
+      foreign_records = {
+        { id = "10",  name = "10"   },
+        { id = "25",  name = "25"   },
+        { id = "50",  name = "50"   },
+        { id = "100", name = "100"  },
+        { id = "250", name = "250"  },
+        { id = "all", name = _"All" },
+      },
+      foreign_id = "id",
+      foreign_name = "name",
+      value = param.get("per_page")
+    }
+    local initiatives_per_page = param.get("initiatives_per_page", atom.integer) or 3
+
+    ui.field.select{
+      attr = { onchange = "this.form.submit();" },
+      name = "initiatives_per_page",
+      label = _"Initiatives per page",
+      foreign_records = {
+        { id = 1,   name = "1"  },
+        { id = 3,   name = "3"  },
+        { id = 5,   name = "5"  },
+        { id = 10,  name = "10" },
+        { id = 25,  name = "25" },
+        { id = 50,  name = "50" },
+      },
+      foreign_id = "id",
+      foreign_name = "name",
+      value = initiatives_per_page
+    }
+  end
+}
+
+local date = param.get("date")
+if not date then
+  date = "today"
+end
+local issues_selector = db:new_selector()
+issues_selector._class = Issue
+
+issues_selector
+  :add_field("*")
+  :add_where{ "sort::date = ?", date }
+  :add_from{ "($) as issue", {
+    Issue:new_selector()
+      :add_field("''", "old_state")
+      :add_field("'new'", "new_state")
+      :add_field("created", "sort")
+    :union(Issue:new_selector()
+      :add_field("'new'", "old_state")
+      :add_field("'accepted'", "new_state")
+      :add_field("accepted", "sort")
+      :add_where("accepted NOTNULL")
+    ):union(Issue:new_selector()
+      :add_field("'accepted'", "old_state")
+      :add_field("'frozen'", "new_state")
+      :add_field("half_frozen", "sort")
+      :add_where("half_frozen NOTNULL")
+    ):union(Issue:new_selector()
+      :add_field("'frozen'", "old_state")
+      :add_field("'voting'", "new_state")
+      :add_field("fully_frozen", "sort")
+      :add_where("fully_frozen NOTNULL")
+    ):union(Issue:new_selector()
+      :add_field("'new'", "old_state")
+      :add_field("'cancelled'", "new_state")
+      :add_field("closed", "sort")
+      :add_where("closed NOTNULL AND accepted ISNULL")
+    ):union(Issue:new_selector()
+      :add_field("'accepted'", "old_state")
+      :add_field("'cancelled'", "new_state")
+      :add_field("closed", "sort")
+      :add_where("closed NOTNULL AND half_frozen ISNULL AND accepted NOTNULL")
+    ):union(Issue:new_selector()
+      :add_field("'frozen'", "old_state")
+      :add_field("'cancelled'", "new_state")
+      :add_field("closed", "sort")
+      :add_where("closed NOTNULL AND fully_frozen ISNULL AND half_frozen NOTNULL")
+    ):union(Issue:new_selector()
+      :add_field("'voting'", "old_state")
+      :add_field("'finished'", "new_state")
+      :add_field("closed", "sort")
+      :add_where("closed NOTNULL AND fully_frozen NOTNULL AND half_frozen ISNULL")
+    )
+  }
+}
+
+execute.view{
+  module = "issue",
+  view = "_list",
+  params = {
+    issues_selector = issues_selector,
+    initiatives_per_page = param.get("initiatives_per_page", atom.number),
+    initiatives_no_sort = true,
+    no_filter = true,
+    no_sort = true,
+    per_page = param.get("per_page"),
+  }
+}

config/default.lua

 config.app_name = "LiquidFeedback"
-config.app_version = "beta5"
+config.app_version = "beta6"
 
 config.app_title = config.app_name .. " (" .. request.get_config_name() .. " environment)"
 

config/development.lua

 
 config.mail_from = "LiquidFeedback"
 config.mail_reply_to = "liquid-support@localhost"
+
+config.issue_discussion_url_func = function(issue) return "http://example.com/issue_" .. tostring(issue.id) end

locale/help/initiative.add_initiator.de.txt

+=Initiator einladen=
+Hier kannst du eine Person aus deiner Kontaktliste zur gleichberechtigten Mitarbeit am Entwurf einladen. Der eingeladene muss die Einladung erst akzeptieren, bevor er als weiterer Initiator gilt (und angezeigt wird). **Vorsicht:** Alle Initiatoren haben die gleichen Rechte und können andere Initiatoren entfernen.

locale/help/initiative.remove_initiator.de.txt

+=Initiator entfernen=
+Du kannst dich oder einen anderen Initiator entfernen, sofern nach dem Entfernen noch mindestens ein Initiator existiert. Um die Initiative insgesamt aufzugeben, wähle bitte ,,Initiative zurückziehen''.