Commits

Anonymous committed 768faea

Version alpha4

Members interested in an issue or supporting an initiative have a weight information attached. Browsing the members causing that weight is possible.

Initiatives may provide a link to an external discussion platform

Direct link on every initiative page to create an alternative initiative

Bugfix: No error when clicking "neutral", when "neutral" is currently selected

  • Participants
  • Parent commits 5c60180
  • Tags alpha4

Comments (0)

Files changed (32)

File app/main/_filter/20_session.lua

 
 request.set_csrf_secret(app.session.additional_secret)
 
-locale.set{lang = app.session.lang or "en"}
+locale.set{lang = app.session.lang or "de"}
 
 execute.inner()

File app/main/admin/_action/member_update.lua

   member = Member:new()
 end
 
-param.update(member, "login", "admin", "name", "ident_number", "active")
+param.update(member, "login", "admin", "name", "active")
 
 local password = param.get("password")
 if password == "********" or #password == 0 then

File app/main/admin/member_edit.lua

     ui.field.password{ label = _"Password",     name = "password", value = (member and member.password) and "********" or "" }
     ui.field.boolean{  label = _"Admin?",       name = "admin" }
     ui.field.boolean{  label = _"Active?",      name = "active" }
-    ui.field.text{     label = _"Ident number", name = "ident_number" }
     ui.submit{         text  = _"Save" }
   end
 }

File app/main/contact/_action/add_member.lua

 contact:save()
 
 if public then
-  slot.put_into("notice", _"Member has been saved as public contact")
+--  slot.put_into("notice", _"Member has been saved as public contact")
 else
-  slot.put_into("notice", _"Member has been saved as private contact")
+--  slot.put_into("notice", _"Member has been saved as private contact")
 end

File app/main/contact/_action/remove_member.lua

 local contact = Contact:by_pk(member.id, other_member.id)
 contact:destroy()
 
-slot.put_into("notice", _"Member has been removed from your contacts")
+--slot.put_into("notice", _"Member has been removed from your contacts")

File app/main/index/_action/login.lua

   end)
   trace.debug('User authenticated')
 else
