]> WPIA git - motion.git/blobdiff - motion.py
Merge branch 'finish_move' into 'master'
[motion.git] / motion.py
index 9e70c991dcd1fbe4354e7b9845749ad419ac964c..0156ba3aa6a4b85498ac87e63ba6b4d552b7b1b0 100644 (file)
--- a/motion.py
+++ b/motion.py
@@ -3,11 +3,16 @@ from flask import Flask
 from flask import render_template, redirect
 from flask import request
 from functools import wraps
 from flask import render_template, redirect
 from flask import request
 from functools import wraps
+from flask_babel import Babel, gettext
 import postgresql
 import filters
 from flaskext.markdown import Markdown
 from markdown.extensions import Extension
 from datetime import date, time, datetime
 import postgresql
 import filters
 from flaskext.markdown import Markdown
 from markdown.extensions import Extension
 from datetime import date, time, datetime
+from flask_language import Language, current_language
+import gettext
+import click
+import re
 
 def get_db():
     db = getattr(g, '_database', None)
 
 def get_db():
     db = getattr(g, '_database', None)
@@ -18,10 +23,14 @@ def get_db():
 
 app = Flask(__name__)
 app.register_blueprint(filters.blueprint)
 
 app = Flask(__name__)
 app.register_blueprint(filters.blueprint)
+babel = Babel(app)
+lang = Language(app)
+gettext.install('motion')
 
 class EscapeHtml(Extension):
     def extendMarkdown(self, md, md_globals):
         del md.preprocessors['html_block']
 
 class EscapeHtml(Extension):
     def extendMarkdown(self, md, md_globals):
         del md.preprocessors['html_block']
+        del md.postprocessors['raw_html']
         del md.inlinePatterns['html']
 
 md = Markdown(app, extensions=[EscapeHtml()])
         del md.inlinePatterns['html']
 
 md = Markdown(app, extensions=[EscapeHtml()])
@@ -32,6 +41,7 @@ class default_settings(object):
     COPYRIGHTLINK="https://wpia.club"
     IMPRINTLINK="https://documents.wpia.club/imprint.html"
     DATAPROTECTIONLINK="https://documents.wpia.club/data_privacy_policy_html_pages_en.html"
     COPYRIGHTLINK="https://wpia.club"
     IMPRINTLINK="https://documents.wpia.club/imprint.html"
     DATAPROTECTIONLINK="https://documents.wpia.club/data_privacy_policy_html_pages_en.html"
+    MAX_PROXY=2
 
 
 # Load config
 
 
 # Load config
@@ -52,9 +62,28 @@ class ConfigProxy:
 prefix = ConfigProxy("GROUP_PREFIX")
 times = ConfigProxy("DURATION")
 debuguser = ConfigProxy("DEBUGUSER")
 prefix = ConfigProxy("GROUP_PREFIX")
 times = ConfigProxy("DURATION")
 debuguser = ConfigProxy("DEBUGUSER")
+motion_wait_minutes = ConfigProxy("MOTION_WAIT_MINUTES")
 
 max_proxy=app.config.get("MAX_PROXY")
 
 
 max_proxy=app.config.get("MAX_PROXY")
 
+@babel.localeselector
+def get_locale():
+    return str(current_language)
+
+@lang.allowed_languages
+def get_allowed_languages():
+    return app.config['LANGUAGES'].keys()
+
+@lang.default_language
+def get_default_language():
+    return 'en'
+
+def get_languages():
+    return app.config['LANGUAGES']
+
+# Manually add vote options to the translation strings. They are used as keys in loops.
+TRANSLATION_STRINGS={_('yes'), _('no'), _('abstain')}
+
 @app.before_request
 def lookup_user():
     global prefix
 @app.before_request
 def lookup_user():
     global prefix
@@ -77,18 +106,18 @@ def lookup_user():
         roles = env.get("ROLES")
 
     if user is None:
         roles = env.get("ROLES")
 
     if user is None:
-        return "Server misconfigured", 500
+        return _('Server misconfigured'), 500
     roles = roles.split(" ")
 
     if user == "<invalid>":
     roles = roles.split(" ")
 
     if user == "<invalid>":
