Commits

Anonymous committed 733f65c

Bugfixes, feature enhancements, code-cleanup, and major work on API

Details:
- API
-- Allow relation name to be passed to helper function util.autoapi{...}
-- Added area API
-- Bugfixes in API
--- Correctly return initiatives (bug #162)
--- Correctly process "id" parameter for initiative API
--- Bugfix related to "state" parameter (bug #165)
--- Changed constant "discussion" to "accepted" (in model/issue.lua, used by API)
--- Fixed JSON encoding in auto_api (bug #181)
--- Ignore list filter "voted" in case of public access
--- Enable access to API without session
- Work on RSS feed (incomplete yet)
- Other bugfixes
-- Handle empty browser identification string
-- Handle invalid date in member/update.lua (bugs #24 #109 #115 #136)
-- Better handle errors while converting uploaded images. (bug #79 +5 duplicates)
-- Don't display revoked initiatives in list of new drafts (bug #134)
-- Fixed syntax error in app/main/member/_action/update_name.lua throwing unexpected error, when new name was too short
-- Do not display refresh support button for revoked initiatives
-- Repaired issue search (bug #150)
-- Fixed typos in german translation files
--- "initi(i)erte"
--- "Er(g)eignisse" (bug #161)
- Code cleanup
-- Removed deprecated motd files locale/motd/de.txt and locale/motd/de_public.txt
-- Removed redundant code in app/main/index/_updated_drafts.lua
- New features and (optical) enhancements
-- Support change of notify email; notification of not approved address added to start page
-- Settings dialog splitted into single pages
-- Mark deactivated members
-- Calendar for birthday selection in profile
-- Policy list public readable when public access is enabled

  • Participants
  • Parent commits e400519
  • Tags beta19

Comments (0)

Files changed (38)

File app/main/_filter/21_auth.lua

      or request.get_view() == "show_tab"
     )
     or request.get_module() == "policy" and request.get_view() == "show"
+    or request.get_module() == "policy" and request.get_view() == "list"
     or request.get_module() == "issue" and request.get_view() == "show"
     or request.get_module() == "issue" and request.get_view() == "show_tab"
     or request.get_module() == "initiative" and request.get_view() == "show"
     or request.get_module() == "initiative" and request.get_view() == "show_partial"
     or request.get_module() == "initiative" and request.get_view() == "show_tab"
+    or request.get_module() == "initiative" and request.get_view() == "show.rss"
     or request.get_module() == "suggestion" and request.get_view() == "show"
     or request.get_module() == "draft" and request.get_view() == "diff"
   then
 
 end
 
+if config.api_enabled and request.get_module() == "api" then
+  auth_needed = false
+end
+
 if config.public_access and not app.session.member_id and auth_needed and request.get_module() == "index" and request.get_view() == "index" then
   request.redirect{ module = "area", view = "list" }
   return

File app/main/_filter_view/34_stylesheet.lua

   slot.put_into("stylesheet_url", config.absolute_base_url .. "static/style.css")
 end
 
-if os.getenv("HTTP_USER_AGENT"):find("Android.*AppleWebKit.*Mobile Safari") then
+if os.getenv("HTTP_USER_AGENT") and os.getenv("HTTP_USER_AGENT"):find("Android.*AppleWebKit.*Mobile Safari") then
   slot.select("html_head", function()
     ui.tag{
       tag = "style",
   }
 else
   execute.inner()
-end
+end

File app/main/api/area.lua

+local id     = param.get("id")
+local min_id = param.get("min_id")
+local max_id = param.get("max_id")
+local order  = param.get("order")
+local limit  = param.get("limit", atom.integer)
+
+local areas_selector = Area:new_selector()
+
+if id then
+  areas_selector:add_where{"area.id = ?", id}
+end
+
+if min_id then
+  areas_selector:add_where{"area.id >= ?", min_id}
+end
+
+if max_id then
+  areas_selector:add_where{"area.id <= ?", max_id}
+end
+
+if order == "name" then
+  areas_selector:add_order_by("area.name")
+end
+
+if order == "member_weight" then
+  areas_selector:add_order_by("area.member_weight DESC")
+end
+
+areas_selector:add_order_by("area.id")
+
+if limit then
+  initiatives_selector:limit(limit)
+end
+
+local api_engine = param.get("api_engine") or "xml"
+
+local fields = {
+
+  { name = "id",                   field = "area.id" },
+  { name = "name",                 field = "area.name" },
+  { name = "description",          field = "area.description" },
+  { name = "direct_member_count",  field = "area.direct_member_count" },
+  { name = "member_weight",        field = "area.member_weight" },
+  { name = "autoreject_weight",    field = "area.autoreject_weight" },
+  { name = "active",               field = "area.active" },
+
+}
+
+util.autoapi{
+  relation_name = "area",
+  selector      = areas_selector,
+  fields        = fields,
+  api_engine    = api_engine
+}

File app/main/api/initiative.lua

-local id             = param.get_id()
+local id             = param.get("id")
 local min_id         = param.get("min_id")
 local max_id         = param.get("max_id")
 local area_id        = param.get("area_id", atom.integer)
 local order          = param.get("order")
 
 local initiatives_selector = Initiative:new_selector()
-  :join("issue", nil, "issue.id = initiative.id")
+  :join("issue", nil, "issue.id = initiative.issue_id")
   :join("area", nil, "area.id = issue.area_id")
   :join("policy", nil, "policy.id = issue.policy_id")
 
 end
 
 if state then
-  Issue:modify_selector_for_state(state)
+  Issue:modify_selector_for_state(initiatives_selector, state)
 end
 
 if agreed then
 }
 
 util.autoapi{
+  relation_name = "initiative",
   selector = initiatives_selector,
   fields = fields,
   api_engine = api_engine

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

 
 if member then
   member.notify_email = member.notify_email_unconfirmed
-  member.notify_email_unconfirmed = nil
-  member.notify_email_secret = nil
+  member.notify_email_unconfirmed   = nil
+  member.notify_email_secret        = nil
+  member.notify_email_secret_expiry = nil
+  member.notify_email_lock_expiry   = nil
   member:save()
   slot.put_into("notice", _"Email address is confirmed now")
 else

File app/main/index/_updated_drafts.lua

-local initiatives_selector = Initiative:new_selector()
-  :join("issue", "_issue_state", "_issue_state.id = initiative.issue_id AND _issue_state.closed ISNULL AND _issue_state.fully_frozen ISNULL")
-  :join("current_draft", "_current_draft", "_current_draft.initiative_id = initiative.id")
-  :join("supporter", "supporter", { "supporter.member_id = ? AND supporter.initiative_id = initiative.id AND supporter.draft_id < _current_draft.id", app.session.member_id })
-
+local initiatives_selector = param.get("initiatives_selector", "table")
 if initiatives_selector:count() > 0 then
   ui.container{
     attr = { style = "font-weight: bold;" },

File app/main/initiative/_show.lua

             new_draft_id = new_draft_id
           }
         }
-        slot.put(" ")
-        ui.link{
-          text   = _"Refresh support to current draft",
-          module = "initiative",
-          action = "add_support",
-          id     = initiative.id,
-          routing = {
-            default = {
-              mode = "redirect",
-              module = "initiative",
-              view = "show",
-              id = initiative.id
+        if not initiative.revoked then
+          slot.put(" ")
+          ui.link{
+            text   = _"Refresh support to current draft",
+            module = "initiative",
+            action = "add_support",
+            id     = initiative.id,
+            routing = {
+              default = {
+                mode = "redirect",
+                module = "initiative",
+                view = "show",
+                id = initiative.id
+              }
             }
           }
-        }
+        end
       end
     }
   end

File app/main/initiative/list_rss.lua

 
 ui.tag{
   tag = "id",
-  content = "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"
+--  content = "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"
 }
 
+--[[
 ui.tag{
   tag = "updated",
   content = "2003-12-14T10:20:09Z"
 }
+--]]
 
 for i, initiative in ipairs(initiatives) do
   ui.tag{

File app/main/initiative/show.rss.lua

   slot.put("</item>")
 end
 
-
 local initiative = Initiative:by_id(param.get_id())
 
 rss_channel{
   title = initiative.name,
   description = initiative.current_draft.content,
   language = "de",
-  copyright = initiative.current_draft.author.name,
-  pubDate = "Tue, 8 Jul 2008 2:43:19"
 }
 
 for i, suggestion in ipairs(initiative.suggestions) do
-  
+
   local text = suggestion.name
 
   text = text .. " ("
   rss_item{
     title = text,
     description = suggestion.content,
-    link = "http://localhost/lf/suggestion/show/" .. tostring(suggestion.id) .. ".html",
-    author = "",
-    guid = "guid",
-    pubDate = "Tue, 8 Jul 2008 2:43:19"
+    link = request.get_base_url() .. "/lf/suggestion/show/" .. tostring(suggestion.id) .. ".html",
   }
 
 end

File app/main/issue/_list.lua

 }
 
 
-if param.get("filter") == "frozen" then
+if app.session.member and param.get("filter") == "frozen" then
   filters[#filters+1] = {
     label = _"Filter",
     name = "filter_voting",

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

   "statement"
 )
 
+if tostring(app.session.member.birthday) == "invalid_date" then
+  app.session.member.birthday = nil
+  slot.put_into("error", _"Date format is not valid. Please use following format: YYYY-MM-DD")
+  return false
+end
+
 app.session.member:save()
 
 

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

+local resend = param.get("resend", atom.boolean)
+
+if app.session.member.notify_email_locked then
+  if resend then
+    slot.put_into("error", _"We have sent an email with activation link already in the last hour. Please try again later.")
+  else
+    slot.put_into("error", _"You can change your email address only once per hour. Please try again later.")
+  end
+  return false
+end
+
+local email
+if resend then
+  email = app.session.member.notify_email_unconfirmed
+else
+  email = param.get("email")
+end
+
+email = util.trim(email)
+
+if #email < 3 then 
+  slot.put_into("error", _"This email address is too short!")
+  return false
+end
+
+local success = app.session.member:set_notify_email(email)
+
+if not success then
+  slot.put_into("error", _"We couldn't deliver a confirmation mail to this address. Please check entered email address.")
+  return false
+end
+
+slot.put_into("notice", _"Your email address has been changed, please check for confirmation email with activation link!")

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

       local convert_func = config.member_image_convert_func[image_type]
       local data_scaled, err, status = convert_func(data)
       if status ~= 0 or data_scaled == nil then
-      error("error while converting image")
+        slot.put_into("error", _"Error while converting image. Please note, that only JPG files are supported!")
+        return false
       end
 
       if not member_image then

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

 
 name = util.trim(name)
 
-if #name < 3 then 
-  slot.put_into(_error, _"This name is too short!")
+if #name < 3 then
+  slot.put_into("error", _"This name is too short!")
+  return false
 end
 
-
 app.session.member.name = name
 
 local db_error = app.session.member:try_save()

File app/main/member/_email_unconfirmed.lua

+if app.session.member.notify_email_unconfirmed then
+
+  local current = Member:new_selector()
+    :add_where{ "id = ?", app.session.member_id }
+    :add_where("notify_email_unconfirmed NOTNULL")
+    :add_where("notify_email_secret_expiry > now()")
+    :optional_object_mode()
+    :exec()
+
+  ui.heading{ level = 2, content = _"Notification address unconfirmed" }
+
+  if current then
+    ui.tag{
+      tag = "div",
+      content = _("You didn't confirmed your email address '#{email}'. You have received an email with an activation link.", { email = app.session.member.notify_email_unconfirmed })
+    }
+  else
+    ui.tag{
+      tag = "div",
+      content = _("You didn't confirmed your email address '#{email}' within 7 days.", { email = app.session.member.notify_email_unconfirmed })
+    }
+  end
+  slot.put("<br />")
+
+  ui.link{
+    text = _"Change email address",
+    module = "member",
+    view = "settings_email",
+  }
+  slot.put("<br />")
+  slot.put("<br />")
+
+  ui.link{
+    text = _("Resend activation email to '#{email}'", { email = app.session.member.notify_email_unconfirmed }),
+    module = "member",
+    action = "update_email",
+    params = {
+      resend = true
+    },
+    routing = {
+      default = {
+        mode = "redirect",
+        module = "index",
+        view = "index"
+      }
+    }
+  }
+
+end

File app/main/member/_show_thumb.lua

       attr = { class = "flags" },
       content = function()
 
+        if not member.active then
+          local text = _"Member is deactivated"
+          ui.image{
+            attr = { alt = text, title = text },
+            static = "icons/16/cross.png"
+          }
+        end
+
         if member.grade then
           ui.link{
             module = "vote",

File app/main/member/developer_settings.lua

-slot.put_into("title", _"Developer features")
+slot.put_into("title", _"Developer settings")
 
 slot.select("actions", function()
   ui.link{

File app/main/member/edit.lua

     ui.field.text{ label = _"Organizational unit", name = "organizational_unit" }
     ui.field.text{ label = _"Internal posts", name = "internal_posts" }
     ui.field.text{ label = _"Real name", name = "realname" }
-    ui.field.text{ label = _"Birthday" .. " YYYY-MM-DD ", name = "birthday" }
+    ui.field.text{ label = _"Birthday" .. " YYYY-MM-DD ", name = "birthday", attr = { id = "profile_birthday" } }
+    ui.script{ static = "gregor.js/gregor.js" }
+    util.gregor("profile_birthday", "document.getElementById('timeline_search_date').form.submit();")
     ui.field.text{ label = _"Address", name = "address", multiline = true }
     ui.field.text{ label = _"email", name = "email" }
     ui.field.text{ label = _"xmpp", name = "xmpp_address" }

File app/main/member/settings.lua

-
 slot.put_into("title", _"Settings")
 
 slot.select("actions", function()
     module = "index",
     view = "index"
   }
-
-  ui.link{
-    content = function()
-        ui.image{ static = "icons/16/wrench.png" }
-        slot.put(_"Developer features")
-    end,
-    module = "member",
-    view = "developer_settings"
-  }
-
 end)
 
-ui.heading{ content = _"Display settings" }
-util.help("member.settings.display", _"Display settings")
-
-ui.form{
-  attr = { class = "vertical" },
-  module = "member",
-  action = "update_display",
-  routing = {
-    ok = {
-      mode = "redirect",
-      module = "index",
-      view = "index"
-    }
-  },
-  content = function()
-    ui.field.select{
-      label = _"Type of tabs",
-      foreign_records = {
-        { id = "tabs",                     name = _"Tabs" },
-        { id = "accordeon",                name = _"Accordion (none expanded)" .. " === " .. _"EXPERIMENTAL FEATURE" .. " ===" },
-        { id = "accordeon_first_expanded", name = _"Accordion (first expanded)" .. " === " .. _"EXPERIMENTAL FEATURE" .. " ===" },
---        { id = "accordeon_all_expanded",   name = _"Accordion (all expanded)" }
-      },
-      foreign_id = "id",
-      foreign_name = "name",
-      name = "tab_mode",
-      value = app.session.member:get_setting_value("tab_mode")
-    }
-    ui.field.select{
-      label = _"Number of initiatives to preview",
-      foreign_records = {
-        { id =  3, name = "3" },
-        { id =  4, name = "4" },
-        { id =  5, name = "5" },
-        { id =  6, name = "6" },
-        { id =  7, name = "7" },
-        { id =  8, name = "8" },
-        { id =  9, name = "9" },
-        { id = 10, name = "10" },
-      },
-      foreign_id = "id",
-      foreign_name = "name",
-      name = "initiatives_preview_limit",
-      value = app.session.member:get_setting_value("initiatives_preview_limit")
-    }
-    ui.submit{ value = _"Change display settings" }
-  end
+ui.tag{
+  tag = "div",
+  content = _"You can change the following settings:"
 }
 
-ui.heading{ content = _"Change your name" }
-util.help("member.settings.name", _"Change name")
-
-ui.form{
-  attr = { class = "vertical" },
-  module = "member",
-  action = "update_name",
-  routing = {
-    ok = {
-      mode = "redirect",
-      module = "index",
-      view = "index"
-    }
-  },
-  content = function()
-    ui.field.text{ label = _"Name", name = "name", value = app.session.member.name }
-    ui.submit{ value = _"Change name" }
-  end
+local pages = {
+  { view = "settings_display",   text = _"Display settings" },
+  { view = "settings_email",     text = _"Change your notification email address" },
+  { view = "settings_name",      text = _"Change your name" },
+  { view = "settings_login",     text = _"Change your login" },
+  { view = "settings_password",  text = _"Change your password" },
+  { view = "developer_settings", text = _"Developer settings" },
 }
 
-ui.heading{ content = _"Change your login" }
-util.help("member.settings.login", _"Change login")
-
-ui.form{
-  attr = { class = "vertical" },
-  module = "member",
-  action = "update_login",
-  routing = {
-    ok = {
-      mode = "redirect",
-      module = "index",
-      view = "index"
+ui.list{
+  attr = { class = "menu_list" },
+  style = "ulli",
+  records = pages,
+  columns = {
+    {
+      content = function(page)
+        ui.link{
+          module = "member",
+          view = page.view,
+          text = page.text
+        }
+      end
     }
-  },
-  content = function()
-    ui.field.text{ label = _"Login", name = "login", value = app.session.member.login }
-    ui.submit{ value = _"Change login" }
-  end
+  }
 }
 
-ui.heading{ content = _"Change your password" }
-util.help("member.settings.password", _"Change password")
-
-ui.form{
-  attr = { class = "vertical" },
-  module = "member",
-  action = "update_password",
-  routing = {
-    ok = {
-      mode = "redirect",
-      module = "index",
-      view = "index"
-    }
-  },
-  content = function()
-    ui.field.password{ label = _"Old password", name = "old_password" }
-    ui.field.password{ label = _"New password", name = "new_password1" }
-    ui.field.password{ label = _"Repeat new password", name = "new_password2" }
-    ui.submit{ value = _"Change password" }
-  end
-}

File app/main/member/settings_display.lua

+slot.put_into("title", _"Display settings")
+
+slot.select("actions", function()
+  ui.link{
+    content = function()
+        ui.image{ static = "icons/16/cancel.png" }
+        slot.put(_"Cancel")
+    end,
+    module = "member",
+    view = "settings"
+  }
+end)
+
+
+util.help("member.settings.display", _"Display settings")
+
+ui.form{
+  attr = { class = "vertical" },
+  module = "member",
+  action = "update_display",
+  routing = {
+    ok = {
+      mode = "redirect",
+      module = "index",
+      view = "index"
+    }
+  },
+  content = function()
+    ui.field.select{
+      label = _"Type of tabs",
+      foreign_records = {
+        { id = "tabs",                     name = _"Tabs" },
+        { id = "accordeon",                name = _"Accordion (none expanded)" .. " === " .. _"EXPERIMENTAL FEATURE" .. " ===" },
+        { id = "accordeon_first_expanded", name = _"Accordion (first expanded)" .. " === " .. _"EXPERIMENTAL FEATURE" .. " ===" },
+--        { id = "accordeon_all_expanded",   name = _"Accordion (all expanded)" }
+      },
+      foreign_id = "id",
+      foreign_name = "name",
+      name = "tab_mode",
+      value = app.session.member:get_setting_value("tab_mode")
+    }
+    ui.field.select{
+      label = _"Number of initiatives to preview",
+      foreign_records = {
+        { id =  3, name = "3" },
+        { id =  4, name = "4" },
+        { id =  5, name = "5" },
+        { id =  6, name = "6" },
+        { id =  7, name = "7" },
+        { id =  8, name = "8" },
+        { id =  9, name = "9" },
+        { id = 10, name = "10" },
+      },
+      foreign_id = "id",
+      foreign_name = "name",
+      name = "initiatives_preview_limit",
+      value = app.session.member:get_setting_value("initiatives_preview_limit")
+    }
+    ui.submit{ value = _"Change display settings" }
+  end
+}

File app/main/member/settings_email.lua

+slot.put_into("title", _"Change your notification email address")
+
+slot.select("actions", function()
+  ui.link{
+    content = function()
+        ui.image{ static = "icons/16/cancel.png" }
+        slot.put(_"Cancel")
+    end,
+    module = "member",
+    view = "settings"
+  }
+end)
+
+util.help("member.settings.email_address", _"Change email")
+
+ui.form{
+  attr = { class = "vertical" },
+  module = "member",
+  action = "update_email",
+  routing = {
+    ok = {
+      mode = "redirect",
+      module = "index",
+      view = "index"
+    }
+  },
+  content = function()
+    if app.session.member.notify_email then
+      ui.field.text{ label = _"Confirmed address", value = app.session.member.notify_email, readonly = true }
+    end
+    if app.session.member.notify_email_unconfirmed then
+      ui.field.text{ label = _"Unconfirmed address", value = app.session.member.notify_email_unconfirmed, readonly = true }
+    end
+    ui.field.text{ label = _"New address", name = "email" }
+    ui.submit{ value = _"Change email" }
+  end
+}
+

File app/main/member/settings_login.lua

+slot.put_into("title", _"Change your login")
+
+slot.select("actions", function()
+  ui.link{
+    content = function()
+        ui.image{ static = "icons/16/cancel.png" }
+        slot.put(_"Cancel")
+    end,
+    module = "member",
+    view = "settings"
+  }
+end)
+
+util.help("member.settings.login", _"Change login")
+
+ui.form{
+  attr = { class = "vertical" },
+  module = "member",
+  action = "update_login",
+  routing = {
+    ok = {
+      mode = "redirect",
+      module = "index",
+      view = "index"
+    }
+  },
+  content = function()
+    ui.field.text{ label = _"Login", name = "login", value = app.session.member.login }
+    ui.submit{ value = _"Change login" }
+  end
+}
+

File app/main/member/settings_name.lua

+slot.put_into("title", _"Change your name")
+
+slot.select("actions", function()
+  ui.link{
+    content = function()
+        ui.image{ static = "icons/16/cancel.png" }
+        slot.put(_"Cancel")
+    end,
+    module = "member",
+    view = "settings"
+  }
+end)
+
+util.help("member.settings.name", _"Change name")
+
+ui.form{
+  attr = { class = "vertical" },
+  module = "member",
+  action = "update_name",
+  routing = {
+    ok = {
+      mode = "redirect",
+      module = "index",
+      view = "index"
+    }
+  },
+  content = function()
+    ui.field.text{ label = _"Name", name = "name", value = app.session.member.name }
+    ui.submit{ value = _"Change name" }
+  end
+}

File app/main/member/settings_password.lua

+slot.put_into("title", _"Change your password")
+
+slot.select("actions", function()
+  ui.link{
+    content = function()
+        ui.image{ static = "icons/16/cancel.png" }
+        slot.put(_"Cancel")
+    end,
+    module = "member",
+    view = "settings"
+  }
+end)
+
+util.help("member.settings.password", _"Change password")
+
+ui.form{
+  attr = { class = "vertical" },
+  module = "member",
+  action = "update_password",
+  routing = {
+    ok = {
+      mode = "redirect",
+      module = "index",
+      view = "index"
+    }
+  },
+  content = function()
+    ui.field.password{ label = _"Old password", name = "old_password" }
+    ui.field.password{ label = _"New password", name = "new_password1" }
+    ui.field.password{ label = _"Repeat new password", name = "new_password2" }
+    ui.submit{ value = _"Change password" }
+  end
+}

File app/main/member/show.lua

 slot.put_into("title", encode.html(_"Member '#{member}'":gsub("#{member}", member.name)))
 
 slot.select("actions", function()
-  if member.id == app.session.member.id then
-  else
+  if not (member.id == app.session.member.id) then
+if not member.active then
+  ui.tag{
+    tag = "div",
+    attr = { class = "interest deactivated_member_info" },
+    content = _"This member is deactivated."
+  }
+  slot.put(" ")
+end
     --TODO performance
     local contact = Contact:by_pk(app.session.member.id, member.id)
     if contact then
           }
         }
       }
-    else
+    elseif member.active then
       ui.link{
         image   = { static = "icons/16/book_add.png" },
         text    = _"Add to my contacts",

File app/main/member/show_tab.lua

 
 if show_as_homepage and app.session.member_id == member.id then
 
+  if app.session.member.notify_email_unconfirmed then
+    tabs[#tabs+1] = {
+      class = "yellow",
+      name = "email_unconfirmed",
+      label = _"Email unconfirmed",
+      icon = { static = "icons/16/bell.png" },
+      module = "member",
+      view = "_email_unconfirmed",
+      params = {}
+    }
+  end
+
   if config.motd_intern then
     tabs[#tabs+1] = {
       class = "yellow",
     :join("issue", "_issue_state", "_issue_state.id = initiative.issue_id AND _issue_state.closed ISNULL AND _issue_state.fully_frozen ISNULL")
     :join("current_draft", "_current_draft", "_current_draft.initiative_id = initiative.id")
     :join("supporter", "supporter", { "supporter.member_id = ? AND supporter.initiative_id = initiative.id AND supporter.draft_id < _current_draft.id", app.session.member_id })
+    :add_where("initiative.revoked ISNULL")
 
   if updated_drafts_selector:count() > 0 then
     tabs[#tabs+1] = {

File config/default.lua

 config.app_name = "LiquidFeedback"
-config.app_version = "beta18"
+config.app_version = "beta19"
 
 config.app_title = config.app_name .. " (" .. request.get_config_name() .. " environment)"
 
 
 config.api_enabled = false
 
-config.feature_rss_enabled = true
+config.feature_rss_enabled = false -- feature is broken
 
 -- OpenID authentication is not fully implemented yet, DO NOT USE BEFORE THIS NOTICE HAS BEEN REMOVED!
 config.auth_openid_enabled = false

File env/util/autoapi.lua

 function util.autoapi_xml(args)
+  local relation_name = assert(args.relation_name)
   local selector = assert(args.selector)
   local fields = assert(args.fields)
   local rows = selector:exec()
-  slot.set_layout("xml")
-  slot.put("<initiative_list>\n")
+  slot.set_layout("xml", "application/xml")
+  slot.put("<", relation_name, "_list>\n")
   for i_row, row in ipairs(rows) do
-    slot.put("  <initiative>\n")
+    slot.put("  <", relation_name, ">\n")
     for i_field, field in ipairs(fields) do
       slot.put("    <", field.name, ">")
       local value
       end
       slot.put("</", field.name, ">\n")
     end
-    slot.put("  </initiative>\n")
+    slot.put("  </", relation_name, ">\n")
   end
-  slot.put("</initiative_list>\n")
+  slot.put("</", relation_name, "_list>\n")
 end
 
 function util.autoapi_json(args)
-  slot.set_layout("blank")
+  slot.set_layout("blank", "application/json")
   local selector = assert(args.selector)
   local fields = assert(args.fields)
   local rows = selector:exec()
-  slot.put("{\n")
+  slot.put("[\n")
   for i_row, row in ipairs(rows) do
     slot.put("  {\n")
     for i_field, field in ipairs(fields) do
-      slot.put("    ", field.name, ": ")
+      slot.put("    \"", field.name, "\": ")
       local value
       if field.func then
         value = field.func(row)
       elseif field.field then
         value = row[field.name]
       end
-        slot.put(encode.json(value))
-      slot.put(",\n")
+      slot.put(encode.json(value))
+      if i_field < #fields then
+        slot.put(",")
+      end
+      slot.put("\n")
     end
-    slot.put("  },\n")
+    slot.put("  }")
+    if i_row < #rows then
+      slot.put(",")
+    end
+    slot.put("\n")
   end
-  slot.put("}\n")
+  slot.put("]\n")
 end
 
 function util.autoapi(args)
+  local relation_name = assert(args.relation_name)
   local selector = assert(args.selector)
   local fields = assert(args.fields)
   local api_engine = assert(args.api_engine)
 
   if api_engine == "xml" then
     util.autoapi_xml{
+      relation_name = relation_name,
       selector = selector,
       fields = fields
     }

File locale/help/member.settings.email_address.de.txt

+Diese E-Mail-Adresse kann für automatische Nachrichten des Systems sowie für die Kontaktaufnahme durch die Administratoren verwendet werden und wird anderen Benutzern nicht angezeigt. Wenn Du die Adresse änderst, erhälts Du eine E-Mail mit einem Bestätigungslink, den du anklicken musst.

File locale/motd/de.txt

Empty file removed.

File locale/motd/de_public.txt

Empty file removed.

File locale/translations.de.lua

 ["Change API key"] = "API-Schlüssel ändern";
 ["Change area delegation"] = "Delegation für Themenbereich ändern";
 ["Change display settings"] = "Anzeige-Einstellungen ändern";
+["Change email"] = "E-Mail-Adresse ändern";
+["Change email address"] = "E-Mail-Adresse ändern";
 ["Change filters and order"] = "Filter und Sortierung ändern";
 ["Change global delegation"] = "Globale Delegation ändern";
 ["Change issue delegation"] = "Delegation für Thema ändern";
 ["Change vote"] = "Abstimmung ändern";
 ["Change your login"] = "Deinen Anmeldenamen ändern";
 ["Change your name"] = "Deinen Namen ändern";
+["Change your notification email address"] = "Deine E-Mail-Adresse für Benachrichtigungen ändern";
 ["Change your password"] = "Dein Kennwort ändern";
 ["Choose initiator"] = "Initiator auswählen";
 ["Choose member"] = "Mitglied auswählen";
 ["Confirm"] = "Bestätigen";
 ["Confirmation code"] = "Bestätigungscode";
 ["Confirmation code invalid!"] = "Bestätigungscode ist ungültig!";
+["Confirmed address"] = "Bestätigte E-Mail";
 ["Contacts"] = "Kontakte";
 ["Content"] = "Inhalt";
 ["Counting of votes"] = "Auszählung";
 ["Current votings in areas you are member of and issues you are interested in:"] = "Jetzt laufende Abstimmungen zu Themen aus Deinen Themenbereichen oder solchen an denen Du interessiert bist:";
 ["Currently no API key is set."] = "Zur Zeit ist kein API-Schlüssel festgelegt.";
 ["Date"] = "Datum";
+["Date format is not valid. Please use following format: YYYY-MM-DD"] = "Datumsformat nicht korrekt. Bitte verwende: JJJJ-MM-TT, also z.B. 1945-05-23";
 ["Degree"] = "Grad";
 ["Delegations"] = "Delegationen";
 ["Delete API key"] = "API-Schlüssel löschen";
 ["Delete filter"] = "Filter löschen";
 ["Description"] = "Beschreibung";
 ["Details"] = "Details";
-["Developer features"] = "Entwicklerfunktionen";
+["Developer settings"] = "Einstellungen für Entwickler";
 ["Diff"] = "Differenz";
 ["Direct member count"] = "Anzahl Direktmitglieder";
 ["Direct membership"] = "Direkte Mitgliedschaft";
 ["Email address is confirmed now"] = "E-Mail-Adresse ist jetzt bestätigt";
 ["Email address too short!"] = "E-Mail-Adresse ist zu kurz!";
 ["Email confirmation request"] = "Bestätigung Deiner E-Mail-Adresse";
+["Email unconfirmed"] = "Unbestätigte E-Mail-Adresse";
 ["Empty help text: #{id}.#{lang}.txt"] = "Leerer Hilfe-Text: #{id}.#{lang}.txt";
+["Error while converting image. Please note, that only JPG files are supported!"] = "Fehler beim Konvertieren des Bilds. Bitte beachte, dass nur JPG-Dateien unterstützt werden.";
 ["Error while resolving openid. Internal message: '#{errmsg}'"] = "Fehler beim Auflösen der OpenID. Interne Fehlermeldung: '#{errmsg}'";
 ["Error while updating member, database reported:<br /><br /> (#{errormessage})"] = "Fehler beim aktualisieren des Mitglieds, die Datenbank berichtet folgenden Fehler:<br /><br /> (#{errormessage})";
 ["External memberships"] = "Externe Mitgliedschaften";
 ["In discussion"] = "In Diskussion";
 ["Incoming delegations"] = "Eingehende Delegationen";
 ["Information about the available policies"] = "Informationen zu den verfügbaren Regelwerken";
-["Initiated"] = "Initiert";
-["Initiated initiatives"] = "Initierte Initiativen";
+["Initiated"] = "Initiiert";
+["Initiated initiatives"] = "Initiierte Initiativen";
 ["Initiative events"] = "Initiativen-Ereignisse";
 ["Initiative is revoked now"] = "Initiative ist jetzt zurückgezogen";
 ["Initiative quorum"] = "Quorum Inititive";
 ["Issue canceled"] = "Thema abgebrochen";
 ["Issue delegation"] = "Issue-Delegation";
 ["Issue delegation active"] = "Delegation für Thema aktiv";
-["Issue events"] = "Themen-Ergeignisse";
+["Issue events"] = "Themen-Ereignisse";
 ["Issue finished"] = "Thema abgeschlossen";
 ["Issue finished without voting"] = "Thema ohne Abstimmung abgeschlossen";
 ["Issue frozen"] = "Thema eingefroren";
 ["Member has not approved latest draft"] = "Mitglied hat den letzten Entwurf noch nicht angenommen";
 ["Member is administrator"] = "Mitglied ist Administrator";
 ["Member is already saved in your contacts!"] = "Mitglied ist schon in Deinen Kontakten!";
+["Member is deactivated"] = "Mitglied ist deaktiviert";
 ["Member is now invited to be initiator"] = "Mitglied ist jetzt als Initiator eingeladen";
 ["Member list"] = "Mitgliederliste";
 ["Member name"] = "Mitglied Name";
 ["My opinion"] = "Meine Meinung";
 ["Name"] = "Name";
 ["New"] = "Neu";
+["New address"] = "Neue E-Mail-Adresse";
 ["New draft"] = "Neuer Entwurf";
 ["New draft has been added to initiative"] = "Neuer Entwurf wurde der Initiative hinzugefügt";
 ["New draft revision"] = "Neue Revision des Entwurfs";
 ["Not voted (revoked from initiator)"] = "Nicht abgestimmt (durch Initiator zurückgezogen)";
 ["Not voted issues"] = "Nicht abgestimmt";
 ["Not yet voted"] = "Noch abzustimmen";
+["Notification address unconfirmed"] = "E-Mail-Adresse für Benachrichtigungen unbestätigt";
 ["Number of incoming delegations, follow link to see more details"] = "Anzahl eingehender Delegationen, Link folgen für mehr Details";
 ["Number of initiatives to preview"] = "Anzahl der Initiativen in der Vorschau";
 ["OK"] = "OK";
 ["Remove my support from this initiative"] = "Meine Unterstützung der Initiative entziehen";
 ["Repeat new password"] = "Neues Kennwort wiederholen";
 ["Request password reset link"] = "Link zum Rücksetzen des Kennworts anfordern";
+["Resend activation email to '#{email}'"] = "E-Mail mit Aktivierungslink erneut an '#{email}' senden";
 ["Reset code"] = "Rücksetzcode";
 ["Reset code is invalid!"] = "Rücksetzcode ist ungültig";
 ["Reset link has been send for this member"] = "Rücksetz-Link wurde versendet";
 ["The initiators suggest to support the following initiative:"] = "Die Initiatoren empfehlen folgende Initiative zu unterstützen:";
 ["There are no more alternative initiatives currently."] = "Es gibt zur Zeit keine weiteren alternative Initiative.";
 ["There were no more alternative initiatives."] = "Es gab keine weiteren alternativen Initiativen.";
+["This email address is too short!"] = "Diese E-Mail-Adresse ist zu kurz!";
 ["This identifier is not allowed for this instance."] = "Dieser Identifier ist für diese Instanz nicht zugelassen.";
 ["This initiative"] = "Diese Initiative";
 ["This initiative compared to alternative initiatives"] = "Diese Initiative im Vergleich zu alternativen Initiativen";
 ["This member has rejected to become initiator of this initiative"] = "Dieses Mitglied hat die Einladung, Initiator zu werden, abgelehnt";
 ["This member is already initiator of this initiative"] = "Dieses Mitglied ist bereits Initiator dieser Initiative";
 ["This member is already invited to become initiator of this initiative"] = "Dieses Mitglied ist bereits eingeladen Initiator dieser Initiative zu werden";
+["This member is deactivated."] = "Dieses Mitglied ist deaktiviert.";
 ["This member is participating, the rest of delegation chain is suspended while discussing"] = "Dieses Mitglied partizipiert, Rest der Delegationskette während der Diskussion ausgesetzt.";
 ["This name is already taken, please choose another one!"] = "Dieser Name ist bereits vergeben, bitte wähle einen anderen!";
 ["This name is really too short!"] = "Dieser Name ist wirklich zu kurz!";
 ["Trustee"] = "Bevollmächtigter";
 ["Tuesday"] = "Dienstag";
 ["Type of tabs"] = "Tabulatortyp";
+["Unconfirmed address"] = "Unbestätigte E-Mail";
 ["Unknown author"] = "Unbekannter Autor";
 ["Updated drafts"] = "Neue Entwürfe";
 ["Upload images"] = "Bilder hochladen";
 ["Voting request updated"] = "Abstimmungswunsch aktualisiert";
 ["Voting started"] = "Abstimmung begonnen";
 ["Voting time"] = "Zeit für die Abstimmung";
+["We couldn't deliver a confirmation mail to this address. Please check entered email address."] = "Wir konnten keine Bestätigungs-E-Mail versenden. Bitte überprüfe die E-Mail-Adresse.";
+["We have sent an email with activation link already in the last hour. Please try again later."] = "Wir haben bereits innerhalb der letzten Stunde eine E-Mail mit Bestätigungslink gesendet. Bitte versuche es später erneut.";
 ["Website"] = "Webseite";
 ["Wednesday"] = "Mittwoch";
 ["Wiki engine"] = "Wiki engine";
 ["You are now initiator of this initiative"] = "Du bist jetzt Initiator dieser Initiative";
 ["You are potential supporter of this initiative"] = "Du bist potentieller Unterstützer dieser Initiative";
 ["You are supporting this initiative"] = "Du unterstützt diese Initiative";
+["You can change the following settings:"] = "Du kannst die folgenden Einstellungen vornehmen:";
+["You can change your email address only once per hour. Please try again later."] = "Du kannst die E-Mail-Adresse nur einmal in der Stunde ändern, bitte versuche es später erneut.";
 ["You can't suggest the initiative you are revoking"] = "Du kannst nicht die Initiative empfehlen, die Du löschen möchtest";
+["You didn't confirmed your email address '#{email}' within 7 days."] = "Du hast die E-Mail-Adresse '#{email}' nicht innerhalb von 7 Tagen bestätigt.";
+["You didn't confirmed your email address '#{email}'. You have received an email with an activation link."] = "Du hast die E-Mail-Adresse '#{email}' nicht bestätigt. Du hast hierzu eine E-Mail mit einem Aktivierungslink erhalten.";
 ["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.";
 ["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 display settings have been updated"] = "Deine Anzeige-Einstellungen wurden aktualisiert";
+["Your email address has been changed, please check for confirmation email with activation link!"] = "Deine E-Mail-Adresse wurde geändert, du hast eine Bestätigungs-E-Mail mit Aktivierungslink erhalten.";
 ["Your global delegation has been deleted."] = "Deine globale Delegation wurde gelöscht";
 ["Your global delegation has been updated."] = "Deine globale Delegation wurde geändert";
 ["Your login has been changed to '#{login}'"] = "Dein Anmeldename wurde auf '#{login}' geändert";

File locale/translations.en.lua

 ["Change API key"] = false;
 ["Change area delegation"] = false;
 ["Change display settings"] = false;
+["Change email"] = false;
+["Change email address"] = false;
 ["Change filters and order"] = false;
 ["Change global delegation"] = false;
 ["Change issue delegation"] = false;
 ["Change vote"] = false;
 ["Change your login"] = false;
 ["Change your name"] = false;
+["Change your notification email address"] = false;
 ["Change your password"] = false;
 ["Choose initiator"] = false;
 ["Choose member"] = false;
 ["Confirm"] = false;
 ["Confirmation code"] = false;
 ["Confirmation code invalid!"] = false;
+["Confirmed address"] = false;
 ["Contacts"] = false;
 ["Content"] = false;
 ["Counting of votes"] = false;
 ["Current votings in areas you are member of and issues you are interested in:"] = false;
 ["Currently no API key is set."] = false;
 ["Date"] = false;
+["Date format is not valid. Please use following format: YYYY-MM-DD"] = false;
 ["Degree"] = false;
 ["Delegations"] = false;
 ["Delete API key"] = false;
 ["Delete filter"] = false;
 ["Description"] = false;
 ["Details"] = false;
-["Developer features"] = false;
+["Developer settings"] = false;
 ["Diff"] = false;
 ["Direct member count"] = false;
 ["Direct membership"] = false;
 ["Email address is confirmed now"] = false;
 ["Email address too short!"] = false;
 ["Email confirmation request"] = false;
+["Email unconfirmed"] = false;
 ["Empty help text: #{id}.#{lang}.txt"] = false;
+["Error while converting image. Please note, that only JPG files are supported!"] = false;
 ["Error while resolving openid. Internal message: '#{errmsg}'"] = false;
 ["Error while updating member, database reported:<br /><br /> (#{errormessage})"] = false;
 ["External memberships"] = false;
 ["Member has not approved latest draft"] = false;
 ["Member is administrator"] = false;
 ["Member is already saved in your contacts!"] = false;
+["Member is deactivated"] = false;
 ["Member is now invited to be initiator"] = false;
 ["Member list"] = false;
 ["Member name"] = false;
 ["My opinion"] = false;
 ["Name"] = false;
 ["New"] = false;
+["New address"] = false;
 ["New draft"] = false;
 ["New draft has been added to initiative"] = false;
 ["New draft revision"] = false;
 ["Not voted (revoked from initiator)"] = false;
 ["Not voted issues"] = false;
 ["Not yet voted"] = false;
+["Notification address unconfirmed"] = false;
 ["Number of incoming delegations, follow link to see more details"] = false;
 ["Number of initiatives to preview"] = false;
 ["OK"] = false;
 ["Remove my support from this initiative"] = false;
 ["Repeat new password"] = false;
 ["Request password reset link"] = false;
+["Resend activation email to '#{email}'"] = false;
 ["Reset code"] = false;
 ["Reset code is invalid!"] = false;
 ["Reset link has been send for this member"] = false;
 ["The initiators suggest to support the following initiative:"] = false;
 ["There are no more alternative initiatives currently."] = false;
 ["There were no more alternative initiatives."] = false;
+["This email address is too short!"] = false;
 ["This identifier is not allowed for this instance."] = false;
 ["This initiative"] = false;
 ["This initiative compared to alternative initiatives"] = false;
 ["This member has rejected to become initiator of this initiative"] = false;
 ["This member is already initiator of this initiative"] = false;
 ["This member is already invited to become initiator of this initiative"] = false;
+["This member is deactivated."] = false;
 ["This member is participating, the rest of delegation chain is suspended while discussing"] = false;
 ["This name is already taken, please choose another one!"] = false;
 ["This name is really too short!"] = false;
 ["Trustee"] = false;
 ["Tuesday"] = false;
 ["Type of tabs"] = false;
+["Unconfirmed address"] = false;
 ["Unknown author"] = false;
 ["Updated drafts"] = false;
 ["Upload images"] = false;
 ["Voting request updated"] = false;
 ["Voting started"] = false;
 ["Voting time"] = false;
+["We couldn't deliver a confirmation mail to this address. Please check entered email address."] = false;
+["We have sent an email with activation link already in the last hour. Please try again later."] = false;
 ["Website"] = false;
 ["Wednesday"] = false;
 ["Wiki engine"] = false;
 ["You are now initiator of this initiative"] = false;
 ["You are potential supporter of this initiative"] = false;
 ["You are supporting this initiative"] = false;
+["You can change the following settings:"] = false;
+["You can change your email address only once per hour. Please try again later."] = false;
 ["You can't suggest the initiative you are revoking"] = false;
+["You didn't confirmed your email address '#{email}' within 7 days."] = false;
+["You didn't confirmed your email address '#{email}'. You have received an email with an activation link."] = false;
 ["You didn't saved any member as contact yet."] = false;
 ["You have saved this member as contact"] = false;
 ["You have saved this member as contact."] = false;
 ["Your delegation for this issue has been deleted."] = false;
 ["Your delegation for this issue has been updated."] = false;
 ["Your display settings have been updated"] = false;
+["Your email address has been changed, please check for confirmation email with activation link!"] = false;
 ["Your global delegation has been deleted."] = false;
 ["Your global delegation has been updated."] = false;
 ["Your login has been changed to '#{login}'"] = false;

File locale/translations.eo.lua

 ["Change API key"] = "Ŝanĝi API-ŝlosilon";
 ["Change area delegation"] = "Ŝanĝi delegacion por temaro";
 ["Change display settings"] = "Ŝanĝi afiŝajn agordojn";
+["Change email"] = false;
+["Change email address"] = false;
 ["Change filters and order"] = "Ŝanĝi filtrojn kaj ordon";
 ["Change global delegation"] = "Ŝanĝi ĝeneralan delegacion";
 ["Change issue delegation"] = "Ŝanĝi delegacion por la temo";
 ["Change vote"] = "Sangi baloton";
 ["Change your login"] = "Ŝanĝi vian salutnomon";
 ["Change your name"] = "Ŝanĝi vian nomon";
+["Change your notification email address"] = false;
 ["Change your password"] = "Ŝanĝi vian pasvorton";
 ["Choose initiator"] = "Elekti inicianton";
 ["Choose member"] = "Elekti membron";
 ["Confirm"] = "Konfirmi";
 ["Confirmation code"] = "Konfirmokodon";
 ["Confirmation code invalid!"] = "La konfirmokodo estas malvalida!";
+["Confirmed address"] = false;
 ["Contacts"] = "Kontaktoj";
 ["Content"] = "Enhavo";
 ["Counting of votes"] = "Nombrado de voĉoj";
 ["Current votings in areas you are member of and issues you are interested in:"] = "Aktualaj voĉdonoj pri temoj en kiuj vi membras kaj kiuj vin interesas:";
 ["Currently no API key is set."] = "Nuntempe ne ekzistas API-ŝlosilo";
 ["Date"] = "Dato";
+["Date format is not valid. Please use following format: YYYY-MM-DD"] = false;
 ["Degree"] = "Grado";
 ["Delegations"] = "Delegacioj";
 ["Delete API key"] = "Forviŝi API-ŝlosilon";
 ["Delete filter"] = "Forviŝi filtron";
 ["Description"] = "Priskribo";
 ["Details"] = "Detaloj";
-["Developer features"] = "Porprogramistaj funkcioj";
+["Developer settings"] = false;
 ["Diff"] = "Diferenco";
 ["Direct member count"] = "Nombro de la rektaj membroj";
 ["Direct membership"] = "Rekta membreco";
 ["Email address is confirmed now"] = "Retadreso nun estas konfirmita";
 ["Email address too short!"] = "Retadreso estas tro mallonga!";
 ["Email confirmation request"] = "Konfirmopeto de via retadreso";
+["Email unconfirmed"] = false;
 ["Empty help text: #{id}.#{lang}.txt"] = "Malplena helpoteksto: #{id}.#{lang}.txt";
+["Error while converting image. Please note, that only JPG files are supported!"] = false;
 ["Error while resolving openid. Internal message: '#{errmsg}'"] = "Eraro dum trovado de openid. Interna mesaĝo: '#{errmsg}'";
 ["Error while updating member, database reported:<br /><br /> (#{errormessage})"] = "Eraro dum ĝisdatigo de la membro, la datumbazo raportas sekvan eraron:<br /><br /> (#{errormessage})";
 ["External memberships"] = "Eksteraj membrecoj";
 ["Member has not approved latest draft"] = "La membro ne konsentis la plej novan skizon";
 ["Member is administrator"] = "La membro estas administranto";
 ["Member is already saved in your contacts!"] = "La membro estas jam konservita en viaj kontaktoj!";
+["Member is deactivated"] = false;
 ["Member is now invited to be initiator"] = "La membro nun estas invitita kiel inicionto";
 ["Member list"] = "Membrolisto";
 ["Member name"] = "Membronomo";
 ["My opinion"] = "Mia opinio";
 ["Name"] = "Nomo";
 ["New"] = "Nova";
+["New address"] = false;
 ["New draft"] = "Nova skizo";
 ["New draft has been added to initiative"] = "La nova skizo estas aldonita al la iniciato";
 ["New draft revision"] = "Nova revizio de la skizo";
 ["Not voted (revoked from initiator)"] = "Ne balotinta (retirita de la iniciinto)";
 ["Not voted issues"] = "Ne balotitaj";
 ["Not yet voted"] = "Ankoraŭ ne balotinta";
+["Notification address unconfirmed"] = false;
 ["Number of incoming delegations, follow link to see more details"] = "Nombro de alvenantaj delegacioj, sekvu ligilon por pli da detaloj";
 ["Number of initiatives to preview"] = "Nombro de iniciatoj por antaŭmontri";
 ["OK"] = "Bone";
 ["Remove my support from this initiative"] = "Forigi mian subtenon de la iniciato";
 ["Repeat new password"] = "Ripeti novan pasvorton";
 ["Request password reset link"] = "Demandi ligilon por remeti la pasvorton";
+["Resend activation email to '#{email}'"] = false;
 ["Reset code"] = "Remetokodo";
 ["Reset code is invalid!"] = "La remetokodo estas malvalida!";
 ["Reset link has been send for this member"] = "Remetoligilo estas dissendita";
 ["The initiators suggest to support the following initiative:"] = "La iniciintoj rekomendas subteni sekvan iniciaton:";
 ["There are no more alternative initiatives currently."] = "Aktuale ne ekzistas pliajn alternativajn iniciatojn.";
 ["There were no more alternative initiatives."] = "Ne ekzistis pliajn alternativajn iniciatojn.";
+["This email address is too short!"] = false;
 ["This identifier is not allowed for this instance."] = "Tiu identigilo ne estas permesita por ĉi tiu instanco.";
 ["This initiative"] = "Tiu iniciato";
 ["This initiative compared to alternative initiatives"] = "Tiu iniciato komparata al la alternativajn iniciatojn";
 ["This member has rejected to become initiator of this initiative"] = "Tiu membro rifuzis inviton por esti inicionto";
 ["This member is already initiator of this initiative"] = "Tiu membro estas jam iniciinto de tiu iniciato";
 ["This member is already invited to become initiator of this initiative"] = "Tiu membro estis jam invitita por esti inicionto de tiu iniciato";
+["This member is deactivated."] = false;
 ["This member is participating, the rest of delegation chain is suspended while discussing"] = "Tiu membro partoprenas, la resta delegaciĉeno estas blokita dum la diskuto";
 ["This name is already taken, please choose another one!"] = "Tiu nomo estas jam prenita, bonvolu elekti alian!";
 ["This name is really too short!"] = "Tiu nomo estas vere tro mallonga!";
 ["Trustee"] = "Fidulo mastrumanta";
 ["Tuesday"] = "Mardo";
 ["Type of tabs"] = "Tipo de langetoj";
+["Unconfirmed address"] = false;
 ["Unknown author"] = "Aŭtoro nekonata";
 ["Updated drafts"] = "Skizoj ĝisdatigitaj";
 ["Upload images"] = "Alŝuti bildojn";
 ["Voting request updated"] = "Voĉdono-peto ĝisdatigita";
 ["Voting started"] = "Voĉdono komenciĝis";
 ["Voting time"] = "Tempo por la voĉdono";
+["We couldn't deliver a confirmation mail to this address. Please check entered email address."] = false;
+["We have sent an email with activation link already in the last hour. Please try again later."] = false;
 ["Website"] = "Retpaĝo";
 ["Wednesday"] = "Merkredo";
 ["Wiki engine"] = "Viki-modulo";
 ["You are already initator"] = "Vi estas jam iniciinto";
 ["You are already not supporting this initiative"] = "Vi ankoraŭ ne subtenas tiun iniciaton";
 ["You are already supporting the latest draft"] = "Vi jam subtenas la plej novan skizon";
-["You are currently not invited to any initiative."] = "Vi aktuale estas invitita al neniu iniciato."; 
+["You are currently not invited to any initiative."] = "Vi aktuale estas invitita al neniu iniciato.";
 ["You are currently not supporting this initiative. By adding suggestions to this initiative you will automatically become a potential supporter."] = "Vi aktuale ne subtenas tiun iniciaton. Se vi aldonas sugeston al tiu iniciato vi estos aŭtomate eventuala subtenonto!";
 ["You are iniator of this initiative"] = "Vi estas iniciinto de tiu iniciato";
 ["You are interested in this issue"] = "Vin interesas tiu temo";
 ["You are now initiator of this initiative"] = "Vi nun estas iniciinto de tiu iniciato";
 ["You are potential supporter of this initiative"] = "Vi estas eventuala subtenanto de tiu iniciato";
 ["You are supporting this initiative"] = "Vi subtenas tiun iniciaton";
+["You can change the following settings:"] = false;
+["You can change your email address only once per hour. Please try again later."] = false;
 ["You can't suggest the initiative you are revoking"] = "Vi ne povas rekomendi la iniciaton, kiun vi nuligas";
+["You didn't confirmed your email address '#{email}' within 7 days."] = false;
+["You didn't confirmed your email address '#{email}'. You have received an email with an activation link."] = false;
 ["You didn't saved any member as contact yet."] = "Vi ankoraŭ ne konservis membron kiel kontakton!";
 ["You have saved this member as contact"] = "Vi konservis membron kiel kontakton";
 ["You have saved this member as contact."] = "Vi konservis membron kiel kontakton.";
 ["Your delegation for this issue has been deleted."] = "Via delegacio por tiu temo estas viŝita";
 ["Your delegation for this issue has been updated."] = "Via delegacio por tiu temo estas ĝisdatigita";
 ["Your display settings have been updated"] = "Viaj afiŝaj-agordoj estas ĝisdatigitaj";
+["Your email address has been changed, please check for confirmation email with activation link!"] = false;
 ["Your global delegation has been deleted."] = "Via ĝenerala delegacio estas viŝita";
 ["Your global delegation has been updated."] = "Via ĝenerala delegacio estas ĝisdatigita";
 ["Your login has been changed to '#{login}'"] = "Via salutnomo estas ĝisdatigita al '#{login}'";

File locale/translations.fr.lua

 ["Change API key"] = false;
 ["Change area delegation"] = false;
 ["Change display settings"] = false;
+["Change email"] = false;
+["Change email address"] = false;
 ["Change filters and order"] = false;
 ["Change global delegation"] = false;
 ["Change issue delegation"] = false;
 ["Change vote"] = false;
 ["Change your login"] = false;
 ["Change your name"] = false;
+["Change your notification email address"] = false;
 ["Change your password"] = false;
 ["Choose initiator"] = false;
 ["Choose member"] = false;
 ["Confirm"] = false;
 ["Confirmation code"] = false;
 ["Confirmation code invalid!"] = false;
+["Confirmed address"] = false;
 ["Contacts"] = false;
 ["Content"] = false;
 ["Counting of votes"] = false;
 ["Current votings in areas you are member of and issues you are interested in:"] = false;
 ["Currently no API key is set."] = false;
 ["Date"] = false;
+["Date format is not valid. Please use following format: YYYY-MM-DD"] = false;
 ["Degree"] = false;
 ["Delegations"] = false;
 ["Delete API key"] = false;
 ["Delete filter"] = false;
 ["Description"] = false;
 ["Details"] = false;
-["Developer features"] = false;
+["Developer settings"] = false;
 ["Diff"] = false;
 ["Direct member count"] = false;
 ["Direct membership"] = false;
 ["Email address is confirmed now"] = false;
 ["Email address too short!"] = false;
 ["Email confirmation request"] = false;
+["Email unconfirmed"] = false;
 ["Empty help text: #{id}.#{lang}.txt"] = false;
+["Error while converting image. Please note, that only JPG files are supported!"] = false;
 ["Error while resolving openid. Internal message: '#{errmsg}'"] = false;
 ["Error while updating member, database reported:<br /><br /> (#{errormessage})"] = false;
 ["External memberships"] = false;
 ["Member has not approved latest draft"] = false;
 ["Member is administrator"] = false;
 ["Member is already saved in your contacts!"] = false;
+["Member is deactivated"] = false;
 ["Member is now invited to be initiator"] = false;
 ["Member list"] = false;
 ["Member name"] = false;
 ["My opinion"] = false;
 ["Name"] = false;
 ["New"] = false;
+["New address"] = false;
 ["New draft"] = false;
 ["New draft has been added to initiative"] = false;
 ["New draft revision"] = false;
 ["Not voted (revoked from initiator)"] = false;
 ["Not voted issues"] = false;
 ["Not yet voted"] = false;
+["Notification address unconfirmed"] = false;
 ["Number of incoming delegations, follow link to see more details"] = false;
 ["Number of initiatives to preview"] = false;
 ["OK"] = false;
 ["Remove my support from this initiative"] = false;
 ["Repeat new password"] = false;
 ["Request password reset link"] = false;
+["Resend activation email to '#{email}'"] = false;
 ["Reset code"] = false;
 ["Reset code is invalid!"] = false;
 ["Reset link has been send for this member"] = false;
 ["The initiators suggest to support the following initiative:"] = false;
 ["There are no more alternative initiatives currently."] = false;
 ["There were no more alternative initiatives."] = false;
+["This email address is too short!"] = false;
 ["This identifier is not allowed for this instance."] = false;
 ["This initiative"] = false;
 ["This initiative compared to alternative initiatives"] = false;
 ["This member has rejected to become initiator of this initiative"] = false;
 ["This member is already initiator of this initiative"] = false;
 ["This member is already invited to become initiator of this initiative"] = false;
+["This member is deactivated."] = false;
 ["This member is participating, the rest of delegation chain is suspended while discussing"] = false;
 ["This name is already taken, please choose another one!"] = false;
 ["This name is really too short!"] = false;
 ["Trustee"] = false;
 ["Tuesday"] = false;
 ["Type of tabs"] = false;
+["Unconfirmed address"] = false;
 ["Unknown author"] = false;
 ["Updated drafts"] = false;
 ["Upload images"] = false;
 ["Voting request updated"] = false;
 ["Voting started"] = false;
 ["Voting time"] = false;
+["We couldn't deliver a confirmation mail to this address. Please check entered email address."] = false;
+["We have sent an email with activation link already in the last hour. Please try again later."] = false;
 ["Website"] = false;
 ["Wednesday"] = false;
 ["Wiki engine"] = false;
 ["You are now initiator of this initiative"] = false;
 ["You are potential supporter of this initiative"] = false;
 ["You are supporting this initiative"] = false;
+["You can change the following settings:"] = false;
+["You can change your email address only once per hour. Please try again later."] = false;
 ["You can't suggest the initiative you are revoking"] = false;
+["You didn't confirmed your email address '#{email}' within 7 days."] = false;
+["You didn't confirmed your email address '#{email}'. You have received an email with an activation link."] = false;
 ["You didn't saved any member as contact yet."] = false;
 ["You have saved this member as contact"] = false;
 ["You have saved this member as contact."] = false;
 ["Your delegation for this issue has been deleted."] = false;
 ["Your delegation for this issue has been updated."] = false;
 ["Your display settings have been updated"] = false;
+["Your email address has been changed, please check for confirmation email with activation link!"] = false;
 ["Your global delegation has been deleted."] = false;
 ["Your global delegation has been updated."] = false;
 ["Your login has been changed to '#{login}'"] = false;

File model/issue.lua

     :add_group_by('"issue"."vote_now"')
     :add_group_by('"issue"."vote_later"')
     :add_group_by('"issue"."voter_count"')
+    :add_group_by('"issue"."admission_time"')
+    :add_group_by('"issue"."discussion_time"')
+    :add_group_by('"issue"."verification_time"')
+    :add_group_by('"issue"."voting_time"')
     :add_group_by('"_interest"."member_id"')
     --:set_distinct()
 end
 
-function Issue:modify_selector_for_state(state)
+function Issue:modify_selector_for_state(initiatives_selector, state)
   if state == "new" then
-    initiatives_selector:add_where("issue.accepted ISNULL AND issue.cancelled ISNULL")
+    initiatives_selector:add_where("issue.accepted ISNULL AND issue.closed ISNULL")
   elseif state == "accepted" then
-    initiatives_selector:add_where("issue.accepted NOTNULL AND issue.half_frozen ISNULL AND issue.cancelled ISNULL")
+    initiatives_selector:add_where("issue.accepted NOTNULL AND issue.half_frozen ISNULL AND issue.closed ISNULL")
   elseif state == "frozen" then
-    initiatives_selector:add_where("issue.half_frozen NOTNULL AND issue.fully_frozen ISNULL AND cancelled ISNULL")
+    initiatives_selector:add_where("issue.half_frozen NOTNULL AND issue.fully_frozen ISNULL AND issue.closed ISNULL")
   elseif state == "voting" then
-    initiatives_selector:add_where("issue.fully_frozen NOTNULL AND issue.finished ISNULL AND issue.cancelled ISNULL")
+    initiatives_selector:add_where("issue.fully_frozen NOTNULL AND issue.closed ISNULL")
   elseif state == "finished" then
-    initiatives_selector:add_where("issue.finished NOTNULL")
+    initiatives_selector:add_where("issue.fully_frozen NOTNULL AND issue.closed NOTNULL")
   elseif state == "cancelled" then
-    initiatives_selector:add_where("issue.cancelled NOTNULL")
+    initiatives_selector:add_where("issue.fully_frozen ISNULL AND issue.closed NOTNULL")
+  else
+    error("Invalid state")
   end
 end
 

File model/member.lua

     content_type  = "text/plain; charset=UTF-8",
     content       = content
   }
+  if success then
+    local lock_expiry = db:query("SELECT now() + '1 hour'::interval AS lock_expiry", "object").lock_expiry
+    self.notify_email_lock_expiry = lock_expiry
+  end
+  self:save()
   return success
 end
 
 function Member.object:set_setting_map(key, subkey, value)
   
 end
+
+function Member.object_get:notify_email_locked()
+  return(
+    Member:new_selector()
+      :add_where{ "id = ?", app.session.member.id }
+      :add_where("notify_email_lock_expiry > now()")
+      :count() == 1
+  )
+end

File static/style.css

   margin-top: 0;
 }
 
+.menu_list li {
+  padding-top: 1ex;
+  padding-bottom: 1ex;
+}
+
+.deactivated_member_info {
+  background-color: #a00;
+  color: #fff;
+}
+
 /*************************************************************************
  * Voting
  */