├── 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 |
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 |
--------------------------------------------------------------------------------