-        return "Access denied", 403;
+        return _('Access denied'), 403;
 
     db = get_db()
     with db.xact():
 
     db = get_db()
     with db.xact():
-        rv = db.prepare("SELECT id FROM voter WHERE email=$1")(user)
+        rv = db.prepare("SELECT id FROM voter WHERE email=$1 AND host=$2")(user, request.host)
         if len(rv) == 0:
         if len(rv) == 0:
-            db.prepare("INSERT INTO voter(\"email\") VALUES($1)")(user)
-            rv = db.prepare("SELECT id FROM voter WHERE email=$1")(user)
+            db.prepare("INSERT INTO voter(\"email\", \"host\") VALUES($1, $2)")(user, request.host)
+            rv = db.prepare("SELECT id FROM voter WHERE email=$1 AND host=$2")(user, request.host)
         g.voter = rv[0].get("id");
         g.proxies_given = ""
         rv = db.prepare("SELECT email, voter_id FROM voter, proxy WHERE proxy.proxy_id = voter.id AND proxy.revoked IS NULL AND proxy.voter_id = $1 ")(g.voter)
         g.voter = rv[0].get("id");
         g.proxies_given = ""
         rv = db.prepare("SELECT email, voter_id FROM voter, proxy WHERE proxy.proxy_id = voter.id AND proxy.revoked IS NULL AND proxy.voter_id = $1 ")(g.voter)
@@ -129,7 +158,7 @@ def init_footer_variables():
         footer = dict( version_year=version_year, 
             copyright_link=app.config.get("COPYRIGHTLINK"),
             copyright_name=app.config.get("COPYRIGHTNAME"),
         footer = dict( version_year=version_year, 
             copyright_link=app.config.get("COPYRIGHTLINK"),
             copyright_name=app.config.get("COPYRIGHTNAME"),
-            imprint_link=app.config.get("DATAPROTECTIONLINK"),
+            imprint_link=app.config.get("IMPRINTLINK"),
             dataprotection_link=app.config.get("DATAPROTECTIONLINK")
         )
     )
             dataprotection_link=app.config.get("DATAPROTECTIONLINK")
         )
     )
@@ -145,11 +174,15 @@ def may_admin(action):
     return action in g.roles
 
 def get_voters():
     return action in g.roles
 
 def get_voters():
-    rv = get_db().prepare("SELECT email FROM voter")
+    rv = get_db().prepare("SELECT email FROM voter WHERE host=$1")(request.host)
     return rv
 
 def get_all_proxies():
     return rv
 
 def get_all_proxies():
-    rv = get_db().prepare("SELECT p.id as id, v1.email as voter_email, v1.id as voterid, v2.email as proxy_email, v2.id as proxyid FROM voter AS v1, voter AS v2, proxy AS p WHERE v2.id = p.proxy_id AND v1.id = p.voter_id AND p.revoked is NULL ORDER BY voter_email, proxy_email")
+    rv = get_db().prepare("SELECT p.id as id, v1.email as voter_email, v1.id as voterid, "\
+                             + "v2.email as proxy_email, v2.id as proxyid "\
+                             + "FROM voter AS v1, voter AS v2, proxy AS p "\
+                             + "WHERE v2.id = p.proxy_id AND v1.id = p.voter_id AND p.revoked is NULL "\
+                             + "AND v1.host=$1 AND v2.host=$1 ORDER BY voter_email, proxy_email")(request.host)
     return rv
 
 @app.teardown_appcontext
     return rv
 
 @app.teardown_appcontext
@@ -211,8 +244,42 @@ def init_db():
                 db.execute(f.read())
                 db.prepare("UPDATE \"schema_version\" SET \"version\"=5")()
 
                 db.execute(f.read())
                 db.prepare("UPDATE \"schema_version\" SET \"version\"=5")()
 
