]> WPIA git - motion.git/commitdiff
Merge branch 'proxy-vote' into 'master'
authorFelix Dörre <felix@dogcraft.de>
Fri, 13 Nov 2020 16:47:48 +0000 (17:47 +0100)
committerFelix Dörre <felix@dogcraft.de>
Fri, 13 Nov 2020 16:47:48 +0000 (17:47 +0100)
Implement proxy vote

Closes #1

See merge request felixdoerre/motion!22

12 files changed:
README.md
config.py.example
motion.py
sql/from_3.sql [new file with mode: 0644]
sql/from_4.sql [new file with mode: 0644]
sql/sample_data.sql
sql/schema.sql
templates/base.html
templates/index.html
templates/proxy.html [new file with mode: 0644]
templates/single_motion.html
tests/test_motion.py

index e06c09e865173400a41156b66c1d2ea491c9761c..18df67fdae1672541b8e4705a7810fa2e90677bc 100644 (file)
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ The following user rights can be granted:
 - cancel: user is able to cancel a running motion
 - finish: user is able to close a running motion
 - audit: user is able to see given votes of a finished motion
+- proxyadmin: user is able to grant proxy rights for users
 
 To grant right use the following (here with vote right as example):
 - on all groups add "vote:*"
index 5bbdb4d9c77b133ac9a4975b156e4e9ee3746194..83cb5fb83a499aa7a98ca328062b4d291aa8aa88 100644 (file)
@@ -13,6 +13,8 @@ DEFAULT_HOST="hostname for migration from db version < 2"
 
 GROUP_PREFIX={'hostname': {'group1': 'g1', 'group2': 'g2'}} # motion types and their id-prefixes
 
+MAX_PROXY=2  # user is allowed to hold up to MAX_PROXY votes
+
 DURATION={'hostname':[3, 7, 14]} # duration period for motions
 
 #DEBUGUSER={'hostname':'username/create:* vote:*'} # remove # at beginning of line to use local debuguser
index 80ac5a36029eb728e0bf7031fb0404caf0397c8e..9e70c991dcd1fbe4354e7b9845749ad419ac964c 100644 (file)
--- a/motion.py
+++ b/motion.py
@@ -53,6 +53,8 @@ prefix = ConfigProxy("GROUP_PREFIX")
 times = ConfigProxy("DURATION")
 debuguser = ConfigProxy("DEBUGUSER")
 
+max_proxy=app.config.get("MAX_PROXY")
+
 @app.before_request
 def lookup_user():
     global prefix
@@ -88,6 +90,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");
+        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 = {}
 
@@ -101,6 +115,7 @@ def lookup_user():
                 g.roles[a[0]] = [group for group in prefix.per_host]
             else:
                 g.roles[a[0]].append(val)
+
     return None
 
 @app.context_processor
@@ -126,6 +141,17 @@ def 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)
@@ -175,8 +201,19 @@ def init_db():
                 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()
 
+
 @app.route("/")
 def main():
     start=int(request.args.get("start", "-1"));
@@ -200,7 +237,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,
-                           categories=get_allowed_cats("create"), singlemotion=False)
+                           categories=get_allowed_cats("create"), singlemotion=False, may_proxyadmin=may_admin("proxyadmin"))
 
 def rel_redirect(loc):
     r = redirect(loc)
@@ -239,7 +276,7 @@ def put_motion():
     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):
@@ -261,19 +298,28 @@ def validate_motion_access(privilege):
         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":
         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(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)
-    return motion_edited(id)
+    return motion_edited(motion)
 
 @app.route("/motion/<string:motion>")
 def show_motion(motion):
@@ -282,23 +328,91 @@ 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")
-    rv = p(motion, g.voter, request.host)
-    if len(rv) == 0:
+    resultmotion = p(motion, g.voter, request.host)
+    if len(resultmotion) == 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)
 
-@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)
+
+@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")
+    voterid=int(voter)
     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")
-    rv = p(id, g.voter)
+    rv = p(id, voterid)
     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:
-        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"))
+
+@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") >= max_proxy:
+            return "Error, Max proxy for '" + proxy + "' reached.", 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")
+
diff --git a/sql/from_3.sql b/sql/from_3.sql
new file mode 100644 (file)
index 0000000..7299f75
--- /dev/null
@@ -0,0 +1,11 @@
+DROP TABLE IF EXISTS proxy;
+CREATE TABLE proxy (id serial NOT NULL,
+                   voter_id INTEGER NOT NULL,
+                   proxy_id INTEGER NOT NULL,
+                   granted timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                   granted_by int NOT NULL,
+                   revoked timestamp NULL DEFAULT NULL,
+                   revoked_by int NULL DEFAULT NULL,
+                   PRIMARY KEY(id));
+CREATE INDEX proxy_voter ON proxy (voter_id);
+CREATE INDEX proxy_proxy ON proxy (proxy_id);
diff --git a/sql/from_4.sql b/sql/from_4.sql
new file mode 100644 (file)
index 0000000..cf1ce70
--- /dev/null
@@ -0,0 +1,3 @@
+ALTER TABLE "vote" ADD COLUMN "proxy_id" INTEGER;
+UPDATE "vote" SET "proxy_id" = "voter_id";
+ALTER TABLE "vote" ALTER COLUMN "proxy_id" SET NOT NULL;
index fd762be112631233ab930418e6cdee68f6b7e262..40532deefb45a49c8cba3e2a6b321949fdb6740e 100644 (file)
@@ -1,4 +1,4 @@
--- sample data for scheme version 3
+-- sample data for scheme version 4
 INSERT INTO voter (id,email) VALUES (1, 'User A');
 INSERT INTO voter (id,email) VALUES (2, 'User B');
 INSERT INTO voter (id,email) VALUES (3, 'User C');
