]> WPIA git - motion.git/blobdiff - motion.py
report 500-error better, remove raw_html postprocessor
[motion.git] / motion.py
index 80ac5a36029eb728e0bf7031fb0404caf0397c8e..23d91f59ca1074e7e361f76dd8ec1dc763e5ca1a 100644 (file)
--- a/motion.py
+++ b/motion.py
@@ -3,11 +3,15 @@ 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
 
 def get_db():
     db = getattr(g, '_database', None)
 
 def get_db():
     db = getattr(g, '_database', None)
@@ -18,10 +22,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 +40,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
@@ -53,6 +62,26 @@ prefix = ConfigProxy("GROUP_PREFIX")
 times = ConfigProxy("DURATION")
 debuguser = ConfigProxy("DEBUGUSER")
 
 times = ConfigProxy("DURATION")
 debuguser = ConfigProxy("DEBUGUSER")
 
+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
@@ -75,11 +104,11 @@ 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():
@@ -88,6 +117,18 @@ def lookup_user():
             db.prepare("INSERT INTO voter(\"email\") VALUES($1)")(user)
             rv = db.prepare("SELECT id FROM voter WHERE email=$1")(user)
         g.voter = rv[0].get("id");
             db.prepare("INSERT INTO voter(\"email\") VALUES($1)")(user)
             rv = db.prepare("SELECT id FROM voter WHERE email=$1")(user)
         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)
+        if len(rv) != 0:
+            g.proxies_given = rv[0].get("email")
+        rv = db.prepare("SELECT email, voter_id FROM voter, proxy WHERE proxy.voter_id = voter.id AND proxy.revoked IS NULL AND proxy.proxy_id = $1 ")(g.voter)
+        if len(rv) != 0:
+            sep = ""
+            g.proxies_received = ""
+            for x in range(0, len(rv)):
+                g.proxies_received += sep + rv[x].get("email")
+                sep =", "
+
     g.user = user
     g.roles = {}
 
     g.user = user
     g.roles = {}
 
@@ -101,6 +142,7 @@ def lookup_user():
                 g.roles[a[0]] = [group for group in prefix.per_host]
             else:
                 g.roles[a[0]].append(val)
                 g.roles[a[0]] = [group for group in prefix.per_host]
             else:
                 g.roles[a[0]].append(val)
+
     return None
 
 @app.context_processor
     return None
 
 @app.context_processor
@@ -126,6 +168,17 @@ def get_allowed_cats(action):
 def may(action, motion):
     return motion in get_allowed_cats(action)
 
 def may(action, motion):
     return motion in get_allowed_cats(action)
 
+def may_admin(action):
+    return action in g.roles
+
+def get_voters():
+    rv = get_db().prepare("SELECT email FROM voter")
+    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")
+    return rv
+
 @app.teardown_appcontext
 def close_connection(exception):
     db = getattr(g, '_database', None)
 @app.teardown_appcontext
 def close_connection(exception):
     db = getattr(g, '_database', None)
@@ -175,8 +228,19 @@ def init_db():
                 db.prepare("ALTER TABLE \"motion\" ALTER COLUMN \"host\" SET NOT NULL")()
                 db.prepare("UPDATE \"schema_version\" SET \"version\"=3")()
 
                 db.prepare("ALTER TABLE \"motion\" ALTER COLUMN \"host\" SET NOT NULL")()
                 db.prepare("UPDATE \"schema_version\" SET \"version\"=3")()
 
+        if ver < 4:
+            with app.open_resource('sql/from_3.sql', mode='r') as f:
+                db.execute(f.read())
+                db.prepare("UPDATE \"schema_version\" SET \"version\"=4")()
+
+        if ver < 5:
+            with app.open_resource('sql/from_4.sql', mode='r') as f:
+                db.execute(f.read())
+                db.prepare("UPDATE \"schema_version\" SET \"version\"=5")()
+
 init_db()
 
 init_db()
 