+        if ver < 6:
+            with app.open_resource('sql/from_5.sql', mode='r') as f:
+                db.execute(f.read())
+                rv=db.prepare("INSERT INTO voter (email, host) (SELECT vt.email, m.host FROM motion AS m, voter AS vt, vote as v "\
+                             + "WHERE (m.id=v.motion_id AND v.voter_id = vt.id) OR (m.id=v.motion_id AND v.proxy_id = vt.id) "\
+                             + "GROUP BY m.host, vt.email ORDER BY m.host, vt.email)")()
+                rv=db.prepare("UPDATE vote SET voter_id = "\
+                             + "(SELECT v_new.id FROM motion AS m, voter AS v_new, voter as v_old "\
+                             + "WHERE v_new.email = v_old.email AND v_old.id = vote.voter_id AND "\
+                             + "vote.motion_id = m.id AND m.host = v_new.host AND v_old.host is NULL)")()
+                rv=db.prepare("UPDATE vote SET proxy_id = "\
+                             + "(SELECT v_new.id FROM motion AS m, voter AS v_new, voter as v_old "\
+                             + "WHERE v_new.email = v_old.email AND v_old.id = vote.proxy_id AND "\
+                             + "vote.motion_id = m.id AND m.host = v_new.host AND v_old.host is NULL)")()
+                db.prepare("DELETE FROM voter WHERE host IS Null")()
+                db.prepare("ALTER TABLE \"voter\" ALTER COLUMN \"host\" SET NOT NULL")()
+                db.prepare("UPDATE \"schema_version\" SET \"version\"=6")()
+
+        if ver < 7:
+            with app.open_resource('sql/from_6.sql', mode='r') as f:
+                db.execute(f.read())
+                db.prepare("UPDATE \"schema_version\" SET \"version\"=7")()
+
 init_db()
 
 init_db()
 
+def is_in_ratelimit(group):
+    rv = get_db().prepare("SELECT EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - posed)) AS timedifference FROM motion WHERE type=$1 AND host=$2 ORDER BY posed DESC LIMIT 1")(group, request.host)
+    if len(rv) == 0:
+        return True
+    rate_limit = motion_wait_minutes.per_host
+    if rate_limit is None:
+        rate_limit = 0
+    if rv[0]['timedifference'] > rate_limit*60:
+        return True
+    else:
+        return _('Error, time between last motion to short. The current setting is %s minute(s).') % (str(rate_limit))
 
 @app.route("/")
 def main():
 
 @app.route("/")
 def main():
@@ -237,29 +304,38 @@ def main():
         else:
             prev = -1
     return render_template('index.html', motions=rv[:10], more=rv[10]["id"] if len(rv) == 11 else None, times=times.per_host, prev=prev,
         else:
             prev = -1
     return render_template('index.html', motions=rv[:10], more=rv[10]["id"] if len(rv) == 11 else None, times=times.per_host, prev=prev,
-                           categories=get_allowed_cats("create"), singlemotion=False, may_proxyadmin=may_admin("proxyadmin"))
+                           categories=get_allowed_cats("create"), singlemotion=False, may_proxyadmin=may_admin("proxyadmin"), languages=get_languages())
 
 def rel_redirect(loc):
     r = redirect(loc)
     r.autocorrect_location_header = False
     return r
 
 
 def rel_redirect(loc):
     r = redirect(loc)
     r.autocorrect_location_header = False
     return r
 
+def write_proxy_log(userid, action, comment):
+    get_db().prepare("INSERT INTO adminlog(user_id, action, comment, action_user_id) VALUES($1, $2, $3, $4)")(userid, action, comment, g.voter)
+
+def write_masking_log(comment):
+    get_db().prepare("INSERT INTO adminlog(user_id, action, comment, action_user_id) VALUES($1, 'motionmasking', $2, $1)")(0, comment)
+
 @app.route("/motion", methods=['POST'])
 def put_motion():
     cat=request.form.get("category", "")
     if cat not in get_allowed_cats("create"):
 @app.route("/motion", methods=['POST'])
 def put_motion():
     cat=request.form.get("category", "")
     if cat not in get_allowed_cats("create"):
-        return "Forbidden", 403
+        return _('Forbidden'), 403
     time = int(request.form.get("days", "3"));
     if time not in times.per_host:
     time = int(request.form.get("days", "3"));
     if time not in times.per_host:
