# emacs
\#*\#
*~
+messages.pot
+messages.mo
+messages.po
DURATION={'hostname':[3, 7, 14]} # duration period for motions
#DEBUGUSER={'hostname':'username/create:* vote:*'} # remove # at beginning of line to use local debuguser
+
+LANGUAGES = {
+ 'en': 'English',
+ 'de': 'Deutsch'
+}
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
+from flask_language import Language, current_language
+import gettext
def get_db():
db = getattr(g, '_database', None)
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):
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
roles = env.get("ROLES")
if user is None:
- return "Server misconfigured", 500
+ return _('Server misconfigured'), 500
roles = roles.split(" ")
if user == "<invalid>":
- return "Access denied", 403;
+ return _('Access denied'), 403;
db = get_db()
with db.xact():
else:
prev = -1
return render_template('index.html', motions=rv[:10], more=rv[10]["id"] if len(rv) == 11 else None, times=times.per_host, prev=prev,
- categories=get_allowed_cats("create"), singlemotion=False, may_proxyadmin=may_admin("proxyadmin"))
+ categories=get_allowed_cats("create"), singlemotion=False, may_proxyadmin=may_admin("proxyadmin"), languages=get_languages())
def rel_redirect(loc):
r = redirect(loc)
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:
- return "Error, invalid length", 400
+ return _('Error, invalid length'), 400
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 =='':
- return "Error, missing content", 400
+ return _('Error, missing content'), 400
db = get_db()
with db.xact():
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")):
- return "Forbidden", 403
+ return _('Forbidden'), 403
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"):
- return "Error, out of time", 403
+ return _('Error, out of time'), 403
return f(motion, id)
decorated_function.__name__ = f.__name__
return decorated_function
@validate_motion_access('cancel')
def cancel_motion(motion, id):
if request.form.get("reason", "none") == "none":
- return "Error, form requires reason", 500
+ return _('Error, form requires reason'), 500
rv = get_db().prepare("UPDATE motion SET canceled=CURRENT_TIMESTAMP, cancelation_reason=$1, canceled_by=$2 WHERE identifier=$3 AND host=$4 AND canceled is NULL")(request.form.get("reason", ""), g.voter, motion, request.host)
return motion_edited(motion)
+ "WHERE motion.identifier=$1 AND motion.host=$3")
resultmotion = p(motion, g.voter, request.host)
if len(resultmotion) == 0:
- return "Error, Not found", 404
+ return _('Error, Not found'), 404
p = get_db().prepare("SELECT voter.email FROM vote INNER JOIN voter ON vote.proxy_id = voter.id WHERE vote.motion_id=$1 AND vote.voter_id=$2 AND vote.proxy_id <> vote.voter_id")
resultproxyname = p(resultmotion[0][0], g.voter)
if may("audit", resultmotion[0].get("type")) and not resultmotion[0].get("running") and not resultmotion[0].get("canceled"):
votes = get_db().prepare("SELECT vote.result, voter.email FROM vote INNER JOIN voter ON voter.id = vote.voter_id WHERE vote.motion_id=$1")(resultmotion[0].get("id"));
votes = get_db().prepare("SELECT vote.result, voter.email, CASE voter.email WHEN proxy.email THEN NULL ELSE proxy.email END as proxyemail FROM vote INNER JOIN voter ON voter.id = vote.voter_id INNER JOIN voter as proxy ON proxy.id = vote.proxy_id WHERE vote.motion_id=$1")(resultmotion[0].get("id"));
- return render_template('single_motion.html', motion=resultmotion[0], may_vote=may("vote", resultmotion[0].get("type")), may_cancel=may("cancel", resultmotion[0].get("type")), votes=votes, proxyvote=resultproxyvote, proxyname=resultproxyname)
+ return render_template('single_motion.html', motion=resultmotion[0], may_vote=may("vote", resultmotion[0].get("type")), may_cancel=may("cancel", resultmotion[0].get("type")), votes=votes, proxyvote=resultproxyvote, proxyname=resultproxyname, languages=get_languages())
@app.route("/motion/<string:motion>/vote/<string:voter>", methods=['POST'])
@validate_motion_access_vote('vote')
if (voterid != g.voter):
rv = db.prepare("SELECT voter_id FROM proxy WHERE proxy.revoked IS NULL AND proxy.proxy_id = $1 AND proxy.voter_id = $2")(g.voter, voterid);
if len(rv) == 0:
- return "Error, proxy not found.", 400
+ return _('Error, proxy not found.'), 400
p = db.prepare("SELECT * FROM vote WHERE motion_id = $1 AND voter_id = $2")
rv = p(id, voterid)
@app.route("/proxy")
def proxy():
if not may_admin("proxyadmin"):
- return "Forbidden", 403
- return render_template('proxy.html', voters=get_voters(), proxies=get_all_proxies(), may_proxyadmin=may_admin("proxyadmin"))
+ return _('Forbidden'), 403
+ return render_template('proxy.html', voters=get_voters(), proxies=get_all_proxies(), may_proxyadmin=may_admin("proxyadmin"), languages=get_languages())
@app.route("/proxy/add", methods=['POST'])
def add_proxy():
if not may_admin("proxyadmin"):
- return "Forbidden", 403
+ return _('Forbidden'), 403
voter=request.form.get("voter", "")
proxy=request.form.get("proxy", "")
if voter == proxy :
- return "Error, voter equals proxy.", 400
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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("/")
click==6.7
Flask==0.12.2
+Flask-Babel==1.0.0
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
<!DOCTYPE html>
<html>
<head>
-<title>{% block title %}Motion list{% endblock %}</title>
+<title>{% block title %}{{_('Motion list')}}{% endblock %}</title>
<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">
+<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
+<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<style type="text/css">
.form-inline .motion {
width: 100%;
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
- <a class="navbar-brand" href="../">{{'Motion list'}}</a>
+ <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>
+ <a class="nav-link" href="/">{{_('Home')}}</a>
</li>
{%- if may_proxyadmin %}
<li class="nav-item">
- <a class="nav-link" href="/proxy">{{'Proxy management'}}</a>
+ <a class="nav-link" href="/proxy">{{_('Proxy management')}}</a>
</li>
{%- endif %}
<li class="nav-item">
- <a class="nav-link">{{'User'}}: {{g.user}}
+ <a class="nav-link">{{_('User')}}: {{g.user}}
{%- if g.proxies_given %}
- <br/>proxy granted to: {{g.proxies_given}}
+ <br/>{{_('proxy granted to')}}: {{g.proxies_given}}
{%- endif %}
{%- if g.proxies_received %}
- <br/>holds proxy of: {{g.proxies_received}}
+ <br/>{{_('holds proxy of')}}: {{g.proxies_received}}
{%- endif %}
</a>
</li>
+ <li class="nav-item dropdown">
+ <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ {{_('Language')}}
+ </a>
+ <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
+ {%- for k, v in languages.items() %}
+ <a class="dropdown-item" href="/language/{{k}}">{{v}}</a>
+ {%- endfor %}
+ </div>
+ </li>
</ul>
</div>
</nav>
<!-- 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>
- | <a href="{{footer.imprint_link}}">Imprint</a>
- | <a href="{{footer.dataprotection_link}}">Data protection</a></p>
+ <p>© {{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>
</footer>
</body>
{% block body %}
<div class="container">
{%- if categories|length != 0 %}
+<div class="card">
+</div>
<form action="/motion" method="POST" class="form-inline">
<div class="motion card">
<div class="motion-title card-heading alert-light from-group">
- <input class="form-control motion-title-input" placeholder="Motion title" type="text" name="title" id="title" required="yes">
+ <input class="form-control motion-title-input" placeholder="{{_('Motion title')}}" type="text" name="title" id="title" required="yes">
{%- if categories|length == 1 %}
<input type="text" class="float form-control" maxwidth="10" disabled value="{{categories[0]}}">
<input type="hidden" name="category" value="{{categories[0]}}">
</select>
</div>
<div class="card-body">
- <textarea class="form-control" placeholder="Motion content" name="content" rows="8"></textarea><br>
- Editing note: Markdown is used formatting.<br>
- To add a line break add two lines, to enter a link use [text](https://domain.tld/link)<br>
- <button class="btn btn-primary" type="submit">Submit Motion</button>
+ <textarea class="form-control" placeholder="{{_('Motion content')}}" name="content" rows="8"></textarea><br>
+ {{_('Editing note: Markdown is used formatting.')}}<br>
+ {{_('To add a line break add two lines, to enter a link use [text](https//domain.tld/link)')}}<br>
+ <button class="btn btn-primary" type="submit">{{_('Submit Motion')}}</button>
</div>
</div>
</form>
{%- endif %}
{%- if prev %}
{%- if prev == -1 %}
-<a href="/" class="btn btn-primary">Prev</a>
+<a href="/" class="btn btn-primary">{{_('Prev')}}</a>
{%- else %}
-<a href="/?start={{ prev }}" class="btn btn-primary">Prev</a>
+<a href="/?start={{ prev }}" class="btn btn-primary">{{_('Prev')}}</a>
{%- endif %}
{%- endif %}
{%- for motion in motions %}
{% include 'motion.html' %}
{%- endfor %}
{%- if more %}
-<a href="/?start={{ more }}" class="btn btn-primary">Next</a>
+<a href="/?start={{ more }}" class="btn btn-primary">{{_('Next')}}</a>
{%- endif %}
</div>
{%- endblock %}
{%- elif motion.yes is defined %}{% if motion.yes != None and motion.no != None and motion.yes > motion.no %} alert-success{% else %} alert-danger{% endif %}
{%- else %} bg-light{%- endif -%}
">
- <span class="title-text">{{motion.name}}</span> ({{ 'Running' if motion.running else ('Canceled' if motion.canceled != None else 'Finished') }})
+ <span class="title-text">{{motion.name}}</span> ({{ _('Running') if motion.running else (_('Canceled') if motion.canceled != None else _('Finished')) }})
<span class="motion-type">{{motion.type}}</span>
<div># {{motion.identifier}}
{%- if singlemotion == False %}
- <a class="btn btn-primary" href="/motion/{{motion.identifier}}" role="button">{{ 'Vote' if motion.running else 'Result' }}</a>
+ <a class="btn btn-primary" href="/motion/{{motion.identifier}}" role="button">{{ _('Vote') if motion.running else _('Result') }}</a>
{%- endif %}
</div>
<div class="date">
- <div>Proposed: {{motion.posed|timestamp}} (UTC) by {{motion.poser}}</div>
+ <div>{{_('Proposed')}}: {{_('%(dt)s (UTC) by %(user)s', dt=motion.posed|timestamp, user=motion.poser)}}</div>
{%- if motion.canceled != None %}
- <div>Canceled: {{motion.canceled|timestamp}} (UTC) by {{motion.canceler}}</div></div>
+ <div>{{_('Canceled')}}: {{_('%(dt)s (UTC) by %(user)s', dt=motion.canceled|timestamp, user=motion.poser)}}</div>
{%- else %}
- <div>Votes until: {{motion.deadline|timestamp}} (UTC)</div></div>
+ <div>{{_('Votes until')}}: {{_('%(dt)s (UTC)', dt=motion.deadline|timestamp)}}</div>
{%- endif %}
+ </div>
</div>
<div class="card-body">
<p>{{motion.content|markdown}}</p>
{%- if motion.yes or motion.no or motion.abstain %}
<p>
{%- for vote in ['yes', 'no', 'abstain'] %}
-{{vote|capitalize}} <span class="badge badge-pill badge-secondary">{{motion[vote]}}</span><br>
+{{_(vote)|capitalize}} <span class="badge badge-pill badge-secondary">{{motion[vote]}}</span><br>
{%- endfor %}
</p>
{%- endif %}
{%- if motion.canceled != None %}
- <p>Cancelation reason: {{motion.cancelation_reason}}</p>
+ <p>{{_('Cancelation reason')}}: {{motion.cancelation_reason}}</p>
{%- endif %}
</div>
{%- block content %}{% endblock %}
<form action="/proxy/add" method="POST">
<table>
<tr>
- <td>Voter</td>
- <td>Proxy</td>
+ <td>{{_('Voter')}}</td>
+ <td>{{_('Proxy')}}</td>
<td></td>
</tr>
<tr>
</select>
</td>
<td>
- <button type="submit" class="btn btn-primary">Add</button>
+ <button type="submit" class="btn btn-primary">{{_('Add')}}</button>
</td>
</tr>
</table>
<form action="/proxy/revoke" method="POST">
<div class="motion card" id="votes">
<div class="card-heading text-white bg-info">
- Granted Proxies
+ {{_('Granted Proxies')}}
</div>
<div class="card-body">
<table>
<thead>
- <th>Voter</th>
- <th>Proxy</th>
+ <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>
+ <td><button type="submit" class="btn btn-danger" name="id" value="{{row.id}}">{{_('Revoke')}}</button></td>
</tr>
{%- endfor %}
</table>
</form>
{%- endif %}
<form action="/proxy/revokeall" method="POST">
- <button type="submit" class="btn btn-danger">Revoke ALL</button>
+ <button type="submit" class="btn btn-danger">{{_('Revoke all')}}</button>
</form>
</div>
{%- endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title -%}
-Motion: {{motion.name}}
+{{_('Motion')}}: {{motion.name}}
{%- endblock %}
{% block body %}
{%- include 'motion.html' %}
{%- if votes %}
<div class="motion card" id="votes">
<div class="card-heading text-white bg-info">
- Motion Votes
+ {{_('Motion Votes')}}
</div>
<div class="card-body">
{%- for row in votes %}
- <div>{{row.email}}: {{row.result}}{%- if row.proxyemail %} : given by {{row.proxyemail}}{%- endif %}</div>
+ <div>{{row.email}}: {{row.result}}{%- if row.proxyemail %} : {_('given by')}} {{row.proxyemail}}{%- endif %}</div>
{%- endfor %}
</div>
</div>
{%- if may_vote %}
<div class="panel panel-info" id="votes">
<div class="panel-body">
-<h3>My vote</h3>
+<h3>{{_('My vote')}}</h3>
{%- if proxyname %}
-Given by {{proxyname[0][0]}}
+{{_('Given by %(pn)s', pn=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>
+{%- 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)|capitalize}}</button>
{%- endfor %}
</form>
{%- for p in proxyvote %}
-<h3>Vote for {{p.email}}</h3>
+<h3>{{_('Vote for %(email)s', email=p.email)}}</h3>
{%- if p.owneremail and p.result%}
-Voted by {{p.owneremail}}
+{{_('Voted by %(email)s', email=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>
+{%- 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)|capitalize}}</button>
{%- endfor %}
</form>
{%- endfor %}
{%- 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">
-<button type="submit" class="btn btn-danger" name="cancel" value="cancel" id="cancel">Cancel</button></br>
+<input type="text" placeholder="{{_('Cancelation reason')}}" name="reason" class="form-control" required="yes">
+<button type="submit" class="btn btn-danger" name="cancel" value="cancel" id="cancel">{{_('Cancel')}}</button></br>
</form>
{%- endif %}
{%- if may_finish %}
<form action="/motion/{{motion.identifier}}/finish" method="POST" class="form-inline">
-<button type="submit" class="btn btn-danger" name="finish" value="finish" id="finish">Finish</button></br>
+<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>
+<a href="/?start={{motion.id}}#motion-{{motion.id}}" class="btn btn-primary">{{_('Back')}}</a>
{%- endblock %}
</div>
</div>
def test_basic_results_data(self):
result = self.app.get('/', environ_base={'USER_ROLES': user}, follow_redirects=True)
testtext= '<div class="motion card" id="motion-3">\n <div class="motion-title card-heading alert-warning">'\
- + '\n <span class=\"title-text\">Motion C</span> (Canceled)\n <span class=\"motion-type\">group1</span>'\
+ + '\n <span class="title-text">Motion C</span> (Canceled)\n <span class="motion-type">group1</span>'\
+ '\n <div># g1.20200402.003'\
+ '\n <a class="btn btn-primary" href="/motion/g1.20200402.003" role="button">Result</a>'\
- + '\n </div>'\
- + '\n <div class=\"date\">\n <div>Proposed: 2020-04-02 21:47:24 (UTC) by User A</div>'\
- + '\n <div>Canceled: 2020-04-03 21:48:24 (UTC) by User A</div></div>\n </div>'\
- + '\n <div class=\"card-body\">\n <p><p>A third motion</p></p>'\
- + '\n <p>\nYes <span class=\"badge badge-pill badge-secondary\">1</span><br>'\
- + '\nNo <span class=\"badge badge-pill badge-secondary\">0</span><br>'\
- + '\nAbstain <span class=\"badge badge-pill badge-secondary\">0</span><br>\n </p>'\
- + '\n <p>Cancelation reason: Entered with wrong text</p>\n </div>\n</div>'
+ + '\n </div>\n <div class="date">'\
+ + '\n <div>Proposed: 2020-04-02 21:47:24 (UTC) by User A</div>'\
+ + '\n <div>Canceled: 2020-04-03 21:48:24 (UTC) by User A</div>\n </div>\n </div>'\
+ + '\n <div class="card-body">\n <p><p>A third motion</p></p>'\
+ + '\n <p>\nYes <span class="badge badge-pill badge-secondary">1</span><br>'\
+ + '\nNo <span class="badge badge-pill badge-secondary">0</span><br>'\
+ + '\nAbstain <span class="badge badge-pill badge-secondary">0</span><br>\n </p>'\
+ + '\n <p>Cancelation reason: Entered with wrong text</p>\n </div>\n</div>\n'
self.assertIn(str.encode(testtext), result.data)
testtext= '<div class="motion card" id="motion-2">\n <div class="motion-title card-heading alert-danger">'\
- + '\n <span class=\"title-text\">Motion B</span> (Finished)\n <span class=\"motion-type\">group1</span>'\
+ + '\n <span class="title-text">Motion B</span> (Finished)\n <span class="motion-type">group1</span>'\
+ '\n <div># g1.20200402.002'\
+ '\n <a class="btn btn-primary" href="/motion/g1.20200402.002" role="button">Result</a>'\
- + '\n </div>'\
- + '\n <div class=\"date\">\n <div>Proposed: 2020-04-02 21:41:26 (UTC) by User A</div>'\
- + '\n <div>Votes until: 2020-04-04 21:41:26 (UTC)</div></div>\n </div>'\
- + '\n <div class=\"card-body\">\n <p><p>A second motion</p></p>'\
- + '\n <p>\nYes <span class=\"badge badge-pill badge-secondary\">1</span><br>'\
- + '\nNo <span class=\"badge badge-pill badge-secondary\">2</span><br>'\
- + '\nAbstain <span class=\"badge badge-pill badge-secondary\">0</span><br>\n </p>\n </div>\n</div>\n'
- self.assertIn(str.encode(testtext), result.data)
- testtext= '<div class=\"motion card\" id=\"motion-1\">\n <div class=\"motion-title card-heading alert-success\">'\
- + '\n <span class=\"title-text\">Motion A</span> (Finished)\n <span class=\"motion-type\">group1</span>'\
+ + '\n </div>\n <div class="date">\n <div>Proposed: 2020-04-02 21:41:26 (UTC) by User A</div>'\
+ + '\n <div>Votes until: 2020-04-04 21:41:26 (UTC)</div>\n </div>\n </div>'\
+ + '\n <div class="card-body">\n <p><p>A second motion</p></p>\n <p>'\
+ + '\nYes <span class="badge badge-pill badge-secondary">1</span><br>'\
+ + '\nNo <span class="badge badge-pill badge-secondary">2</span><br>'\
+ + '\nAbstain <span class="badge badge-pill badge-secondary">0</span><br>\n </p>\n </div>\n</div>\n'
+ self.assertIn(str.encode(testtext), result.data)
+ testtext= '<div class="motion card" id="motion-1">\n <div class="motion-title card-heading alert-success">'\
+ + '\n <span class="title-text">Motion A</span> (Finished)\n <span class="motion-type">group1</span>'\
+ '\n <div># g1.20200402.001'\
+ '\n <a class="btn btn-primary" href="/motion/g1.20200402.001" role="button">Result</a>'\
- + '\n </div>'\
- + '\n <div class=\"date">\n <div>Proposed: 2020-04-02 21:40:33 (UTC) by User A</div>'\
- + '\n <div>Votes until: 2020-04-02 21:40:33 (UTC)</div></div>\n </div>'\
- + '\n <div class=\"card-body\">\n <p><p>My special motion</p></p>'\
- + '\n <p>\nYes <span class=\"badge badge-pill badge-secondary\">2</span><br>'\
- + '\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>'
+ + '\n </div>\n <div class="date">\n <div>Proposed: 2020-04-02 21:40:33 (UTC) by User A</div>'\
+ + '\n <div>Votes until: 2020-04-02 21:40:33 (UTC)</div>\n </div>\n </div>'\
+ + '\n <div class="card-body">\n <p><p>My special motion</p></p>\n <p>'\
+ + '\nYes <span class="badge badge-pill badge-secondary">2</span><br>'\
+ + '\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'
self.assertIn(str.encode(testtext), result.data)
testtext= 'Proxy management'
self.assertNotIn(str.encode(testtext), result.data)
result = self.app.get('/', environ_base={'USER_ROLES': user})
resulttext=self.buildResultText('A fourth motion', 1, 0, 0)
result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
- testtext= 'class=\"btn btn-success\" name=\"vote\" value="yes" id="vote-yes">yes</button>'
+ testtext= 'class=\"btn btn-success\" name=\"vote\" value="yes" id="vote-yes">Yes</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"no\" id=\"vote-no\">no</button>'
+ testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"no\" id=\"vote-no\">No</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">abstain</button>'
+ testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">Abstain</button>'
self.assertIn(str.encode(testtext), result.data)
def test_vote_no(self):
resulttext=self.buildResultText('A fourth motion', 0, 1, 0)
self.assertIn(str.encode(resulttext), result.data)
result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
- testtext= 'class="btn btn-primary" name="vote\" value=\"yes\" id=\"vote-yes\">yes</button>'
+ testtext= 'class="btn btn-primary" name="vote\" value=\"yes\" id=\"vote-yes\">Yes</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-success\" name=\"vote\" value=\"no\" id=\"vote-no\">no</button>'
+ testtext= 'class=\"btn btn-success\" name=\"vote\" value=\"no\" id=\"vote-no\">No</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">abstain</button>'
+ testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">Abstain</button>'
self.assertIn(str.encode(testtext), result.data)
def test_vote_abstain(self):
resulttext=self.buildResultText('A fourth motion', 0, 0, 1)
self.assertIn(str.encode(resulttext), result.data)
result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
- testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"yes\" id=\"vote-yes\">yes</button>'
+ testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"yes\" id=\"vote-yes\">Yes</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"no\" id=\"vote-no\">no</button>'
+ testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"no\" id=\"vote-no\">No</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-success\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">abstain</button>'
+ testtext= 'class=\"btn btn-success\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">Abstain</button>'
self.assertIn(str.encode(testtext), result.data)
def test_vote_change(self):
def test_see_old_vote(self):
motion='g1.20200402.002'
result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
- testtext= '<div>Proposed: 2020-04-02 21:41:26 (UTC) by User A</div>\n <div>Votes until: 2020-04-04 21:41:26 (UTC)</div></div>'\
- + '\n </div>\n <div class="card-body">\n <p><p>A second motion</p></p>\n </div>\n</div>'\
+ testtext= '<div>Proposed: 2020-04-02 21:41:26 (UTC) by User A</div>\n <div>Votes until: 2020-04-04 21:41:26 (UTC)</div>'\
+ + '\n </div>\n </div>\n <div class="card-body">\n <p><p>A second motion</p></p>\n </div>\n</div>'\
+ '\n<a href="/?start=2#motion-2" class="btn btn-primary">Back</a>'
self.assertIn(str.encode(testtext), result.data)
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>'
+ + '<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'
+ + '<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
# 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>'
+ + '<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
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'
+ + '<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):
--- /dev/null
+# Translation files for WPIA motion tool
+
+Add the needed translation files here.
+
+The software is translated via Transifex: https://www.transifex.com/wpia/motion/.
+
+The current files are also available in the motion-translations repository on our gitlab system https://git.ccs-baumann.de/wpia/motion-translations.
+
+To extract the translation files use:
+
+```
+pybabel extract -F ./translations/babel.cfg -k _l -o ./translations/messages.pot --input-dirs=.
+```
+
+To create a language file use e.g. de
+
+```
+pybabel init -i ./translations/messages.pot -d translations -l de
+```
+
+The translation files are maintained in a repo and translated on Transifex [https://www.transifex.com/wpia/landingpage-1/dashboard/](https://www.transifex.com/wpia/landingpage-1/dashboard/).
+
+The translation file are stored in this files structure translations/XX/LC_MESSAGES/messages.po with XX as language code.
+
+To compile the translated text from *.po to *.mo use:
+
+```
+pybabel compile -f -d translations
+```
--- /dev/null
+[python: **.py]
+[jinja2: **/templates/**.html]
+encoding = utf-8
+extensions=jinja2.ext.autoescape,jinja2.ext.with_