-  slot.select("notice", function()
+  slot.select("error", function()
     ui.tag{ content = _'Invalid username or password!' }
   end)
   trace.debug('User NOT authenticated')

File app/main/index/login.lua

 
 ui.tag{
   tag = 'p',
-  content = 'You need to be logged in, to use this system.'
+  content = _'You need to be logged in, to use this system.'
 }
 
 ui.form{

File app/main/initiative/_action/create.lua

 end
 
 initiative.issue_id = issue.id
-param.update(initiative, "name")
+
+param.update(initiative, "name", "discussion_url")
 initiative:save()
 
 local draft = Draft:new()

File app/main/initiative/_action/update.lua

+local initiative = Initiative:by_id(param.get_id())
+param.update(initiative, "discussion_url")
+initiative:save()
+
+slot.put_into("notice", _"Initiative successfully updated")
+

File app/main/initiative/_list.lua

         columns[#columns+1] = {
           content = function(record)
             if record.issue.accepted and record.issue.closed and record.issue.ranks_available then 
-              ui.field.rank{ value = record.rank }
+              ui.field.rank{ attr = { class = "rank" }, value = record.rank }
+            end
+          end
+        }
+        columns[#columns+1] = {
+          content = function(record)
+            if record.issue.accepted and record.issue.closed and record.issue.ranks_available then 
               if record.negative_votes and record.positive_votes then
                 local max_value = record.issue.voter_count
                 ui.bargraph{

File app/main/initiative/edit.lua

+local initiative = Initiative:by_id(param.get_id())
+
+slot.put_into("title", _"Edit initiative")
+
+ui.form{
+  record = initiative,
+  module = "initiative",
+  action = "update",
+  id = initiative.id,
+  attr = { class = "vertical" },
+  routing = {
+    default = {
+      mode = "redirect",
+      module = "initiative",
+      view = "show",
+      id = initiative.id
+    }
+  },
+  content = function()
+    ui.field.text{ label = _"Discussion URL",  name = "discussion_url" }
+    ui.submit{ text = _"Save" }
+  end
+}

File app/main/initiative/new.lua

       }
     end
     ui.field.text{ label = _"Name",  name = "name" }
+    ui.field.text{ label = _"Discussion URL",  name = "discussion_url" }
     ui.field.text{ label = _"Draft", name = "draft", multiline = true, attr = { style = "height: 50ex;" } }
     ui.submit{ text = _"Save" }
   end

File app/main/initiative/show.lua

 local initiative = Initiative:new_selector():add_where{ "id = ?", param.get_id()}:single_object_mode():exec()
 
-slot.put_into("html_head", '<link rel="alternate" type="application/rss+xml" title="RSS" href="../show/' .. tostring(initiative.id) .. '.rss" />')
+--slot.put_into("html_head", '<link rel="alternate" type="application/rss+xml" title="RSS" href="../show/' .. tostring(initiative.id) .. '.rss" />')
 
 execute.view{
   module = "supporter",
 
 slot.select("actions", function()
 
-  if Initiator:by_pk(initiative.id, app.session.member.id) then
+  local initiator = Initiator:by_pk(initiative.id, app.session.member.id)
+
+  if initiator then
     ui.link{
       content = function()
         ui.image{ static = "icons/16/script_add.png" }
     }
   end
 
-  ui.twitter("http://example.com/i" .. tostring(initiative.id) .. " " .. initiative.name)
+  if not initiative.issue.fully_frozen and not initiative.issue.closed then
+    ui.link{
+      attr = { class = "action" },
+      content = function()
+        ui.image{ static = "icons/16/script_add.png" }
+        slot.put(_"Create alternative initiative" )
+      end,
+      module = "initiative",
+      view = "new",
+      params = { issue_id = initiative.issue.id }
+    }
+  end
+--  ui.twitter("http://example.com/i" .. tostring(initiative.id) .. " " .. initiative.name)
 
+  if initiative.discussion_url and #initiative.discussion_url > 0 then
+    ui.link{
+      attr = { 
+        target = _"blank",
+        title = initiative.discussion_url
+      },
+      content = function()
+        ui.image{ static = "icons/16/comments.png" }
+        slot.put(_"External discussion")
+      end,
+      external = initiative.discussion_url
+    }
+  end
+  if initiator then
+    ui.link{
+      content = _"(change)",
+      module = "initiative",
+      view = "edit",
+      id = initiative.id
+    }
+  end
 end)
 
 
     name = "supporter",
     label = _"Supporter",
     content = function()
-      execute.view{ module = "member", view = "_list", params = { members_selector = initiative:get_reference_selector("supporting_members") } }
+      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_population_snapshot", nil, "direct_population_snapshot.event = issue.latest_snapshot_event AND direct_population_snapshot.issue_id = issue.id AND direct_population_snapshot.member_id = member.id")
+            :add_field("direct_population_snapshot.weight")
+            :add_where("direct_supporter_snapshot.event = issue.latest_snapshot_event")
+        }
+      }
     end
   },
   {

File app/main/interest/_show_box.lua

   ui.container{
     attr = { class = "content", id = "interest_content" },
     content = function()
+      ui.container{
+        attr = {
+          class = "close",
+          style = "cursor: pointer;",
+          onclick = "document.getElementById('interest_content').style.display = 'none';"
+        },
+        content = _"X"
+      }
       if interest then
         ui.link{
           content = _"Remove my interest",
           params = { issue_id = issue.id, delete = true },
           routing = { default = { mode = "redirect", module = "issue", view = "show", id = issue.id } }
         }
+        slot.put("<br />")
+        slot.put("<br />")
         if interest.autoreject then
           ui.field.text{ value = _"Autoreject is on." }
           ui.link{
           routing = { default = { mode = "redirect", module = "issue", view = "show", id = issue.id } }
         }
       end
-        ui.container{
-          attr = {
-            class = "head",
-            style = "cursor: pointer;",
-            onclick = "document.getElementById('interest_content').style.display = 'none';"
-          },
-          content = _"Click here to close."
-        }
     end
   }
 end)

File app/main/interest/show_incoming.lua

+local issue = Issue:by_id(param.get("issue_id", atom.integer))
+local member = Member:by_id(param.get("member_id", atom.integer))
+
+local members_selector = Member:new_selector()
+  :join("delegating_interest_snapshot", nil, "delegating_interest_snapshot.member_id = member.id")
+  :add_where{ "delegating_interest_snapshot.issue_id = ?", issue.id }
+  :add_where{ "delegating_interest_snapshot.event = ?", issue.latest_snapshot_event }
+  :add_where{ "delegating_interest_snapshot.delegate_member_ids[1] = ?", member.id }
+  :add_field{ "delegating_interest_snapshot.weight" }
+
+execute.view{
+  module = "member",
+  view = "_list",
+  params = { 
+    members_selector = members_selector,
+    issue = issue,
+    trustee = member
+  }
+}

File app/main/issue/_show_box.lua

   local time_left = issue.state_time_left
   if time_left then
     ui.field.text{ 
-      label = "Time left",
+      label = _"Time left",
       value = time_left
     }
   end
   local next_state_names = issue.next_states_names
   if next_state_names then
     ui.field.text{ 
-      label = _"Next states", 
+      label = _"Next state",
       value = next_state_names
     }
   end

File app/main/issue/show.lua

         }
       }
       slot.put("<br />")