-        return "Error, invalid length", 400
+        return _('Error, invalid length'), 400
     title=request.form.get("title", "")
     title=title.strip()
     if title =='':
     title=request.form.get("title", "")
     title=title.strip()
     if title =='':
-        return "Error, missing title", 400
+        return _('Error, missing title'), 400
     content=request.form.get("content", "")
     content=content.strip()
     if content =='':
     content=request.form.get("content", "")
     content=content.strip()
     if content =='':
-        return "Error, missing content", 400
+        return _('Error, missing content'), 400
+    ratelimit = is_in_ratelimit(cat)
+    if ratelimit is not True:
+        return ratelimit, 400
 
     db = get_db()
     with db.xact():
 
     db = get_db()
     with db.xact():
@@ -270,7 +346,10 @@ def put_motion():
         if len(sr) == 0 or sr[0][0] is None:
             ident=prefix.per_host[cat]+"."+t.strftime("%Y%m%d")+".001"
         else:
         if len(sr) == 0 or sr[0][0] is None:
             ident=prefix.per_host[cat]+"."+t.strftime("%Y%m%d")+".001"
         else:
-            ident=prefix.per_host[cat]+"."+t.strftime("%Y%m%d")+"."+("%03d" % (int(sr[0][0].split(".")[2])+1))
+            nextId = int(sr[0][0].split(".")[2])+1
+            if nextId >= 1000:
+                return _('Too many motions for this day'), 500
+            ident=prefix.per_host[cat]+"."+t.strftime("%Y%m%d")+"."+("%03d" % nextId)
         p = db.prepare("INSERT INTO motion(\"name\", \"content\", \"deadline\", \"posed_by\", \"type\", \"identifier\", \"host\") VALUES($1, $2, CURRENT_TIMESTAMP + $3 * interval '1 days', $4, $5, $6, $7)")
         p(title, content, time, g.voter, cat, ident, request.host)
     return rel_redirect("/")
         p = db.prepare("INSERT INTO motion(\"name\", \"content\", \"deadline\", \"posed_by\", \"type\", \"identifier\", \"host\") VALUES($1, $2, CURRENT_TIMESTAMP + $3 * interval '1 days', $4, $5, $6, $7)")
         p(title, content, time, g.voter, cat, ident, request.host)
     return rel_redirect("/")
@@ -285,14 +364,14 @@ def validate_motion_access(privilege):
             with db.xact():
                 rv = db.prepare("SELECT id, type, deadline < CURRENT_TIMESTAMP AS expired, canceled FROM motion WHERE identifier=$1 AND host=$2")(motion, request.host);
                 if len(rv) == 0:
             with db.xact():
                 rv = db.prepare("SELECT id, type, deadline < CURRENT_TIMESTAMP AS expired, canceled FROM motion WHERE identifier=$1 AND host=$2")(motion, request.host);
                 if len(rv) == 0:
-                    return "Error, Not found", 404
+                    return _('Error, Not found'), 404
                 id = rv[0].get("id")
                 if not may(privilege, rv[0].get("type")):
                 id = rv[0].get("id")
                 if not may(privilege, rv[0].get("type")):
-                    return "Forbidden", 403
+                    return _('Forbidden'), 403
                 if rv[0].get("canceled") is not None:
                 if rv[0].get("canceled") is not None:
-                    return "Error, motion was canceled", 403
+                    return _('Error, motion was canceled'), 403
                 if rv[0].get("expired"):
                 if rv[0].get("expired"):
-                    return "Error, out of time", 403
+                    return _('Error, out of time'), 403
             return f(motion, id)
         decorated_function.__name__ = f.__name__
         return decorated_function
             return f(motion, id)
         decorated_function.__name__ = f.__name__
         return decorated_function
@@ -311,7 +390,7 @@ def validate_motion_access_vote(privilege):
 @validate_motion_access('cancel')
 def cancel_motion(motion, id):
     if request.form.get("reason", "none") == "none":
 @validate_motion_access('cancel')
 def cancel_motion(motion, id):
     if request.form.get("reason", "none") == "none":
