350 lines
11 KiB
Python
350 lines
11 KiB
Python
"""A script for the Sortierhut Game
|
|
|
|
Based on the Flask and WTForms web frameworks.
|
|
"""
|
|
|
|
__version__ = '0.0'
|
|
__author__ = 'aamma, asauer'
|
|
|
|
import uuid
|
|
import csv
|
|
import os
|
|
|
|
from flask import Flask, render_template, request, redirect, url_for
|
|
from wtforms import Form, StringField, SelectField, RadioField, TextAreaField, BooleanField, validators
|
|
|
|
|
|
app = Flask(__name__)
|
|
app.jinja_env.trim_blocks = True
|
|
app.jinja_env.lstrip_blocks = True
|
|
|
|
|
|
datafile = 'data.csv'
|
|
solutionfile = 'solutions.csv'
|
|
|
|
|
|
def create_used_id_list():
|
|
"""Read datafile and create a list of the id in each row.
|
|
|
|
Useful to compare if a given id is already used.
|
|
"""
|
|
data = list()
|
|
if not os.path.exists(datafile):
|
|
open(datafile, 'a').close()
|
|
|
|
with open(datafile, 'r') as f:
|
|
reader = csv.reader(f)
|
|
|
|
for line in reader:
|
|
# line[0] is the row's id
|
|
data.append(line[0])
|
|
|
|
return data
|
|
|
|
|
|
def convert_csv_to_list_of_lists(csv_file):
|
|
"""Convert a csv file to a list whose items are lists.
|
|
|
|
csv_file -- a valid comma-separated file.
|
|
|
|
Useful because a list of lists can be easily iterated.
|
|
"""
|
|
list_of_lists = list()
|
|
|
|
if not os.path.exists(csv_file):
|
|
open(csv_file, 'a').close()
|
|
|
|
with open(csv_file, 'r', newline='') as f:
|
|
reader = csv.reader(f)
|
|
|
|
for line in reader:
|
|
list_of_lists.append(line)
|
|
|
|
return list_of_lists
|
|
|
|
|
|
def check_input(form, field):
|
|
"""Check if a SelectField from the questions() is still in default mode.
|
|
|
|
form -- the form on which this function is called (instance of QuestionForm).
|
|
field -- the form field whose input is checked.
|
|
"""
|
|
if field.data == 'default':
|
|
raise validators.ValidationError('Bitte wähle eine Option aus')
|
|
|
|
|
|
def check_id(form, field):
|
|
"""Check if the datafile contains a given id.
|
|
|
|
form -- the form on which this function is called (instance of AnswerForm).
|
|
field -- the id input field whose input is checked.
|
|
"""
|
|
used_ids = create_used_id_list()
|
|
if field.data not in used_ids:
|
|
raise validators.ValidationError('Leider keine gültige ID')
|
|
|
|
|
|
class QuestionForm(Form):
|
|
"""A form of humorous questions."""
|
|
computers = [
|
|
('default', 'Bitte auswählen'),
|
|
('hal', 'HAL 9000 (2001: Odyssee im Weltraum)'),
|
|
('deepthought', 'Deep Thought (Hitchhiker\'s Guide)'),
|
|
('neuromancer', 'Neuromancer (Neuromancer)'),
|
|
('samantha', 'Samatha (Her)'),
|
|
('tars', 'TARS und CASE (Interstellar)')
|
|
]
|
|
statuses = [
|
|
('default', 'Bitte auswählen'),
|
|
('200', '200 OK'),
|
|
('300', '300 Multiple Choices'),
|
|
('301', '301 Moved Permanently'),
|
|
('402', '402 Payment Required'),
|
|
('404', '404 (Sleep) Not Found'),
|
|
('408', '408 Request Timeout'),
|
|
('418', '418 I am a teapot'),
|
|
('450', '450 Blocked by Windows Parental Controls'),
|
|
('451', '451 Unavailable For Legal Reasons'),
|
|
('502', '502 Bad Gateway (Internetkurort)')
|
|
]
|
|
vegetables = [
|
|
('default', 'Bitte auswählen'),
|
|
('karotte', '🥕 Karotte'),
|
|
('broccoli','🥦 Brokkoli'),
|
|
('aubergine', '🍆 Aubergine'),
|
|
('kartoffel', '🥔 Kartoffel'),
|
|
('bretzel', '🥨 Bretzel'),
|
|
('tomate', '🍅 Tomate'),
|
|
('chili', '🌶️ Chili')
|
|
]
|
|
spirit_animals = [
|
|
('default', 'Bitte auswählen'),
|
|
('feuer', 'Feuerfuchs'),
|
|
('wasser', 'Wasserhahn'),
|
|
('erde', 'Erdferkel'),
|
|
('luft', 'Luftschlange')
|
|
]
|
|
operating_systems = [
|
|
('default', 'Bitte auswählen'),
|
|
('windows', 'Windows'),
|
|
('mac', 'Mac'),
|
|
('linux', 'Linux'),
|
|
('kaffee', 'Kaffee'),
|
|
('glados', 'GLaDOS')
|
|
]
|
|
|
|
computer = SelectField(u'Fiktionaler Lieblingscomputer', choices=computers,
|
|
validators=[validators.InputRequired(), check_input])
|
|
status = SelectField(u'Welcher HTTP-Statuscode trifft am häufigsten auf dich zu?',
|
|
choices=statuses,
|
|
validators=[validators.InputRequired(), check_input])
|
|
vegetable = SelectField(u'Lieblingsgemüse', choices=vegetables,
|
|
validators=[validators.InputRequired(), check_input])
|
|
spirit_animal = SelectField(u'Spirit Animal', choices=spirit_animals,
|
|
validators=[validators.InputRequired(), check_input])
|
|
operating_system = SelectField(u'Bevorzugtes Betriebssystem', choices=operating_systems,
|
|
validators=[validators.InputRequired(), check_input])
|
|
check = BooleanField('Bitte nicht ankreuzen')
|
|
|
|
|
|
class AnswerForm(Form):
|
|
"""A form for a user_id as a sort of password."""
|
|
user_id = StringField('Id', [validators.InputRequired(), check_id])
|
|
|
|
|
|
class SolutionForm(Form):
|
|
"""A form for a solution idea for the Sortierhut riddle."""
|
|
choices = [
|
|
('computer', 'Fiktionaler Lieblingscomputer'),
|
|
('status', 'HTTP-Statuscode'),
|
|
('vegetable', 'Lieblingsgemüse'),
|
|
('spirit_animal', 'Spirit Animal'),
|
|
('operating system', 'Betriebssystem'),
|
|
('check', 'Bitte nicht ankreuzen')
|
|
]
|
|
|
|
user_id = StringField('id', [validators.InputRequired(),])
|
|
question = RadioField(choices=choices, validators=[validators.InputRequired(),])
|
|
solution_text = TextAreaField(u'Textfeld', [validators.InputRequired(),])
|
|
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Display a home page with an explanation and many navigation links."""
|
|
return render_template('index.html', title='Sortierhut')
|
|
|
|
|
|
@app.route('/questions', methods=['GET', 'POST'])
|
|
def questions():
|
|
"""Display a form with some questions.
|
|
|
|
Raise ValidationError and stay on questions() page
|
|
if any of the dropdown fields are still in the default position on
|
|
"Bitte auswählen".
|
|
|
|
Else redirect to result().
|
|
"""
|
|
def generate_id():
|
|
"""Generate a new unique user id."""
|
|
data = create_used_id_list()
|
|
# best loop ever
|
|
while True:
|
|
new_id = str(uuid.uuid1()).split('-')[0]
|
|
if new_id in data:
|
|
break
|
|
else:
|
|
return new_id
|
|
|
|
form = QuestionForm(request.form)
|
|
# Assign a number to each house
|
|
houses = dict()
|
|
houses[0] = 'knuth'
|
|
houses[1] = 'lovelace'
|
|
houses[2] = 'turing'
|
|
houses[4] = 'hopper'
|
|
houses[8] = 'lamarr'
|
|
|
|
if request.method == 'POST' and form.validate():
|
|
with open(datafile, 'a', newline='') as f:
|
|
new_row = list()
|
|
new_id = generate_id()
|
|
new_row.append(new_id)
|
|
|
|
for item in request.form:
|
|
new_row.append(request.form[item])
|
|
|
|
# If the BooleanField is not checked,
|
|
# the request.form dict does not contain an entry for 'check'
|
|
if len(new_row) == 6:
|
|
new_row.append('n')
|
|
|
|
# This is the riddle's kern:
|
|
# Compute the mod 10 of the selected http status code
|
|
# and this then determines the sorting
|
|
magical_house_number = int(new_row[2]) % 10
|
|
new_row.append(houses[magical_house_number])
|
|
|
|
writer = csv.writer(f)
|
|
writer.writerow(new_row)
|
|
|
|
return redirect(url_for('result', user_id=new_id))
|
|
|
|
return render_template('questions.html', form=form, title='Sortierhut')
|
|
|
|
|
|
@app.route('/result<user_id>')
|
|
def result(user_id):
|
|
"""Display the user's id and a comment on what to do next.
|
|
|
|
user_id -- argument passed by redirect call in the questions() function.
|
|
|
|
Only called in questions().
|
|
"""
|
|
return render_template('result.html', title='Sortierhut', id=user_id)
|
|
|
|
|
|
@app.route('/answers', methods=['GET', 'POST'])
|
|
def access_answers():
|
|
"""Display a form for an id that is needed to access the corresponded answers.
|
|
|
|
Raise ValidationError and stay on access_answers() page
|
|
if no id is submitted or the id is not valid.
|
|
|
|
Else redirect to view_answers() page.
|
|
"""
|
|
form = AnswerForm(request.form)
|
|
|
|
if request.method == 'POST' and form.validate():
|
|
ident = request.form['user_id']
|
|
return redirect(url_for('view_answers', user_id=ident))
|
|
|
|
return render_template('access_answers.html', form=form, title='Sortierhut')
|
|
|
|
|
|
@app.route('/view_answers<user_id>')
|
|
def view_answers(user_id):
|
|
"""Display the user's answers.
|
|
|
|
user_id -- argument passed by the redirect call in the access_answers() function.
|
|
|
|
Only called in access_answers().
|
|
"""
|
|
# Create list object to be filled with the datafile row matching the user_id
|
|
user_row = list()
|
|
|
|
with open(datafile, 'r', newline='') as f:
|
|
reader = csv.reader(f)
|
|
|
|
for line in reader:
|
|
# line[0] is the row's id
|
|
if line[0] == user_id:
|
|
user_row = line
|
|
break
|
|
|
|
return render_template('view_answers.html', title='Sortierhut',
|
|
user_id=user_id, user_row=user_row)
|
|
|
|
|
|
@app.route('/submit_solution', methods=['GET', 'POST'])
|
|
def submit_solution():
|
|
"""Display a form for solution ideas.
|
|
|
|
Raise ValidationError and stay on submit_solution() page
|
|
if the form input is not valid.
|
|
|
|
Else redirect to thanks() page.
|
|
"""
|
|
form = SolutionForm(request.form)
|
|
|
|
if request.method == 'POST' and form.validate():
|
|
with open(solutionfile, 'a', newline='') as f:
|
|
solution_list = list()
|
|
|
|
# Transfer input data from request.form dict to solution_list list
|
|
for item in request.form:
|
|
solution_list.append(request.form[item])
|
|
|
|
writer = csv.writer(f)
|
|
writer.writerow(solution_list)
|
|
|
|
return redirect(url_for('thanks'))
|
|
|
|
return render_template('submit_solution.html', form=form, title='Sortierhut')
|
|
|
|
|
|
@app.route('/thanks')
|
|
def thanks():
|
|
"""Display how the right solution will be announced.
|
|
|
|
Only called in submit_solution().
|
|
"""
|
|
return render_template('thanks.html', title='Sortierhut')
|
|
|
|
|
|
@app.route('/houses')
|
|
def houses():
|
|
"""Display a description of the 5 groups (=houses)."""
|
|
return render_template('houses.html', title='Sortierhut')
|
|
|
|
|
|
@app.route('/admint_form_fooboar02.104')
|
|
def admin_form():
|
|
"""Display a table of the data from submitted question form."""
|
|
file_data = convert_csv_to_list_of_lists(datafile)
|
|
return render_template('admin_form.html', title='Sortierhut',
|
|
text='Hallo, Admin Fooboar', file_data=file_data)
|
|
|
|
|
|
@app.route('/admint_solution_fooboar02.104')
|
|
def admin_solution():
|
|
"""Display a table of the submitted solutions."""
|
|
file_data = convert_csv_to_list_of_lists(solutionfile)
|
|
return render_template('admin_solution.html', title='Sortierhut', file_data=file_data)
|
|
|
|
|
|
@app.route('/reveal_solution')
|
|
def reveal_solution():
|
|
"""Display the correct solution."""
|
|
return render_template('reveal_solution.html', title='Sortierhut')
|
|
|