+
 @app.route("/")
 def main():
     start=int(request.args.get("start", "-1"));
 @app.route("/")
 def main():
     start=int(request.args.get("start", "-1"));
@@ -200,7 +264,7 @@ 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)
+                           categories=get_allowed_cats("create"), singlemotion=False, may_proxyadmin=may_admin("proxyadmin"), languages=get_languages())
 
 def rel_redirect(loc):
     r = redirect(loc)
 
 def rel_redirect(loc):
     r = redirect(loc)
@@ -211,18 +275,18 @@ def rel_redirect(loc):
 def put_motion():
     cat=request.form.get("category", "")
     if cat not in get_allowed_cats("create"):
 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
 
     db = get_db()
     with db.xact():
 
     db = get_db()
     with db.xact():
@@ -233,13 +297,16 @@ 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("/")
 
 def motion_edited(motion):
         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("/")
 
 def motion_edited(motion):
-    return rel_redirect("/?start=" + str(motion) + "#motion-" + str(motion))
+    return rel_redirect("/motion/" + motion)
 
 def validate_motion_access(privilege):
     def decorator(f):
 
 def validate_motion_access(privilege):
     def decorator(f):
@@ -248,32 +315,41 @@ 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 decorator
     
             return f(motion, id)
         decorated_function.__name__ = f.__name__
         return decorated_function
     return decorator
     
+def validate_motion_access_vote(privilege):
+    simple_decorator = validate_motion_access(privilege)
+    def decorator(f):
+        def decorated_function(motion, voter):
+            return simple_decorator(lambda motion, id : f(motion, voter, id))(motion)
+        decorated_function.__name__ = f.__name__
+        return decorated_function
+    return decorator
+
 @app.route("/motion/<string:motion>/cancel", methods=['POST'])
 @validate_motion_access('cancel')
 def cancel_motion(motion, id):
     if request.form.get("reason", "none") == "none":
 @app.route("/motion/<string:motion>/cancel", methods=['POST'])
 @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)
     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(id)
+    return motion_edited(motion)
 
 @app.route("/motion/<string:motion>/finish", methods=['POST'])
 @validate_motion_access('finish')
 def finish_motion(motion, id):
     rv = get_db().prepare("UPDATE motion SET deadline=CURRENT_TIMESTAMP WHERE identifier=$1 AND host=$2 AND canceled is NULL")(motion, request.host)
 
 @app.route("/motion/<string:motion>/finish", methods=['POST'])
 @validate_motion_access('finish')
 def finish_motion(motion, id):
     rv = get_db().prepare("UPDATE motion SET deadline=CURRENT_TIMESTAMP WHERE identifier=$1 AND host=$2 AND canceled is NULL")(motion, request.host)
-    return motion_edited(id)
+    return motion_edited(motion)
 
 @app.route("/motion/<string:motion>")
 def show_motion(motion):
 
 @app.route("/motion/<string:motion>")
 def show_motion(motion):
@@ -282,23 +358,107 @@ def show_motion(motion):
                          + "LEFT JOIN voter poser ON poser.id = motion.posed_by "\
                          + "LEFT JOIN voter canceler ON canceler.id = motion.canceled_by "
                          + "WHERE motion.identifier=$1 AND motion.host=$3")
                          + "LEFT JOIN voter poser ON poser.id = motion.posed_by "\
                          + "LEFT JOIN voter canceler ON canceler.id = motion.canceled_by "
                          + "WHERE motion.identifier=$1 AND motion.host=$3")