-      if not issue.frozen and not issue.closed then
+      if not issue.fully_frozen and not issue.closed then
         ui.link{
           attr = { class = "action" },
           content = function()
   },
 --]]
   {
+    name = "interested_members",
+    label = _"Interested members",
+    content = function()
+      execute.view{
+        module = "member",
+        view = "_list",
+        params = {
+          issue = issue,
+          members_selector =  issue:get_reference_selector("interested_members_snapshot")
+            :join("issue", nil, "issue.id = direct_interest_snapshot.issue_id")
+            :add_field("direct_interest_snapshot.weight")
+            :add_where("direct_interest_snapshot.event = issue.latest_snapshot_event")
+        }
+      }
+    end
+  },
+  {
     name = "delegations",
     label = _"Delegations",
     content = function()
         content = function()
           ui.field.text{ label = _"State", name = "state" }
           ui.field.timestamp{ label = _"Created at",            name = "created" }
-          ui.field.text{      label = _"admission_time",        value = policy.admission_time }
-          ui.field.integer{   label = _"issue_quorum_num",      value = policy.issue_quorum_num }
-          ui.field.integer{   label = _"issue_quorum_den",      value = policy.issue_quorum_den }
-          ui.field.timestamp{ label = _"Accepted",              name = "accepted" }
-          ui.field.text{      label = _"discussion_time",       value = policy.discussion_time }
+          ui.field.text{      label = _"Admission time",        value = policy.admission_time }
+          ui.field.text{
+            label = _"Issue quorum",
+            value = format.percentage(policy.issue_quorum_num / policy.issue_quorum_den)
+          }
+          ui.field.timestamp{ label = _"Accepted at",              name = "accepted" }
+          ui.field.text{      label = _"Discussion time",       value = policy.discussion_time }
           ui.field.vote_now{   label = _"Vote now", name = "vote_now" }
           ui.field.vote_later{ label = _"Vote later", name = "vote_later" }
-          ui.field.timestamp{ label = _"Half frozen",           name = "half_frozen" }
-          ui.field.text{      label = _"verification_time",     value = policy.verification_time }
-          ui.field.integer{ label   = _"initiative_quorum_num", value = policy.initiative_quorum_num }
-          ui.field.integer{ label   = _"initiative_quorum_den", value = policy.initiative_quorum_den }
-          ui.field.timestamp{ label = _"Fully frozen",          name = "fully_frozen" }
-          ui.field.text{      label = _"voting_time",           value = policy.voting_time }
+          ui.field.timestamp{ label = _"Half frozen at",           name = "half_frozen" }
+          ui.field.text{      label = _"Verification time",     value = policy.verification_time }
+          ui.field.text{
+            label   = _"Initiative quorum",
+            value = format.percentage(policy.initiative_quorum_num / policy.initiative_quorum_den)
+          }
+          ui.field.timestamp{ label = _"Fully frozen at",          name = "fully_frozen" }
+          ui.field.text{      label = _"Voting time",           value = policy.voting_time }
           ui.field.timestamp{ label = _"Closed",                name = "closed" }
         end
       }

File app/main/member/_action/update_avatar.lua

 
 local data = param.get("avatar")
 
-local data_scaled, err, status = os.pfilter(data, "convert", "-", "-thumbnail", "48x48", "-")
+local data_scaled, err, status = os.pfilter(data, "convert", "-", "-thumbnail", "48x48", "jpeg:-")
 
 if status ~= 0 or data_scaled == nil then
  error("error while converting image")

File app/main/member/_list.lua

 local members_selector = param.get("members_selector", "table")
