├── requirements.txt ├── README.md ├── app.py ├── templates └── index.html └── static └── stylesheets └── style.css /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Redis 3 | gunicorn 4 | nose 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vote App Frontend 2 | 3 | This is a frontend app, part of [Example Voting App](https://github.com/schoolofdevops/example-voting-app). 4 | 5 | To build and run this app as a container, 6 | 7 | * use `python:alpine3.17` container base image 8 | * map/expose `container port 80` 9 | * copy over the source code 10 | * run `pip install -r requirements.txt` to install dependencies 11 | * launch the app with `gunicorn app:app -b 0.0.0.0:80` command 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, make_response, g 2 | from redis import Redis 3 | import os 4 | import socket 5 | import random 6 | import json 7 | 8 | option_a = os.getenv('OPTION_A', "Emacs") 9 | option_b = os.getenv('OPTION_B', "Vi") 10 | hostname = socket.gethostname() 11 | version = 'v1' 12 | 13 | app = Flask(__name__) 14 | 15 | def get_redis(): 16 | if not hasattr(g, 'redis'): 17 | g.redis = Redis(host="redis", db=0, socket_timeout=5) 18 | return g.redis 19 | 20 | @app.route("/", methods=['POST','GET']) 21 | def hello(): 22 | voter_id = request.cookies.get('voter_id') 23 | if not voter_id: 24 | voter_id = hex(random.getrandbits(64))[2:-1] 25 | 26 | vote = None 27 | 28 | if request.method == 'POST': 29 | redis = get_redis() 30 | vote = request.form['vote'] 31 | data = json.dumps({'voter_id': voter_id, 'vote': vote}) 32 | redis.rpush('votes', data) 33 | 34 | resp = make_response(render_template( 35 | 'index.html', 36 | option_a=option_a, 37 | option_b=option_b, 38 | hostname=hostname, 39 | vote=vote, 40 | version=version, 41 | )) 42 | resp.set_cookie('voter_id', voter_id) 43 | return resp 44 | 45 | 46 | if __name__ == "__main__": 47 | app.run(host='0.0.0.0', port=80, debug=True, threaded=True) 48 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{option_a}} vs {{option_b}}! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

App Version {{version}}

17 |

{{option_a}} vs {{option_b}}!

18 |
19 | 20 | 21 |
22 |
23 | (Tip: you can change your vote) 24 |
25 |
26 | Processed by container ID {{hostname}} 27 |
28 |
29 |
30 | 31 | 32 | 33 | {% if vote %} 34 | 48 | {% endif %} 49 | 50 | 51 | -------------------------------------------------------------------------------- /static/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | *{ 4 | box-sizing:border-box; 5 | } 6 | html,body{ 7 | margin: 0; 8 | padding: 0; 9 | background-color: #F7F8F9; 10 | height: 100vh; 11 | font-family: 'Open Sans'; 12 | } 13 | 14 | button{ 15 | border-radius: 0; 16 | width: 100%; 17 | height: 50%; 18 | } 19 | 20 | button[type="submit"] { 21 | -webkit-appearance:none; -webkit-border-radius:0; 22 | } 23 | 24 | button i{ 25 | float: right; 26 | padding-right: 30px; 27 | margin-top: 3px; 28 | } 29 | 30 | button.a{ 31 | background-color: #1aaaf8; 32 | } 33 | 34 | button.b{ 35 | background-color: #00cbca; 36 | } 37 | 38 | #tip{ 39 | text-align: left; 40 | color: #c0c9ce; 41 | font-size: 14px; 42 | } 43 | 44 | #hostname{ 45 | position: absolute; 46 | bottom: 100px; 47 | right: 0; 48 | left: 0; 49 | color: #8f9ea8; 50 | font-size: 24px; 51 | } 52 | 53 | #content-container{ 54 | z-index: 2; 55 | position: relative; 56 | margin: 0 auto; 57 | display: table; 58 | padding: 10px; 59 | max-width: 940px; 60 | height: 100%; 61 | } 62 | #content-container-center{ 63 | display: table-cell; 64 | text-align: center; 65 | } 66 | 67 | #content-container-center h3{ 68 | color: #254356; 69 | } 70 | 71 | #choice{ 72 | transition: all 300ms linear; 73 | line-height: 1.3em; 74 | display: inline; 75 | vertical-align: middle; 76 | font-size: 3em; 77 | } 78 | #choice a{ 79 | text-decoration:none; 80 | } 81 | #choice a:hover, #choice a:focus{ 82 | outline:0; 83 | text-decoration:underline; 84 | } 85 | 86 | #choice button{ 87 | display: block; 88 | height: 80px; 89 | width: 330px; 90 | border: none; 91 | color: white; 92 | text-transform: uppercase; 93 | font-size:18px; 94 | font-weight: 700; 95 | margin-top: 10px; 96 | margin-bottom: 10px; 97 | text-align: left; 98 | padding-left: 50px; 99 | } 100 | 101 | #choice button.a:hover{ 102 | background-color: #1488c6; 103 | } 104 | 105 | #choice button.b:hover{ 106 | background-color: #00a2a1; 107 | } 108 | 109 | #choice button.a:focus{ 110 | background-color: #1488c6; 111 | } 112 | 113 | #choice button.b:focus{ 114 | background-color: #00a2a1; 115 | } 116 | 117 | #background-stats{ 118 | z-index:1; 119 | height:100%; 120 | width:100%; 121 | position:absolute; 122 | } 123 | #background-stats div{ 124 | transition: width 400ms ease-in-out; 125 | display:inline-block; 126 | margin-bottom:-4px; 127 | width:50%; 128 | height:100%; 129 | } 130 | --------------------------------------------------------------------------------