-        return "Error, form requires reason", 500
+        return _('Error, form requires reason'), 500
     rv = get_db().prepare("UPDATE motion SET canceled=CURRENT_TIMESTAMP, cancelation_reason=$1, canceled_by=$2 WHERE identifier=$3 AND host=$4 AND canceled is NULL")(request.form.get("reason", ""), g.voter, motion, request.host)
     return motion_edited(motion)
 
     rv = get_db().prepare("UPDATE motion SET canceled=CURRENT_TIMESTAMP, cancelation_reason=$1, canceled_by=$2 WHERE identifier=$3 AND host=$4 AND canceled is NULL")(request.form.get("reason", ""), g.voter, motion, request.host)
     return motion_edited(motion)
 
@@ -330,7 +409,7 @@ def show_motion(motion):
                          + "WHERE motion.identifier=$1 AND motion.host=$3")
     resultmotion = p(motion, g.voter, request.host)
     if len(resultmotion) == 0:
                          + "WHERE motion.identifier=$1 AND motion.host=$3")
     resultmotion = p(motion, g.voter, request.host)
     if len(resultmotion) == 0:
-        return "Error, Not found", 404
+        return _('Error, Not found'), 404
 
     p = get_db().prepare("SELECT voter.email FROM vote INNER JOIN voter ON vote.proxy_id = voter.id WHERE vote.motion_id=$1 AND vote.voter_id=$2 AND vote.proxy_id <> vote.voter_id")
     resultproxyname = p(resultmotion[0][0], g.voter)
 
     p = get_db().prepare("SELECT voter.email FROM vote INNER JOIN voter ON vote.proxy_id = voter.id WHERE vote.motion_id=$1 AND vote.voter_id=$2 AND vote.proxy_id <> vote.voter_id")
     resultproxyname = p(resultmotion[0][0], g.voter)
@@ -346,7 +425,7 @@ def show_motion(motion):
     if may("audit", resultmotion[0].get("type")) and not resultmotion[0].get("running") and not resultmotion[0].get("canceled"):
         votes = get_db().prepare("SELECT vote.result, voter.email FROM vote INNER JOIN voter ON voter.id = vote.voter_id WHERE vote.motion_id=$1")(resultmotion[0].get("id"));
         votes = get_db().prepare("SELECT vote.result, voter.email, CASE voter.email WHEN proxy.email THEN NULL ELSE proxy.email END as proxyemail FROM vote INNER JOIN voter ON voter.id = vote.voter_id INNER JOIN voter as proxy ON proxy.id = vote.proxy_id WHERE vote.motion_id=$1")(resultmotion[0].get("id"));
     if may("audit", resultmotion[0].get("type")) and not resultmotion[0].get("running") and not resultmotion[0].get("canceled"):
         votes = get_db().prepare("SELECT vote.result, voter.email FROM vote INNER JOIN voter ON voter.id = vote.voter_id WHERE vote.motion_id=$1")(resultmotion[0].get("id"));
         votes = get_db().prepare("SELECT vote.result, voter.email, CASE voter.email WHEN proxy.email THEN NULL ELSE proxy.email END as proxyemail FROM vote INNER JOIN voter ON voter.id = vote.voter_id INNER JOIN voter as proxy ON proxy.id = vote.proxy_id WHERE vote.motion_id=$1")(resultmotion[0].get("id"));
-    return render_template('single_motion.html', motion=resultmotion[0], may_vote=may("vote", resultmotion[0].get("type")), may_cancel=may("cancel", resultmotion[0].get("type")), votes=votes, proxyvote=resultproxyvote, proxyname=resultproxyname)
+    return render_template('single_motion.html', motion=resultmotion[0], may_vote=may("vote", resultmotion[0].get("type")), may_cancel=may("cancel", resultmotion[0].get("type")), may_finish=may("finish", resultmotion[0].get("type")), votes=votes, proxyvote=resultproxyvote, proxyname=resultproxyname, languages=get_languages())
 
 @app.route("/motion/<string:motion>/vote/<string:voter>", methods=['POST'])
 @validate_motion_access_vote('vote')
 
 @app.route("/motion/<string:motion>/vote/<string:voter>", methods=['POST'])
 @validate_motion_access_vote('vote')