+local initiative = param.get("initiative", "table")
+local issue = param.get("issue", "table")
+local trustee = param.get("trustee", "table")
+
+local options = {
+  {
+    name = "name",
+    label = _"A-Z",
+    order_by = "name"
+  },
+  {
+    name = "name_desc",
+    label = _"Z-A",
+    order_by = "name DESC"
+  },
+}
+
+if initiative then
+  options[#options+1] = {
+    name = "delegations",
+    label = _"Delegations",
+    order_by = "weight DESC"
+  }
+end
 
 ui.order{
   name = "member_list",
   selector = members_selector,
-  options = {
-    {
-      name = "name",
-      label = _"A-Z",
-      order_by = "name"
-    },
-    {
-      name = "name_desc",
-      label = _"Z-A",
-      order_by = "name DESC"
-    },
-  },
+  options = options,
   content = function()
     ui.paginate{
       selector = members_selector,
         ui.container{
           attr = { class = "member_list" },
           content = function()
-            for i, member in ipairs(members_selector:exec()) do
+            local members = members_selector:exec()
+            local columns = { 
+              {
+                label = _"Name",
+                content = function(member)
+                  ui.link{
+                    module = "member",
+                    view = "show",
+                    id = member.id,
+                    content = function()
+                      ui.image{
+                        attr = { width = 48, height = 48 },
+                        module    = "member",
+                        view      = "avatar",
+                        id        = member.id,
+                        extension = "jpg"
+                      }
+                    end
+                  }
+                end
+              },
+              {
+                label = _"Name",
+                content = function(member)
+                  ui.link{
+                    module = "member",
+                    view = "show",
+                    id = member.id,
+                    content = member.name
+                  }
+                  if member.admin then
+                    ui.image{
+                      attr = { 
+                        alt   = _"Administrator",
+                        title = _"Administrator"
+                      },
+                      static = "icons/16/cog.png"
+                    }
+                  end
+                  -- TODO performance
+                  local contact = Contact:by_pk(app.session.member.id, member.id)
+                  if contact then
+                    ui.image{
+                      attr = { 
+                        alt   = _"Saved as contact",
+                        title = _"Saved as contact"
+                      },
+                      static = "icons/16/book_edit.png"
+                    }
+                  end
+                end
+              }
+            }
+
+            if initiative then
+              columns[#columns+1] = {
+                label = _"Delegations",
+                field_attr = { style = "text-align: right;" },
+                content = function(member)
+                  if member.weight > 1 then
+                    ui.link{
+                      content = member.weight,
+                      module = "support",
+                      view = "show_incoming",
+                      params = { member_id = member.id, initiative_id = initiative.id }
+                    }
+                  end
+                end
+              }
+            end
+
+--[[            ui.list{
+              records = members,
+              columns = columns
+            }
+--]]
+---[[
+            for i, member in ipairs(members) do
               execute.view{
                 module = "member",
                 view = "_show_thumb",
-                params = { member = member }
+                params = { member = member, initiative = initiative, issue = issue, trustee = trustee }
               }
             end
+---]]
           end
         }
-            slot.put('<br style="clear: left;" />')
+        slot.put('<br style="clear: left;" />')
+        if issue then
+          ui.field.timestamp{ label = _"Last snapshot:", value = issue.snapshot }
+        end
+        if initiative then
+          ui.field.timestamp{ label = _"Last snapshot:", value = initiative.issue.snapshot }
+        end
       end
     }
   end

File app/main/member/_show.lua

   record = member,
   readonly = true,
   content = function()
-    ui.field.boolean{ label = _"Admin?",       name = "admin" }
-    ui.field.boolean{ label = _"Locked?",      name = "locked" }
+    if member.admin then
+      ui.field.boolean{ label = _"Admin?",       name = "admin" }
+    end
+    if member.locked then
+      ui.field.boolean{ label = _"Locked?",      name = "locked" }
+    end
     if member.ident_number then
       ui.field.text{    label = _"Ident number", name = "ident_number" }
     end

File app/main/member/_show_thumb.lua

 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)
   name = encode.html(member.name)
 end
 
-ui.link{
+ui.container{
   attr = { class = "member_thumb" },
-  module = "member",
-  view = "show",
-  id = member.id,
   content = function()
-    ui.image{
-      attr = { width = 48, height = 48 },
-      module    = "member",
-      view      = "avatar",
-      id        = member.id,
-      extension = "jpg"
+    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
+        if member.admin then
+          ui.image{
+            attr = { 
+              alt   = _"Member is administrator",
+              title = _"Member is administrator"
+            },
+            static = "icons/16/cog.png"
+          }
+        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
     }
-    slot.put(name)
+    
+    ui.link{
+      attr = { title = _"Show member" },
+      module = "member",
+      view = "show",
+      id = member.id,
+      content = function()
+        ui.image{
+          attr = { width = 48, height = 48 },
+          module    = "member",
+          view      = "avatar",
+          id        = member.id,
+          extension = "jpg"
+        }
+        ui.container{
+          attr = { class = "member_name" },
+          content = function()
+            slot.put(name)
+          end
+        }
+      end
+    }
   end
-}
+}

File app/main/member/show.lua

 if member.id == app.session.member.id then
   slot.put_into("actions", _"That's me!")
 else
