-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.py
285 lines (251 loc) · 10.5 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
from flask import abort, escape, Flask, render_template, request, session
from functools import wraps
import json
import sys
import uuid
app = Flask(__name__, static_folder="static")
# Decorators
def requires_admin(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if "team_id" not in session or session["team_id"] != settings["admin_id"]:
abort(404)
return f(*args, **kwargs)
return decorated_function
def requires_team_leader(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if "team_id" not in session:
abort(404)
elif session["team_id"] not in [t["leader"] for t in teams]:
abort(404)
return f(*args, **kwargs)
return decorated_function
def requires_team_member(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if "team_id" not in session:
abort(404)
elif session["team_id"] not in [t["member"] for t in teams]:
abort(404)
return f(*args, **kwargs)
return decorated_function
def requires_team_leader_or_member(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if "team_id" not in session:
abort(404)
elif session["team_id"] not in [t["leader"] for t in teams]:
abort(404)
elif session["team_id"] not in [t["member"] for t in teams]:
abort(404)
return f(*args, **kwargs)
return decorated_function
def requires_login(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if "team_id" not in session:
abort(404)
elif session["team_id"] != settings["admin_id"] and session["team_id"] not in [t["leader"] for t in teams] and session["team_id"] not in [t["member"] for t in teams]:
abort(404)
return f(*args, **kwargs)
return decorated_function
def requires_post(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if request.method == "GET":
abort(405)
return f(*args, **kwargs)
return decorated_function
# Routes
@app.route("/")
def main():
if "team_id" in session:
if session["team_id"] == settings["admin_id"]:
# Administrator, so show admin page
return render_template("admin.html", **settings)
elif session["team_id"] in [t["leader"] for t in teams]:
# Team leader, so show entry page
team_data = [t for t in teams if t["leader"] == session["team_id"]][0]
return render_template("leader.html", **settings, team_data=team_data)
elif session["team_id"] in [t["member"] for t in teams]:
# Team member, so show submitted page
team_data = [t for t in teams if t["member"] == session["team_id"]][0]
return render_template("member.html", **settings, team_data=team_data)
# Not yet part of a team or an admin, so show basic index page
return render_template("index.html", **settings)
@app.route("/api/join", methods=["POST"])
def join():
if "team_id" not in request.form:
return "Team not specified", 400
requested_id = request.form["team_id"].lower()
if requested_id == settings["admin_id"] or requested_id in [t["leader"] for t in teams] or requested_id in [t["member"] for t in teams]:
session["team_id"] = requested_id
return "Team successfully joined", 200
else:
return "Team not found", 404
@app.route("/api/leave")
def leave():
if "team_id" in session:
session.pop("team_id")
return "Team successfully left", 200
else:
return "Already not in a team", 400
@app.route("/api/round/start", methods=["GET", "POST"])
@requires_admin
@requires_post
def round_start():
if quiz["state"] != "preround":
return "Quiz not expecting to start a round", 403
elif "question_count" not in request.form or request.form["question_count"] == "":
return "Question count not specified", 400
else:
try:
question_count = int(request.form["question_count"])
except:
return "Question count not integer", 400
quiz["question_count"] = question_count
quiz["state"] = "answering"
quiz["round_id"] = str(uuid.uuid4())[:8]
for i in range(len(teams)):
teams[i]["submitted"] = False
teams[i]["answers"] = [""] * question_count
return f"Round started with {question_count} question{'s' if question_count != 1 else ''}", 200
@app.route("/api/round/stop", methods=["GET", "POST"])
@requires_admin
@requires_post
def round_stop():
if quiz["state"] != "answering":
return "Quiz not expecting to stop a round", 403
else:
quiz["state"] = "postround"
return "Round stopped", 200
@app.route("/api/round/complete", methods=["GET", "POST"])
@requires_admin
@requires_post
def round_complete():
if quiz["state"] != "postround":
return "Quiz not expecting to complete a round", 403
else:
quiz["state"] = "preround"
return "Round complete, waiting to start a new round", 200
@app.route("/api/status")
@requires_login
def status():
if session["team_id"] == settings["admin_id"]:
if quiz["state"] == "preround":
return {"state": quiz["state"],
"teams": [{"name": t["name"], "leader": t["leader"], "member": t["member"]} for t in teams]}, 200
elif quiz["state"] == "answering":
return {"state": quiz["state"],
"question_count": quiz["question_count"],
"round_id": quiz["round_id"],
"teams": [{"name": t["name"], "leader": t["leader"], "member": t["member"]} for t in teams],
"submitted": [t["name"] for t in teams if t["submitted"]]}, 200
elif quiz["state"] == "postround":
return {"state": quiz["state"],
"question_count": quiz["question_count"],
"round_id": quiz["round_id"],
"teams": [{"name": t["name"], "leader": t["leader"], "member": t["member"]} for t in teams],
"submissions": [{"name": t["name"], "answers": t["answers"]} for t in teams if t["submitted"]]}, 200
else:
return {"state": "invalid",
"teams": [{"name": t["name"], "leader": t["leader"], "member": t["member"]} for t in teams]}, 500
else:
if quiz["state"] == "preround":
return {"state": "preround"}, 200
elif quiz["state"] == "answering":
team_data = [t for t in teams if t["leader"] == session["team_id"] or t["member"] == session["team_id"]][0]
if team_data["submitted"]:
return {"state": "answering",
"question_count": quiz["question_count"],
"round_id": quiz["round_id"],
"submitted": team_data["submitted"],
"answers": team_data["answers"]}, 200
else:
return {"state": "answering",
"question_count": quiz["question_count"],
"round_id": quiz["round_id"],
"submitted": team_data["submitted"]}, 200
elif quiz["state"] == "postround":
team_data = [t for t in teams if t["leader"] == session["team_id"] or t["member"] == session["team_id"]][0]
return {"state": "postround",
"question_count": quiz["question_count"],
"round_id": quiz["round_id"],
"answers": team_data["answers"]}, 200
else:
return {"state": "invalid"}, 500
@app.route("/api/answers.txt")
@requires_admin
def answers():
if quiz["state"] != "postround":
return "Quiz not expecting to return an answers file", 403
else:
return "\n\n".join(
t['name'] + "\n" +
"\n".join(f"{i+1}) {a}" for i, a in enumerate(t['answers']))
for t in teams if t["submitted"]
), 200
@app.route("/api/create", methods=["GET", "POST"])
@requires_admin
@requires_post
def create():
if "team_name" not in request.form or request.form["team_name"] == "":
return "Team name not specified", 400
else:
existing_ids = [t["leader"] for t in teams] + [t["member"] for t in teams]
leader_id = str(uuid.uuid4())[:8]
while leader_id in existing_ids:
leader_id = str(uuid.uuid4())[:8]
member_id = str(uuid.uuid4())[:8]
while member_id in existing_ids:
member_id = str(uuid.uuid4())[:8]
teams.append({"name": escape(request.form["team_name"]),
"leader": leader_id,
"member": member_id,
"submitted": False,
"answers": []})
return {"leader": leader_id, "member": member_id}, 200
@app.route("/api/exportstate")
@requires_admin
def exportstate():
try:
with open("state_data.json", "w") as f:
state = json.dumps([quiz, teams])
f.write(state)
return "State exported successfully", 200
except Exception as e:
return "State failed to export:\n" + str(e), 500
@app.route("/api/submit", methods=["POST"])
@requires_team_leader
def submit():
if quiz["state"] != "answering":
return "Quiz not expecting to accept an answer submission", 403
elif "answers" not in request.form:
return "Answers not specified", 400
t = [i for i, t in enumerate(teams) if t["leader"] == session["team_id"]][0]
teams[t]["submitted"] = True
teams[t]["answers"] = [escape(a) for a in json.loads(request.form["answers"])]
return "Answers submitted successfully", 200
if __name__ == "__main__":
with open("quiz_settings.json") as f:
settings = json.loads(f.read())
if "" in settings.values():
print("All settings require values: please check quiz_settings.json")
exit(1)
if len(sys.argv) > 1:
print("Using predefined state from " + sys.argv[1])
with open(sys.argv[1]) as f:
quiz, teams = json.loads(f.read())
else:
quiz = {"state": "preround", "question_count": 0}
teams = []
app.secret_key = settings["secret_key"]
if settings["https_enabled"]:
if settings["ssl_fullchain"] != None and settings["ssl_privkey"] != None:
print("Running with HTTPS enabled")
app.run(host="0.0.0.0", port=443, ssl_context=(settings["ssl_fullchain"], settings["ssl_privkey"]))
exit()
print("Requested to run with HTTPS enabled, but ssl_fullchain or ssl_privkey settings not provided")
print("Running with HTTPS disabled")
app.run(host="0.0.0.0", port=80)