-    rv = p(motion, g.voter, request.host)
-    if len(rv) == 0:
-        return "Error, Not found", 404
-    votes = None
-    if may("audit", rv[0].get("type")) and not rv[0].get("running") and not rv[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")(rv[0].get("id"));
-    return render_template('single_motion.html', motion=rv[0], may_vote=may("vote", rv[0].get("type")), may_cancel=may("cancel", rv[0].get("type")), may_finish=may("finish", rv[0].get("type")), votes=votes, singlemotion=True)
+    resultmotion = p(motion, g.voter, request.host)
+    if len(resultmotion) == 0:
+        return _('Error, Not found'), 404
 
 
-@app.route("/motion/<string:motion>/vote", methods=['POST'])
-@validate_motion_access('vote')
-def vote(motion, id):
+    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 v.result, proxy.voter_id, voter.email, CASE WHEN proxy.proxy_id = v.proxy_id THEN NULL ELSE voter.email END AS owneremail FROM proxy LEFT JOIN "\
+                          + "(SELECT vote.voter_id, vote.result, vote.proxy_id FROM vote "\
+                          + "WHERE vote.motion_id=$1) AS v ON proxy.voter_id = v.voter_id "\
+                          + "LEFT JOIN voter ON proxy.voter_id = voter.id "\
+                          + "WHERE proxy.proxy_id=$2 AND proxy.revoked IS NULL")
+    resultproxyvote = p(resultmotion[0][0], g.voter)
+
+    votes = None
+    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, languages=get_languages())
+
+@app.route("/motion/<string:motion>/vote/<string:voter>", methods=['POST'])
+@validate_motion_access_vote('vote')
+def vote(motion, voter, id):
     v = request.form.get("vote", "abstain")
     v = request.form.get("vote", "abstain")
+    voterid=int(voter)
     db = get_db()
     db = get_db()
+
+    # test if voter is proxy
+    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
+
     p = db.prepare("SELECT * FROM vote WHERE motion_id = $1 AND voter_id = $2")
     p = db.prepare("SELECT * FROM vote WHERE motion_id = $1 AND voter_id = $2")
-    rv = p(id, g.voter)
+    rv = p(id, voterid)
     if len(rv) == 0:
     if len(rv) == 0:
-        db.prepare("INSERT INTO vote(motion_id, voter_id, result) VALUES($1,$2,$3)")(id, g.voter, v)
+        db.prepare("INSERT INTO vote(motion_id, voter_id, result, proxy_id) VALUES($1,$2,$3,$4)")(id, voterid, v, g.voter)
     else:
     else:
-        db.prepare("UPDATE vote SET result=$3, entered=CURRENT_TIMESTAMP WHERE motion_id=$1 AND voter_id = $2")(id, g.voter, v)
-    return motion_edited(id)
+        db.prepare("UPDATE vote SET result=$3, entered=CURRENT_TIMESTAMP, proxy_id=$4 WHERE motion_id=$1 AND voter_id = $2")(id, voterid, v, g.voter)
+    return motion_edited(motion)
+
+@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"), languages=get_languages())
+
+@app.route("/proxy/add", methods=['POST'])
+def add_proxy():
+    if not may_admin("proxyadmin"):
+        return _('Forbidden'), 403
+    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);
+    if len(rv) == 0:
+        return _('Error, voter not found.'), 400
+    voterid = rv[0].get("id")
+    rv = get_db().prepare("SELECT id FROM voter WHERE email=$1")(proxy);
+    if len(rv) == 0:
+        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:
+        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:
+        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)
+    return rel_redirect("/proxy")
+
+@app.route("/proxy/revoke", methods=['POST'])
+def revoke_proxy():
+    if not may_admin("proxyadmin"):
+        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))
+    return rel_redirect("/proxy")
+
+@app.route("/proxy/revokeall", methods=['POST'])
+def revoke_proxy_all():
+    if not may_admin("proxyadmin"):
+        return _('Forbidden'), 403
+    rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE revoked IS NULL")(g.voter)
+    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")
+def create_user(email):
+    db = get_db()
+    with db.xact():
+        rv = db.prepare("SELECT id FROM voter WHERE lower(email)=lower($1)")(email)
+        messagetext=_("User '%s' already exists.") % (email)
+        if len(rv) == 0:
+            db.prepare("INSERT INTO voter(\"email\") VALUES($1)")(email)
+            messagetext=_("User '%s' inserted.") % (email)
+    click.echo(messagetext)