@@ -359,7 +438,7 @@ def vote(motion, voter, id):
     if (voterid != g.voter):
         rv = db.prepare("SELECT voter_id FROM proxy WHERE proxy.revoked IS NULL AND proxy.proxy_id = $1 AND proxy.voter_id = $2")(g.voter, voterid);
         if len(rv) == 0:
     if (voterid != g.voter):
         rv = db.prepare("SELECT voter_id FROM proxy WHERE proxy.revoked IS NULL AND proxy.proxy_id = $1 AND proxy.voter_id = $2")(g.voter, voterid);
         if len(rv) == 0:
-            return "Error, proxy not found.", 400
+            return _('Error, proxy not found.'), 400
 
     p = db.prepare("SELECT * FROM vote WHERE motion_id = $1 AND voter_id = $2")
     rv = p(id, voterid)
 
     p = db.prepare("SELECT * FROM vote WHERE motion_id = $1 AND voter_id = $2")
     rv = p(id, voterid)
@@ -372,47 +451,89 @@ def vote(motion, voter, id):
 @app.route("/proxy")
 def proxy():
     if not may_admin("proxyadmin"):
 @app.route("/proxy")
 def proxy():
     if not may_admin("proxyadmin"):
-        return "Forbidden", 403
-    return render_template('proxy.html', voters=get_voters(), proxies=get_all_proxies(), may_proxyadmin=may_admin("proxyadmin"))
+        return _('Forbidden'), 403
+    return render_template('proxy.html', voters=get_voters(), proxies=get_all_proxies(), may_proxyadmin=may_admin("proxyadmin"), languages=get_languages())
 
 @app.route("/proxy/add", methods=['POST'])
 def add_proxy():
     if not may_admin("proxyadmin"):
 
 @app.route("/proxy/add", methods=['POST'])
 def add_proxy():
     if not may_admin("proxyadmin"):
-        return "Forbidden", 403
+        return _('Forbidden'), 403
     voter=request.form.get("voter", "")
     proxy=request.form.get("proxy", "")
     if voter == proxy :
     voter=request.form.get("voter", "")
     proxy=request.form.get("proxy", "")
     if voter == proxy :
-        return "Error, voter equals proxy.", 400
-    rv = get_db().prepare("SELECT id FROM voter WHERE email=$1")(voter);
+        return _('Error, voter equals proxy.'), 400
+    rv = get_db().prepare("SELECT id FROM voter WHERE email=$1 AND host=$2")(voter, request.host);
     if len(rv) == 0:
     if len(rv) == 0:
-        return "Error, voter not found.", 400
+        return _('Error, voter not found.'), 400
     voterid = rv[0].get("id")
     voterid = rv[0].get("id")
-    rv = get_db().prepare("SELECT id FROM voter WHERE email=$1")(proxy);
+    rv = get_db().prepare("SELECT id, host FROM voter WHERE email=$1 AND host=$2")(proxy, request.host);
     if len(rv) == 0:
     if len(rv) == 0:
-        return "Error, proxy not found.", 400
+        return _('Error, proxy not found.'), 400
     proxyid = rv[0].get("id")
     rv = get_db().prepare("SELECT id FROM proxy WHERE voter_id=$1 AND revoked is NULL")(voterid);
     if len(rv) != 0:
     proxyid = rv[0].get("id")
     rv = get_db().prepare("SELECT id FROM proxy WHERE voter_id=$1 AND revoked is NULL")(voterid);
     if len(rv) != 0:
-        return "Error, proxy allready given.", 400
+        return _('Error, proxy allready given.'), 400
     rv = get_db().prepare("SELECT COUNT(id) as c FROM proxy WHERE proxy_id=$1 AND revoked is NULL GROUP BY proxy_id")(proxyid);
     if len(rv) != 0:
     rv = get_db().prepare("SELECT COUNT(id) as c FROM proxy WHERE proxy_id=$1 AND revoked is NULL GROUP BY proxy_id")(proxyid);
     if len(rv) != 0:
-        if rv[0].get("c") >= max_proxy:
-            return "Error, Max proxy for '" + proxy + "' reached.", 400
+        if rv[0].get("c") is None or rv[0].get("c") >= max_proxy:
+            return _("Error, Max proxy for '%s' reached.") % (proxy), 400
     rv = get_db().prepare("INSERT INTO proxy(voter_id, proxy_id, granted_by) VALUES ($1,$2,$3)")(voterid, proxyid, g.voter)
     rv = get_db().prepare("INSERT INTO proxy(voter_id, proxy_id, granted_by) VALUES ($1,$2,$3)")(voterid, proxyid, g.voter)
+    write_proxy_log(voterid, 'proxygranted', 'proxy: '+str(proxyid))
     return rel_redirect("/proxy")
 
 @app.route("/proxy/revoke", methods=['POST'])
 def revoke_proxy():
     if not may_admin("proxyadmin"):
     return rel_redirect("/proxy")
 
 @app.route("/proxy/revoke", methods=['POST'])
 def revoke_proxy():
     if not may_admin("proxyadmin"):
-        return "Forbidden", 403
+        return _('Forbidden'), 403
     id=request.form.get("id", "")
     rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE id=$2")(g.voter, int(id))
     id=request.form.get("id", "")
     rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE id=$2")(g.voter, int(id))
+    write_proxy_log(int(id), 'proxyrevoked', '')
     return rel_redirect("/proxy")
 
 @app.route("/proxy/revokeall", methods=['POST'])
 def revoke_proxy_all():
     if not may_admin("proxyadmin"):
     return rel_redirect("/proxy")
 
 @app.route("/proxy/revokeall", methods=['POST'])
 def revoke_proxy_all():
     if not may_admin("proxyadmin"):
-        return "Forbidden", 403
+        return _('Forbidden'), 403
     rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE revoked IS NULL")(g.voter)
     rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE revoked IS NULL")(g.voter)
+    write_proxy_log(g.voter, 'proxyrevokedall', '')
     return rel_redirect("/proxy")
 
     return rel_redirect("/proxy")
 
+@app.route("/language/<string:language>")
+def set_language(language):
+    lang.change_language(language)
+    return rel_redirect("/")
+
+@app.cli.command("create-user")
+@click.argument("email")
+@click.argument("host")
+def create_user(email, host):
+    db = get_db()
+    with db.xact():
+        rv = db.prepare("SELECT id FROM voter WHERE lower(email)=lower($1) AND host=$2")(email, host)
+        messagetext=_("User '%s' already exists on %s.") % (email, host)
+        if len(rv) == 0:
+            db.prepare("INSERT INTO voter(\"email\", \"host\") VALUES($1, $2)")(email, host)
+            messagetext=_("User '%s' inserted to %s.") % (email, host)
+    click.echo(messagetext)
+
+@app.cli.command("motion-masking")
+@click.argument("motion")
+@click.argument("motionreason")
+@click.argument("host")
+def motion_masking(motion, motionreason, host):
+    if re.search(r"[%_\\]", motion):
+        messagetext = _("No wildcards allowed for motion entry '%s'.") % (motion)
+        click.echo(messagetext)
+    else:
+        db = get_db()
+        with db.xact():
+            rv = db.prepare("SELECT id FROM motion WHERE identifier LIKE $1 AND host = $2")(motion+"%", host)
+            count = len(rv)
+            messagetext = _("%s record(s) affected by masking of '%s'.") % (count, motion)
+            click.echo(messagetext)
+            if len(rv) != 0:
+                rv = db.prepare("SELECT id FROM motion WHERE content LIKE $1 AND host = $2")('%'+motionreason+"%", host)
+                rv = db.prepare("UPDATE motion SET name=$3, content=$4 WHERE identifier LIKE $1 AND host = $2 RETURNING id ")(motion+"%", host, _("Motion masked"), _("Motion masked on base of motion [%s](%s) on %s") % (motionreason, motionreason, datetime.now().strftime("%Y-%m-%d")))
+                messagetext = _("%s record(s) updated by masking of '%s'.") % (len(rv), motion)
+                write_masking_log(_("%s motion(s) masked on base of motion %s with motion identifier '%s' on host %s") %(len(rv), motionreason, motion, host))
+                click.echo(messagetext)