]> WPIA git - motion.git/commitdiff
add: implement proxy voting
authorINOPIAE <m.maengel@inopiae.de>
Sun, 23 Feb 2020 19:54:02 +0000 (20:54 +0100)
committerINOPIAE <m.maengel@inopiae.de>
Tue, 10 Nov 2020 19:48:02 +0000 (20:48 +0100)
motion.py
sql/from_4.sql [new file with mode: 0644]
sql/sample_data.sql
sql/schema.sql
templates/base.html
templates/single_motion.html
tests/test_motion.py

index 0503b829722ea6831a5a7ac0fa5b071ea33cccaf..9e70c991dcd1fbe4354e7b9845749ad419ac964c 100644 (file)
--- a/motion.py
+++ b/motion.py
@@ -90,10 +90,11 @@ 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");
-        rv = db.prepare("SELECT email FROM voter, proxy WHERE proxy.proxy_id = voter.id AND proxy.revoked IS NULL AND proxy.voter_id = $1 ")(g.voter)
+        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 FROM voter, proxy WHERE proxy.voter_id = voter.id AND proxy.revoked IS NULL AND proxy.proxy_id = $1 ")(g.voter)
+        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 = ""
@@ -205,6 +206,11 @@ def init_db():
                 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()
 
 
@@ -270,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):
@@ -292,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):
@@ -313,26 +328,46 @@ 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, may_proxyadmin=may_admin("proxyadmin"))
 
-@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():
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 ea020f8698733b2da6bd1e8503fd94d45c65bf05..40532deefb45a49c8cba3e2a6b321949fdb6740e 100644 (file)
@@ -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 be4b3d2a0ac4cb94c2ea272dcbbeb130e0225ac0..6e0bc561fc317131254f33a35ef1d010cc091d87 100644 (file)
@@ -25,6 +25,7 @@ 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;
@@ -41,4 +42,4 @@ 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(4);
+INSERT INTO schema_version(version) VALUES(5);
index db44681aa7b6bbabcb29da3e9a8f60d003b47235..c65a9484050021a70375e67986bfd032d2bc767d 100644 (file)
 .motion p {
   padding: 10px;
 }
+td {
+  padding: 4px;
+}
 form {
-  margin-top:5px;
-  margin-bottom:5px;
+  padding-top: 4px;
+  padding-bottom: 4px;
 }
 </style>
 </head>
@@ -82,13 +85,12 @@ form {
     </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 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 6551a31ed7e516e538b83fec1f8416ecd19e98de..0bee29420e123a31029dcc6922b3d1c9103a71d7 100644 (file)
@@ -27,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)
         )
@@ -93,6 +93,8 @@ class GeneralTests(BasicTest):
         self.init_test()
         global user
         user = 'testuser/'
+        global userid
+        userid = 4
         self.db_sampledata()
 
     def tearDown(self):
@@ -161,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)
 
@@ -212,6 +214,8 @@ class VoterTests(BasicTest):
         self.init_test()
         global user
         user='testuser/vote:*'
+        global userid
+        userid = 4
         self.db_sampledata()
 
     def tearDown(self):
@@ -227,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)
@@ -241,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)
@@ -256,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)
@@ -271,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)
@@ -289,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)
 
@@ -718,7 +722,7 @@ class ProxyManagementTests(BasicTest):
         testtext= 'holds proxy of: User B\n'
         self.assertIn(str.encode(testtext), result.data)
 
-        response = self.revokeProxy(user, 4)
+        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      '\
@@ -763,6 +767,103 @@ class ProxyManagementTests(BasicTest):
             + '</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()