-  slot.select("actions", function()
-    ui.link{
-      content = function()
-        ui.image{ static = "icons/16/book_add.png" }
-        slot.put(encode.html(_"Add to my contacts"))
-      end,
-      module  = "contact",
-      action  = "add_member",
-      id      = member.id
-    }
-  end)
+  --TODO performance
+  local contact = Contact:by_pk(app.session.member.id, member.id)
+  if contact then
+    slot.select("actions", function()
+      ui.field.text{ value = _"You have saved this member as contact." }
+      ui.link{
+        text = _"Remove from contacts",
+        module = "contact",
+        action = "remove_member",
+        id = contact.other_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)
+  else
+    slot.select("actions", function()
+      ui.link{
+        content = function()
+          ui.image{ static = "icons/16/book_add.png" }
+          slot.put(encode.html(_"Add to my contacts"))
+        end,
+        module  = "contact",
+        action  = "add_member",
+        id      = 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)
+  end
 end
 
 

File app/main/opinion/_action/update.lua

 
 local opinion = Opinion:by_pk(member_id, suggestion_id)
 
-if opinion and param.get("delete") then
-  opinion:destroy()
-  slot.put_into("notice", _"Your opinion has been updated")
+if param.get("delete") then
+  if opinion then
+    opinion:destroy()
+  end
+  slot.put_into("notice", _"Your opinion has been deleted")
   return
 end
 

File app/main/suggestion/_list.lua

       records = suggestions_selector:exec(),
       columns = {
         {
-          label = _"Name",
+          label = _"Suggestion",
           content = function(record)
             ui.link{
               text = record.name,
           end
         },
         {
-          label = _"Support",
+          label = _"Collective opinion",
+          label_attr = { style = "width: 101px;" },
           content = function(record)
             if record.minus2_unfulfilled_count then
               local max_value = record.initiative.issue.population
           end
         },
         {
+          label = _"My opinion",
           content = function(record)
             local degree
             local opinion = Opinion:by_pk(app.session.member.id, record.id)
           end
         },
         {
+          content = function(record)
+            local opinion = Opinion:by_pk(app.session.member.id, record.id)
+            if opinion and not opinion.fulfilled then
+              ui.image{ static = "icons/16/cross.png" }
+            end
+          end
+        },
+        {
           label = _"Suggestion currently not implemented",
           label_attr = { style = "width: 101px;" },
           content = function(record)
           end
         },
         {
+          content = function(record)
+            local opinion = Opinion:by_pk(app.session.member.id, record.id)
+            if opinion and opinion.fulfilled then
+                ui.image{ static = "icons/16/tick.png" }
+            end
+          end
+        },
+        {
           label = _"Suggestion currently implemented",
           label_attr = { style = "width: 101px;" },
           content = function(record)
           end
         },
         {
+          label_attr = { style = "width: 200px;" },
           content = function(record)
             local degree
             local opinion = Opinion:by_pk(app.session.member.id, record.id)
             end
             if opinion then
               if not opinion.fulfilled then
-                ui.image{ static = "icons/16/cross.png" }
+                local text = ""
+                if opinion.degree > 0 then
+                  text = _"Mark suggestion as implemented and express satisfaction"
+                else
+                  text = _"Mark suggestion as implemented and express dissatisfaction"
+                end
                 ui.link{
                   attr = { class = "action" },
-                  text = _"set implented",
+                  text = text,
                   module = "opinion",
                   action = "update",
                   routing = { default = { mode = "redirect", module = request.get_module(), view = request.get_view(), id = param.get_id_cgi(), params = param.get_all_cgi() } },
                   }
                 }
               else
-                ui.image{ static = "icons/16/tick.png" }
+                if opinion.degree > 0 then
+                  text = _"Mark suggestion as not implemented and express dissatisfaction"
+                else
+                  text = _"Mark suggestion as not implemented and express satisfaction"
+                end
                 ui.link{
                   attr = { class = "action" },
-                  text = _"remove implemented",
+                  text = text,
                   module = "opinion",
                   action = "update",
                   routing = { default = { mode = "redirect", module = request.get_module(), view = request.get_view(), id = param.get_id_cgi(), params = param.get_all_cgi() } },
             end
           end
         },
+        {
+          content = function(record)
+            local opinion = Opinion:by_pk(app.session.member.id, record.id)
+            if opinion then
+              if (opinion.fulfilled and opinion.degree > 0) or (not opinion.fulfilled and opinion.degree < 0) then
+                ui.image{ static = "icons/16/thumb_up_green.png" }
+              else
+                ui.image{ static = "icons/16/thumb_down_red.png" }
+              end
+            end
+          end
+        },
       }
     }
   end

File app/main/supporter/show_incoming.lua

+local initiative = Initiative:by_id(param.get("initiative_id", atom.integer))
+local issue = initiative.issue
+local member = Member:by_id(param.get("member_id", atom.integer))
+
+local members_selector = Member:new_selector()
+  :join("delegating_population_snapshot", nil, "delegating_population_snapshot.member_id = member.id")
+  :add_where{ "delegating_population_snapshot.issue_id = ?", issue.id }
+  :add_where{ "delegating_population_snapshot.event = ?", issue.latest_snapshot_event }
+  :add_where{ "delegating_population_snapshot.delegate_member_ids[1] = ?", member.id }
+  :add_field{ "delegating_population_snapshot.weight" }
+
+execute.view{
+  module = "member",
+  view = "_list",
+  params = { 
+    members_selector = members_selector,
+    issue = issue,
+    trustee = member
+  }
+}

File config/default.lua

 config.app_name = "LiquidFeedback"
-config.app_version = "alpha3"
+config.app_version = "alpha4"
 
 config.app_title = config.app_name .. " (" .. request.get_config_name() .. " environment)"
 
 config.app_service_provider = "Snake Oil<br/>10000 Berlin<br/>Germany"
 
 config.member_image_convert = {
-  avatar = { "convert", "-", "-thumbnail", "48x48", "-" }
+  avatar = { "convert", "-", "-thumbnail", "48x48", "jpeg:-" }
 }
 
 -- uncomment the following two lines to use C implementations of chosen

File locale/translations.de.lua

 #!/usr/bin/env lua
 return {
+["(change)"] = "(ändern)";
+["+ #{weight}"] = false;
 ["A-Z"] = false;
 ["About"] = false;
 ["About LiquidFeedback"] = "Über LiquidFeedback";
 ["Admin"] = "Admin";
 ["Admin menu"] = "Admin Menü";
 ["Admin?"] = "Admin?";
+["Administrator"] = false;
 ["Admitted"] = "zugelassen";
 ["Any"] = "Alle";
 ["Area"] = "Themenbereich";
 ["Click here to close."] = "Zum Schließen hier klicken";
 ["Close"] = "Schließen";
 ["Closed"] = "geschlossen";
+["Collective opinion"] = "Meinungsbild";
 ["Commit suggestion"] = "Anregung speichern";
 ["Compare"] = "Vergleichen";
 ["Contacts"] = "Kontakte";
 ["Content"] = "Inhalt";
+["Create alternative initiative"] = "Alternative Initiative hinzufügen";
 ["Create new area"] = "Neuen Themenbereich anlegen";
 ["Create new issue"] = "Neues Thema anlegen";
 ["Created at"] = "Erzeugt am/um";
 ["Diff"] = false;
 ["Direct member count"] = "Anzahl Direktmitglieder";
 ["Direct supporter [change]"] = "Direkte Unterstützung [ändern]";
+["Discussion"] = "Diskussion";
+["Discussion URL"] = "Diskussions-URL";
 ["Draft"] = "Entwurf";
 ["Edit"] = "Bearbeiten";
 ["Edit draft"] = "Entwurf bearbeiten";
+["Edit initiative"] = "Initiative bearbeiten";
 ["Edit my page"] = "Meine Seite bearbeiten";
 ["Error while updating member, database reported:<br /><br /> (#{errormessage})"] = "Fehler beim aktualisieren des Mitglieds, die Datenbank berichtet folgenden Fehler:<br /><br /> (#{errormessage})";
+["External discussion"] = "Externe Diskussion";
 ["External memberships"] = "Externe Mitgliedschaften";
 ["External posts"] = "Externe Ämter";
 ["Filter"] = false;
 ["In discussion"] = "In Diskussion";
 ["Incoming delegations"] = "Eingehende Delegationen";
 ["Initiative successfully created"] = "Initiative erfolgreich erzeugt";
+["Initiative successfully updated"] = "Initiative erfolgreich aktualisiert";
 ["Initiative: '#{name}'"] = "Initiative: '#{name}'";
 ["Initiatives"] = "Initiativen";
 ["Initiators"] = "Initiatoren";
 ["Interest not existant"] = "Interesse existiert nicht";
 ["Interest removed"] = "Interesse entfernt";
 ["Interest updated"] = "Interesse aktualisiert";
+["Interested members"] = "Interessierte Mitglieder";
 ["Internal posts"] = "Interne Ämter";
 ["Invalid username or password!"] = "Ungültiger Benutzername oder Kennwort";
 ["Issue"] = "Thema";
 ["Issue delegation"] = "Issue-Delegation";
 ["Issue policy"] = "Regelwerk für Thema";
 ["Issues"] = "Themen";
+["Last snapshot:"] = "Letzte Auszählung:";
 ["License"] = "Lizenz";
 ["Locked?"] = "Gesperrt?";
 ["Logged in as:"] = "Angemeldet als:";
 ["Login successful!"] = "Anmeldung erfolgreich";
 ["Logout"] = "Abmelden";
 ["Logout successful"] = "Abmeldung erfolgreich";
+["Mark suggestion as implemented and express dissatisfaction"] = "Anregung als umgesetzt markieren und Unzufriedenheit ausdrücken";
+["Mark suggestion as implemented and express satisfaction"] = "Anregung als umgesetzt markieren und Zufriedenheit ausdrücken";
+["Mark suggestion as not implemented and express dissatisfaction"] = "Anregung als nicht umgesetzt markieren und Unzufriedenheit ausdrücken";
+["Mark suggestion as not implemented and express satisfaction"] = "Anregung als nicht umgesetzt markieren und Zufriedenheit ausdrücken";
 ["Member '#{member}'"] = "Mitglied '#{member}'";
 ["Member has been removed from your contacts"] = "Mitglied wurde aus Deinen Kontakten entfernt";
 ["Member has been saved as private contact"] = "Mitglied wurde als privater Kontakt gespeichert";
 ["Member has been saved as public contact"] = "Mitglied wurde als öffentlicher Kontakt gespeichert";
+["Member is administrator"] = "Mitglied ist Administrator";
 ["Member is already saved in your contacts!"] = "Mitglied ist schon in Deinen Kontakten!";
 ["Member list"] = "Mitgliederliste";
 ["Member login"] = "Mitglied Login";
 ["Membership removed"] = "Mitgliedschaft entfernt";
 ["Membership updated"] = "Mitgliedschaft aktualisiert";
 ["Mobile phone"] = "Mobiltelefon";
+["My opinion"] = "Meine Meinung";
 ["Name"] = "Name";
 ["New"] = "Neu";
 ["New draft has been added to initiative"] = "Neuer Entwurf wurde der Initiative hinzugefügt";
 ["New passwords does not match."] = "Du hast nicht zweimal das gleiche Kennwort eingegeben";
 ["New passwords is too short."] = "Das neue Kennwort ist zu kurz";
 ["Newest"] = "Neueste";
-["Next states"] = "Nächste Statuse";
+["Next state"] = "Nächster Zustand";
 ["No supporter [change]"] = "Keine Unterstützung (ändern)";
+["Number of incoming delegations, follow link to see more details"] = "Anzahl eingehender Delegationen, Link folgen für mehr Details";
 ["OK"] = "OK";
 ["Old draft revision"] = "Alte Revision des Entwurfs";
 ["Old drafts"] = "Alte Entwürfe";
 ["Register new member"] = "Neues Mitglied registrieren";
 ["Remove"] = "Entfernen";
 ["Remove autoreject"] = "Auto-Ablehnen abschalten";
+["Remove from contacts"] = "Aus den Kontakten entfernen";
 ["Remove my interest"] = "Interesse abmelden";
 ["Remove my membership"] = "Mitgliedschaft aufgeben";
 ["Remove my support from this initiative"] = "Meine Unterstützung der Initiative entziehen";
 ["Revoke"] = "Widerrufen";
 ["Revoked at"] = "Zurückgezogen am/um";
 ["Save"] = "Speichern";
+["Saved as contact"] = "Als Kontakt speichern";
 ["Search"] = "Suchen";
 ["Search initiatives"] = "Suche Initiativen";
 ["Search issues"] = "Suche Themen";
 ["Show areas not in use"] = "Zeige nicht verwendente Themenbereiche";
 ["Show diff"] = "Änderungen anzeigen";
 ["Show locked members"] = "Zeige gesperrte Mitglieder";
+["Show member"] = "Mitglied anzeigen";
 ["Software"] = false;
 ["State"] = "Zustand";
 ["Statement"] = false;
+["Suggestion"] = "Anregung";
 ["Suggestion currently implemented"] = "Anregung zur Zeit umgesetzt";
 ["Suggestion currently not implemented"] = "Anregung zur Zeit nicht umgesetzt";
 ["Suggestion for initiative: '#{name}'"] = "Anregung für Initiative '#{name}'";
 ["Supporter"] = "Unterstützer";
 ["That's me!"] = "Das bin ich";
 ["The drafts do not differ"] = "Die Entwürfe unterscheiden sich nicht";
+["Time left"] = "Restzeit";
 ["Trustee"] = "Bevollmächtigter";
 ["Unknown author"] = "Unbekannter Autor";
 ["Upload avatar"] = "Avatar hochladen";
 ["You are not a member. [more]"] = "Du bist kein Mitglied. [mehr]";
 ["You are not interested. [more]"] = "Du bist nicht interessiert. [mehr]";
 ["You didn't saved any member as contact yet."] = "Du hast noch kein Mitglied als Kontakt gespeichert!";
+["You have saved this member as contact"] = "Du hast das Mitglied als Kontakt gespeichert";
+["You have saved this member as contact."] = "Du hast das Mitglied als Kontakt gespeichert.";
+["You need to be logged in, to use this system."] = "Du musst eingeloggt sein, um das System zu benutzen";
 ["Your delegation for this area has been deleted."] = "Deine Delegation für dieses Themengebiet wurde gelöscht";
 ["Your delegation for this area has been updated."] = "Deine Delegation für dieses Themengebiet wurde geändert";
 ["Your delegation for this issue has been deleted."] = "Deine Delegation für dieses Thema wurde gelöscht";
 ["Your delegation for this issue has been updated."] = "Deine Delegation für dieses Thema wurde geändert";
 ["Your global delegation has been deleted."] = "Deine globale Delegation wurde gelöscht";
 ["Your global delegation has been updated."] = "Deine globale Delegation würde geändert";
+["Your opinion has been deleted"] = "Deine Meinung wurde gelöscht";
 ["Your opinion has been updated"] = "Deine Meinung wurde aktualisiert";
 ["Your page has been updated"] = "Deine Seite wurde aktualisiert";
 ["Your password has been updated successfully"] = "Das Kennwort wurde erfolgreich geändert";
 ["Your vote is delegated. [more]"] = "Deine Stimme ist delegiert. [mehr]";
 ["Z-A"] = false;
 ["admission_time"] = false;
+["blank"] = false;
 ["delete<br /><br />"] = false;
 ["discussion_time"] = false;
 ["email"] = false;
 ["must"] = "muss";
 ["must not"] = "darf nicht";
 ["neutral"] = "neutral";
-["remove implemented"] = "entferne umgesetzt";
-["set implented"] = "setze umgesetzt";
 ["should"] = "soll";
 ["should not"] = "soll nicht";
 ["verification_time"] = false;

File model/initiative.lua

   ref                   = 'supporting_members'
 }
 
+Initiative:add_reference{
+  mode                  = 'mm',
+  to                    = "Member",
+  this_key              = 'id',
+  that_key              = 'id',
+  connected_by_table    = 'direct_supporter_snapshot',
+  connected_by_this_key = 'initiative_id',
+  connected_by_that_key = 'member_id',
+  ref                   = 'supporting_members_snapshot'
+}
+
 function Initiative:get_search_selector(search_string)
   return self:new_selector()
     :add_field( {'"highlight"("initiative"."name", ?)', search_string }, "name_highlighted")

File model/issue.lua

   ref                   = 'members'
 }
 
+Issue:add_reference{
+  mode                  = 'mm',
+  to                    = "Member",
+  this_key              = 'id',
+  that_key              = 'id',
+  connected_by_table    = 'direct_interest_snapshot',
+  connected_by_this_key = 'issue_id',
+  connected_by_that_key = 'member_id',
+  ref                   = 'interested_members_snapshot'
+}
+
 function Issue:get_state_name_for_state(value)
   local state_name_table = {
     new          = _"New",
-    accepted     = _"Accepted",
+    accepted     = _"Discussion",
     frozen       = _"Frozen",
     voting       = _"Voting",
     finished     = _"Finished",
 
 function Issue.object_get:state()
   if self.accepted then
-    if self.fully_frozen then
-      return "voting"
-    elseif self.half_frozen then
-      return "frozen"
-    elseif self.closed then
+    if self.closed then
       if self.ranks_available then
         return "finished"
       else
         return "cancelled"
       end
+    elseif self.fully_frozen then
+      return "voting"
+    elseif self.half_frozen then
+      return "frozen"
     else
       return "accepted"
     end

File static/icons/16/bullet_disk.png

Added
New image

File static/icons/16/comments.png

Added
New image

File static/style.css

   border: 1px solid #ccc;
 }
 
-.member_thumb:hover {
-  border: 1px solid #000;
+.member_thumb a:hover div {
+  background-color: #444;
+  color: #fff;
+}
+
+.member_thumb .flags a:hover {
+  background-color: #444;
+  color: #fff;
 }
 
 .member_thumb img {
   margin-right: 0.5em;
   vertical-align: bottom;
+  float: left;
 }
 
 .member_thumb div {
   display: inline;
+}
+
+.member_thumb .member_name {
+  display: block;
+  margin-top: 3ex;
   margin-right: 0.5em;
 }
 
+.member_thumb .flags {
+  float: right;
+  font-size: 75%;
+}
+
 .draft_content {
   background-color: #eee;
   border: 1px solid #ccc;
   border: 2px solid #faa;
   background-color: #fee;
   padding: 1ex;
-}
+}
+