@@ -15,10 +15,10 @@ INSERT INTO motion (id,identifier,name,type,host,content,posed,posed_by,deadline
     (4,'g1.20200402.004','Motion D','group1','127.0.0.1:5000','A fourth motion', current_timestamp ,1,current_timestamp + interval '1' day,Null,Null,Null);
 ALTER SEQUENCE motion_id_seq RESTART WITH 5;
 
-INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (1,1,'yes','2020-04-02 21:54:34.469784');
-INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (1,2,'yes','2020-04-02 21:54:34.469784');
-INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (1,3,'no','2020-04-02 21:54:34.469784');
-INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (2,1,'yes','2020-04-02 21:54:34.469784');
-INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (2,2,'no','2020-04-02 21:54:34.469784');
-INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (2,3,'no','2020-04-02 21:54:34.469784');
-INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (3,3,'yes','2020-04-02 21:48:34.469784');
+INSERT INTO vote (motion_id,voter_id,proxy_id,result,entered) VALUES (1,1,1,'yes','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,proxy_id,result,entered) VALUES (1,2,2,'yes','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,proxy_id,result,entered) VALUES (1,3,2,'no','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,proxy_id,result,entered) VALUES (2,1,1,'yes','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,proxy_id,result,entered) VALUES (2,2,2,'no','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,proxy_id,result,entered) VALUES (2,3,3,'no','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,proxy_id,result,entered) VALUES (3,3,3,'yes','2020-04-02 21:48:34.469784');
index cd47201bc863ba6eb894c6a3e9d7d041bf017e1f..6e0bc561fc317131254f33a35ef1d010cc091d87 100644 (file)
@@ -25,8 +25,21 @@ CREATE TABLE vote (motion_id INTEGER NOT NULL,
                  voter_id INTEGER NOT NULL,
                  result vote_type NOT NULL,
                  entered timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                 proxy_id INTEGER NOT NULL,
                  PRIMARY KEY(motion_id, voter_id));
 
+DROP TABLE IF EXISTS proxy;
+CREATE TABLE proxy (id serial NOT NULL,
+                   voter_id INTEGER NOT NULL,
+                   proxy_id INTEGER NOT NULL,
+                   granted timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                   granted_by int NOT NULL,
+                   revoked timestamp NULL DEFAULT NULL,
+                   revoked_by int NULL DEFAULT NULL,
+                   PRIMARY KEY(id));
+CREATE INDEX proxy_voter ON proxy (voter_id);
+CREATE INDEX proxy_proxy ON proxy (proxy_id);
+
 DROP TABLE IF EXISTS schema_version;
 CREATE TABLE schema_version (version INTEGER NOT NULL);
-INSERT INTO schema_version(version) VALUES(3);
+INSERT INTO schema_version(version) VALUES(5);
index 9d66ff4a749a06a5546c8844f5813e62603fab41..c65a9484050021a70375e67986bfd032d2bc767d 100644 (file)
@@ -2,7 +2,7 @@
 <html>
 <head>
 <title>{% block title %}Motion list{% endblock %}</title>
-<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
 <style type="text/css">
 .form-inline .motion {
   width: 100%;
 .motion p {
   padding: 10px;
 }
+td {
+  padding: 4px;
+}
 form {
-  margin-top:5px;
-  margin-bottom:5px;
+  padding-top: 4px;
+  padding-bottom: 4px;
 }
 </style>
 </head>
 <body>
+<nav class="navbar navbar-expand-lg navbar-light bg-light">
+  <a class="navbar-brand" href="../">{{'Motion list'}}</a>
+  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
+    <span class="navbar-toggler-icon"></span>
+  </button>
+   <div class="collapse navbar-collapse" id="navbarNavDropdown">
+    <ul class="navbar-nav">
+      <li class="nav-item">
+        <a class="nav-link" href="/">{{'Home'}}</a>
+      </li>
+{%- if may_proxyadmin %}
+      <li class="nav-item">
+        <a class="nav-link" href="/proxy">{{'Proxy management'}}</a>
+      </li>
+{%- endif %}
+      <li class="nav-item">
+        <a class="nav-link">{{'User'}}: {{g.user}}
+{%- if g.proxies_given %}
+         <br/>proxy granted to: {{g.proxies_given}}
+{%- endif %}
+{%- if g.proxies_received %}
+         <br/>holds proxy of: {{g.proxies_received}}
+{%- endif %}
+        </a>
+      </li>
+    </ul>
+  </div>
+</nav>
 {%- block body %}
 {% endblock %}
 <!-- Footer -->
   <footer class="page-footer">
     <div class="footer-copyright text-center py-3">
-      <p>© {{footer.version_year}} Copyright: <a href="{{footer.copyright_link}}">{{footer.copyright_name}}</a> 
+      <p>&copy; {{footer.version_year}} Copyright: <a href="{{footer.copyright_link}}">{{footer.copyright_name}}</a> 
       | <a href="{{footer.imprint_link}}">Imprint</a> 
       | <a href="{{footer.dataprotection_link}}">Data protection</a></p>
     </div>
index ad4b643d7bf1b456749199289b2f30a4e6d22bd1..16adecc2a9127fda861e405ce655b348eccc9ec7 100644 (file)
@@ -2,11 +2,6 @@
 {% block body %}
 <div class="container">
 {%- if categories|length != 0 %}
-<div class="card">
-<div class="card-body">
-User: {{g.user}}
-</div>
-</div>
 <form action="/motion" method="POST" class="form-inline">
 <div class="motion card">
   <div class="motion-title card-heading alert-light from-group">
diff --git a/templates/proxy.html b/templates/proxy.html
new file mode 100644 (file)
index 0000000..d81214c
--- /dev/null
@@ -0,0 +1,61 @@
+{% extends "base.html" %}
+{% block body %}
+<div class="container">
+<form action="/proxy/add" method="POST">
+  <table>
+  <tr>
+    <td>Voter</td>
+    <td>Proxy</td>
+    <td></td>
+  </tr>
+  <tr>
+    <td>
+      <select class="float form-control" name="voter">
+    {%- for voter in voters %}
+        <option>{{voter.email}}</option>
+    {%- endfor %}
+      </select>
+    </td>
+    <td>
+      <select class="float form-control" name="proxy">
+        {%- for voter in voters %}
+          <option>{{voter.email}}</option>
+        {%- endfor %}
+      </select>
+    </td>
+    <td>
+      <button type="submit" class="btn btn-primary">Add</button>
+    </td>
+  </tr>
+  </table>
+</form>
+{%- if proxies %}
+<form action="/proxy/revoke" method="POST">
+<div class="motion card" id="votes">
+  <div class="card-heading text-white bg-info">
+    Granted Proxies
+  </div>
+  <div class="card-body">
+    <table>
+      <thead>
+        <th>Voter</th>
+        <th>Proxy</th>
+        <th></th>
+      </thead>
+      {%- for row in proxies %}
+      <tr>
+        <td>{{row.voter_email}}</td>
+        <td>{{row.proxy_email}}</td>
+        <td><button type="submit" class="btn btn-danger" name="id" value="{{row.id}}">Revoke</button></td>
+      </tr>
+      {%- endfor %}
+    </table>
+  </div>
+</div>
+</form>
+{%- endif %}
+<form action="/proxy/revokeall" method="POST">
+  <button type="submit" class="btn btn-danger">Revoke ALL</button>
+</form>
+</div>
+{%- endblock %}
\ No newline at end of file
index 8935636a76ded2a4a9af21b484b09509a0515b0d..c53e4b51307bb67b66d5b7079662fa89bc8c8c29 100644 (file)
@@ -11,19 +11,40 @@ Motion: {{motion.name}}
   </div>
   <div class="card-body">
     {%- for row in votes %}
-    <div>{{row.email}}: {{row.result}}</div>
+    <div>{{row.email}}: {{row.result}}{%- if row.proxyemail %} : given by {{row.proxyemail}}{%- endif %}</div>
     {%- endfor %}
   </div>
 </div>
 {%- endif %}
+
 {%- if motion.running %}
+
 {%- if may_vote %}
-<form action="/motion/{{motion.identifier}}/vote" method="POST">
+<div class="panel panel-info" id="votes">
+  <div class="panel-body">
+<h3>My vote</h3>
+{%- if proxyname %}
+Given by {{proxyname[0][0]}}
+{%- endif %}
+<form action="/motion/{{motion.identifier}}/vote/{{g.voter}}" method="POST">
 {%- for vote in ['yes','no','abstain'] %}
 <button type="submit" class="btn btn-{{ 'success' if vote == motion.result else 'primary' }}" name="vote" value="{{vote}}" id="vote-{{vote}}">{{vote}}</button>
 {%- endfor %}
 </form>
+
+{%- for p in proxyvote %}
+<h3>Vote for {{p.email}}</h3>
+{%- if p.owneremail and p.result%}
+Voted by {{p.owneremail}}
+{%- endif %}
+<form action="/motion/{{motion.identifier}}/vote/{{p.voter_id}}" method="POST">
+{%- for vote in ['yes','no','abstain'] %}
+<button type="submit" class="btn btn-{{ 'success' if vote == p.result else 'primary' }}" name="vote" value="{{vote}}" id="vote-{{vote}}">{{vote}}</button>
+{%- endfor %}
+</form>
+{%- endfor %}
 {%- endif %}
+
 {%- if may_cancel %}
 <form action="/motion/{{motion.identifier}}/cancel" method="POST" class="form-inline">
 <input type="text" placeholder="cancelation reason" name="reason" class="form-control" required="yes">
@@ -35,6 +56,9 @@ Motion: {{motion.name}}
 <button type="submit" class="btn btn-danger" name="finish" value="finish" id="finish">Finish</button></br>
 </form>
 {%- endif %}
+
 {%- endif %}
 <a href="/?start={{motion.id}}#motion-{{motion.id}}" class="btn btn-primary">Back</a>
 {%- endblock %}
+  </div>
+</div>
index 1bc65858461318e377cf8660479502895cf80cff..0bee29420e123a31029dcc6922b3d1c9103a71d7 100644 (file)
@@ -9,7 +9,8 @@ app.config.update(
     DEBUGUSER = {},
     GROUP_PREFIX = {'127.0.0.1:5000': {'group1': 'g1', 'group2': 'g2'}},
     DURATION = {'127.0.0.1:5000':[3, 7, 14]},
-    SERVER_NAME = '127.0.0.1:5000'
+    SERVER_NAME = '127.0.0.1:5000',
+    MAX_PROXY=2
 )
 
 app.config['TESTING'] = True
@@ -26,9 +27,9 @@ class BasicTest(TestCase):
         self.db_clear()
 
     # functions to manipulate motions
-    def createVote(self, user, motion, vote):
+    def createVote(self, user, motion, vote, voter):
         return self.app.post(
-            '/motion/' + motion +'/vote',
+            '/motion/' + motion + '/vote/' + str(voter),
             environ_base={'USER_ROLES': user},
             data=dict(vote=vote)
         )
@@ -54,6 +55,20 @@ class BasicTest(TestCase):
             environ_base={'USER_ROLES': user}
         )
 
+    def addProxy(self, user, voter, proxy):
+        return self.app.post(
+            '/proxy/add',
+            environ_base={'USER_ROLES': user},
+            data=dict(voter=voter, proxy=proxy)
+        )
+
+    def revokeProxy(self, user, id):
+        return self.app.post(
+            '/proxy/revoke',
+            environ_base={'USER_ROLES': user},
+            data=dict(id=id)
+        )
+
     def buildResultText(self, motiontext, yes, no, abstain):
         return '<p>'+motiontext+'</p></p>\n    <p>\nYes <span class=\"badge badge-pill badge-secondary\">'+str(yes)+'</span><br>'\
             + '\nNo <span class=\"badge badge-pill badge-secondary\">'+str(no)+'</span><br>'\
@@ -78,6 +93,8 @@ class GeneralTests(BasicTest):
         self.init_test()
         global user
         user = 'testuser/'
+        global userid
+        userid = 4
         self.db_sampledata()
 
     def tearDown(self):
@@ -126,6 +143,8 @@ class GeneralTests(BasicTest):
             + '\nNo <span class=\"badge badge-pill badge-secondary\">1</span><br>'\
             + '\nAbstain <span class=\"badge badge-pill badge-secondary\">0</span><br>\n    </p>\n  </div>\n</div>\n</div>'
         self.assertIn(str.encode(testtext), result.data)
+        testtext= 'Proxy management'
+        self.assertNotIn(str.encode(testtext), result.data)
 
         # start with second motion
         result = self.app.get('/', environ_base={'USER_ROLES': user}, query_string=dict(start=2))
@@ -144,7 +163,7 @@ class GeneralTests(BasicTest):
 
     def test_vote(self):
         motion='g1.20200402.004'
-        response = self.createVote(user, motion, 'yes')
+        response = self.createVote(user, motion, 'yes', userid)
         self.assertEqual(response.status_code, 403)
         self.assertIn(str.encode('Forbidden'), response.data)
 
@@ -169,13 +188,34 @@ class GeneralTests(BasicTest):
         self.assertEqual(result.status_code, 404)
         self.assertIn(str.encode('Error, Not found'), result.data)
 
+    def test_no_proxy(self):
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(result.status_code, 403)
+        self.assertIn(str.encode('Forbidden'), result.data)
+
+    def test_no_proxy_add(self):
+        result = self.app.post('proxy/add', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(result.status_code, 403)
+        self.assertIn(str.encode('Forbidden'), result.data)
+
+    def test_no_proxy_revoke(self):
+        result = self.app.post('proxy/revoke', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(result.status_code, 403)
+        self.assertIn(str.encode('Forbidden'), result.data)
 
+    def test_no_proxy_revokeAll(self):
+        result = self.app.post('proxy/revokeall', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(result.status_code, 403)
+        self.assertIn(str.encode('Forbidden'), result.data)
+        
 class VoterTests(BasicTest):
 
     def setUp(self):
         self.init_test()
         global user
         user='testuser/vote:*'
+        global userid
+        userid = 4
         self.db_sampledata()
 
     def tearDown(self):
@@ -191,7 +231,7 @@ class VoterTests(BasicTest):
 
     def test_vote_yes(self):
         motion='g1.20200402.004'
-        response = self.createVote(user, motion, 'yes')
+        response = self.createVote(user, motion, 'yes', userid)
         self.assertEqual(response.status_code, 302)
         result = self.app.get('/', environ_base={'USER_ROLES': user})
         resulttext=self.buildResultText('A fourth motion', 1, 0, 0)
@@ -205,7 +245,7 @@ class VoterTests(BasicTest):
 
     def test_vote_no(self):
         motion='g1.20200402.004'
-        response = self.createVote(user, motion, 'no')
+        response = self.createVote(user, motion, 'no', userid)
         self.assertEqual(response.status_code, 302)
         result = self.app.get('/', environ_base={'USER_ROLES': user})
         resulttext=self.buildResultText('A fourth motion', 0, 1, 0)
@@ -220,7 +260,7 @@ class VoterTests(BasicTest):
 
     def test_vote_abstain(self):
         motion='g1.20200402.004'
-        response = self.createVote(user, motion, 'abstain')
+        response = self.createVote(user, motion, 'abstain', userid)
         self.assertEqual(response.status_code, 302)
         result = self.app.get('/', environ_base={'USER_ROLES': user})
         resulttext=self.buildResultText('A fourth motion', 0, 0, 1)
@@ -235,17 +275,17 @@ class VoterTests(BasicTest):
 
     def test_vote_change(self):
         motion='g1.20200402.004'
-        response = self.createVote(user, motion, 'yes')
+        response = self.createVote(user, motion, 'yes', userid)
         self.assertEqual(response.status_code, 302)
         result = self.app.get('/', environ_base={'USER_ROLES': user})
         resulttext=self.buildResultText('A fourth motion', 1, 0, 0)
         self.assertIn(str.encode(resulttext), result.data)
-        response = self.createVote(user, motion, 'no')
+        response = self.createVote(user, motion, 'no', userid)
         self.assertEqual(response.status_code, 302)
         result = self.app.get('/', environ_base={'USER_ROLES': user})
         resulttext=self.buildResultText('A fourth motion', 0, 1, 0)
         self.assertIn(str.encode(resulttext), result.data)
-        response = self.createVote(user, motion, 'abstain')
+        response = self.createVote(user, motion, 'abstain', userid)
         self.assertEqual(response.status_code, 302)
         result = self.app.get('/', environ_base={'USER_ROLES': user})
         resulttext=self.buildResultText('A fourth motion', 0, 0, 1)
@@ -253,41 +293,41 @@ class VoterTests(BasicTest):
 
     def test_vote_group(self):
         motion='g1.20200402.004'
-        response = self.createVote(user, motion, 'yes')
+        response = self.createVote(user, motion, 'yes', userid)
         self.assertEqual(response.status_code, 302)
 
         motion='g1.20200402.004'
         user1='testuser/vote:group1'
-        response = self.createVote(user1, motion, 'yes')
+        response = self.createVote(user1, motion, 'yes', userid)
         self.assertEqual(response.status_code, 302)
 
         motion='g1.20200402.004'
         user1='testuser/vote:group1 vote:group2'
-        response = self.createVote(user1, motion, 'yes')
+        response = self.createVote(user1, motion, 'yes', userid)
         self.assertEqual(response.status_code, 302)
 
     def test_vote_wrong_group(self):
         motion='g1.20200402.004'
         user1='testuser/vote:group2'
-        response = self.createVote(user1, motion, 'yes')
+        response = self.createVote(user1, motion, 'yes', userid)
         self.assertEqual(response.status_code, 403)
         self.assertIn(str.encode('Forbidden'), response.data)
 
     def test_vote_closed(self):
         motion='g1.20200402.002'
-        response = self.createVote(user, motion, 'abstain')
+        response = self.createVote(user, motion, 'abstain', userid)
         self.assertEqual(response.status_code, 403)
         self.assertIn(str.encode('Error, out of time'), response.data)
 
     def test_vote_canceled(self):
         motion='g1.20200402.003'
-        response = self.createVote(user, motion, 'abstain')
+        response = self.createVote(user, motion, 'abstain', userid)
         self.assertEqual(response.status_code, 403)
         self.assertIn(str.encode('Error, motion was canceled'), response.data)
 
     def test_vote_not_given(self):
         motion='g1.30190402.001'
-        response = self.createVote(user, motion, 'abstain')
+        response = self.createVote(user, motion, 'abstain', userid)
         self.assertEqual(response.status_code, 404)
         self.assertIn(str.encode('Error, Not found'), response.data)
 
@@ -517,6 +557,313 @@ class AuditMotionTests(BasicTest):
             + '\n    <div>User C: no</div>\n  </div>\n</div>\n<a href="/?start=2#motion-2" class="btn btn-primary">Back</a>'
         self.assertIn(str.encode(testtext), result.data)
 
+class ProxyManagementTests(BasicTest):
+
+    def setUp(self):
+        self.init_test()
+        global user
+        user='testuser/proxyadmin:*'
+        self.db_sampledata()
+
+    def tearDown(self):
+        pass
+
+    def test_see_proxy(self):
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= 'div class="container">\n<form action="/proxy/add" method="POST">'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= '<select class="float form-control" name="voter">\n        '\
+            + '<option>User A</option>\n        <option>User B</option>\n        '\
+            + '<option>User C</option>\n        '\
+            + '<option>testuser</option>\n      '\
+            + '</select>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<select class="float form-control" name="proxy">\n          '\
+            + '<option>User A</option>\n          '\
+            + '<option>User B</option>\n          '\
+            + '<option>User C</option>\n          '\
+            + '<option>testuser</option>\n      '\
+            + '</select>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n    '\
+            + '</table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<a class="nav-link" href="/proxy">Proxy management</a>'
+        self.assertIn(str.encode(testtext), result.data)
+
+    def test_add_proxy(self):
+        voter=''
+        proxy=''
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter equals proxy.'), response.data)
+
+        voter='User A'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy not found.'), response.data)
+
+        voter='User Z'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter not found.'), response.data)
+
+        voter=''
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter not found.'), response.data)
+
+        voter='User B'
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter equals proxy.'), response.data)
+
+        voter='User A'
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<form action="/proxy/revoke" method="POST">'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        '\
+            + '<th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy allready given.'), response.data)
+
+        voter='User A'
+        proxy='User C'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy allready given.'), response.data)
+
+        voter='User C'
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        '\
+            + '<th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+
+        voter='testuser'
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, Max proxy for \'User B\' reached.'), response.data)
+        
+        voter='testuser'
+        proxy='User A'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to: User A\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+
+        voter='User B'
+        proxy='testuser'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User B</td>\n        <td>testuser</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="4">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to: User A\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of: User B\n'
+        self.assertIn(str.encode(testtext), result.data)
+
+        response = self.revokeProxy(user, userid)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to: User A\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+
+        response = self.revokeProxy(user, 3)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+
+        result = self.app.post('proxy/revokeall', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n    '\
+            + '</table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+
+class ProxyVoteTests(BasicTest):
+
+    def setUp(self):
+        self.init_test()
+        global user
+        user='testuser/vote:* proxyadmin:*'
+        self.db_sampledata()
+
+    def tearDown(self):
+        pass
+
+    def test_proxy_vote(self):
+        voter='testuser'
+        proxy='User B'
+        proxyid=2
+        proxyuser='User B/vote:*'
+
+        response = self.addProxy(user, proxy, voter)
+        self.assertEqual(response.status_code, 302)
+
+        motion='g1.20200402.004'
+        response = self.createVote(user, motion, 'yes', proxyid)
+        self.assertEqual(response.status_code, 302)
+
+        # testuser view
+        result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
+        # own vote without change
+        testtext= '<form action="/motion/g1.20200402.004/vote/4" method="POST">\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="yes" id="vote-yes">yes</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">no</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">abstain</button>\n</form>'
+        self.assertIn(str.encode(testtext), result.data)
+        # proxy vote with change
+        testtext= '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
+            + '<button type="submit" class="btn btn-success" name="vote" value="yes" id="vote-yes">yes</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">no</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">abstain</button>\n</form>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        
+        # User B view
+        result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': proxyuser}, follow_redirects=True)
+        # own vote without change
+        testtext= '<h3>My vote</h3>\nGiven by testuser\n'\
+            + '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
+            + '<button type="submit" class="btn btn-success" name="vote" value="yes" id="vote-yes">yes</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">no</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">abstain</button>\n</form>'
+        self.assertIn(str.encode(testtext), result.data)
+        
+        # change vote
+        response = self.createVote(user, motion, 'no', proxyid)
+        self.assertEqual(response.status_code, 302)
+
+        result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="yes" id="vote-yes">yes</button>\n'\
+            + '<button type="submit" class="btn btn-success" name="vote" value="no" id="vote-no">no</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">abstain</button>\n</form>\n'
+        self.assertIn(str.encode(testtext), result.data)
+
+    def test_proxy_vote_no_proxy(self):
+        voter='testuser'
+        proxy='User B'
+        # wrong proxy id
+        proxyid=3
+
+        response = self.addProxy(user, proxy, voter)
+        self.assertEqual(response.status_code, 302)
+
+        motion='g1.20200402.004'
+        response = self.createVote(user, motion, 'yes', proxyid)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy not found'), response.data)
+        
+        # non existing id
+        proxyid=10000
+
+        motion='g1.20200402.004'
+        response = self.createVote(user, motion, 'yes', proxyid)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy not found'), response.data)
+
+    def test_proxy_vote_no_voter(self):
+        voter='User A'
+        proxy='User B'
+        proxyid=2
+
+        response = self.addProxy(user, proxy, voter)
+        self.assertEqual(response.status_code, 302)
+
+        user1='testuser1/'
+        motion='g1.20200402.004'
+        response = self.createVote(user1, motion, 'yes', proxyid)
+        self.assertEqual(response.status_code, 403)
+        self.assertIn(str.encode('Forbidden'), response.data)
+
+
 
 if __name__ == "__main__":
     unittest.main()