import sys
from website.yaml_file import YamlFile
if(sys.version_info.major < 3 or sys.version_info.minor < 6):
    print('Hedy requires Python 3.6 or newer to run. However, your version of Python is', '.'.join([str(sys.version_info.major), str(sys.version_info.minor), str(sys.version_info.micro)]))
    quit()

# coding=utf-8
import datetime
import collections
import hedy
import json
import logging
import os
from os import path
import re
import traceback
import uuid
from ruamel import yaml
from flask_commonmark import Commonmark
from werkzeug.urls import url_encode
from config import config
from website.auth import auth_templates, current_user, requires_login, is_admin, is_teacher
from utils import timems, load_yaml_rt, dump_yaml_rt, version, is_debug_mode
import utils
import textwrap

# app.py
from flask import Flask, request, jsonify, session, abort, g, redirect, Response, make_response
from flask_helpers import render_template
from flask_compress import Compress

# Hedy-specific modules
import hedy_content
import hedyweb
from website import querylog, aws_helpers, jsonbin, translating, ab_proxying, cdn, database

# Set the current directory to the root Hedy folder
os.chdir(os.path.join(os.getcwd(), __file__.replace(os.path.basename(__file__), '')))

# Define and load all available language data
ALL_LANGUAGES = {
    'en': 'English',
    'nl': 'Nederlands',
    'es': 'Español',
    'fr': 'Français',
    'pt_pt': 'Português(pt)',
    'pt_br': 'Português(br)',
    'de': 'Deutsch',
    'it': 'Italiano',
    'sw': 'Swahili',
    'hu': 'Magyar',
    'el': 'Ελληνικά',
    'zh': "简体中文",
    'cs': 'Čeština',
    'bn': 'বাংলা',
    'hi': 'हिंदी',
    'id': 'Bahasa Indonesia',
    'fy': 'Frysk'
}
# Define fall back languages for adventures
FALL_BACK_ADVENTURE = {
    'fy': 'nl',
    'pt_br': 'pt_pt'
}

LEVEL_DEFAULTS = collections.defaultdict(hedy_content.NoSuchDefaults)
for lang in ALL_LANGUAGES.keys():
    LEVEL_DEFAULTS[lang] = hedy_content.LevelDefaults(lang)

ADVENTURES = collections.defaultdict(hedy_content.NoSuchAdventure)
for lang in ALL_LANGUAGES.keys():
    ADVENTURES[lang] = hedy_content.Adventures(lang)

TRANSLATIONS = hedyweb.Translations()

DATABASE = database.Database()

def load_adventure_for_language(lang):
    adventures_for_lang = ADVENTURES[lang]

    if not adventures_for_lang.has_adventures():
        # The default fall back language is English
        fall_back = FALL_BACK_ADVENTURE.get(lang, "en")        
        adventures_for_lang = ADVENTURES[fall_back]
    return adventures_for_lang.adventures_file['adventures']

def load_adventures_per_level(lang, level):

    loaded_programs = {}
    # If user is logged in, we iterate their programs that belong to the current level. Out of these, we keep the latest created program for both the level mode(no adventure) and for each of the adventures.
    if current_user(request)['username']:
        user_programs = DATABASE.programs_for_user(current_user(request)['username'])
        for program in user_programs:
            if program['level'] != level:
                continue
            program_key = 'level' if not program.get('adventure_name') else program['adventure_name']
            if not program_key in loaded_programs:
                loaded_programs[program_key] = program
            elif loaded_programs[program_key]['date'] < program['date']:
                loaded_programs[program_key] = program

    all_adventures =[]

    adventures = load_adventure_for_language(lang)

    for short_name, adventure in adventures.items():
        if not level in adventure['levels']:
            continue
        # end adventure is the quiz
        # if quizzes are not enabled, do not load it
        if short_name == 'end' and not config['quiz-enabled']:
            continue
        all_adventures.append({
            'short_name': short_name,
            'name': adventure['name'],
            'image': adventure.get('image', None),
            'default_save_name': adventure['default_save_name'],
            'text': adventure['levels'][level].get('story_text', 'No Story Text'),
            'start_code': adventure['levels'][level].get('start_code', ''),
            'loaded_program': '' if not loaded_programs.get(short_name) else {
                'name': loaded_programs.get(short_name)['name'],
                'code': loaded_programs.get(short_name)['code']
             }
        })
    # We create a 'level' pseudo assignment to store the loaded program for level mode, if any.
    all_adventures.append({
        'short_name': 'level',
        'loaded_program': '' if not loaded_programs.get('level') else {
            'name': loaded_programs.get('level')['name'],
            'code': loaded_programs.get('level')['code']
         }
    })
    return all_adventures

# Load main menu(do it once, can be cached)
with open(f'main/menu.json', 'r', encoding='utf-8') as f:
    main_menu_json = json.load(f)

logging.basicConfig(
    level=logging.DEBUG,
    format='[%(asctime)s] %(levelname)-8s: %(message)s')


app = Flask(__name__, static_url_path='')
# Ignore trailing slashes in URLs
app.url_map.strict_slashes = False

cdn.Cdn(app, os.getenv('CDN_PREFIX'), os.getenv('HEROKU_SLUG_COMMIT', 'dev'))

# Set session id if not already set. This must be done as one of the first things,
# so the function should be defined high up.
@app.before_request
def set_session_cookie():
    session_id()

if os.getenv('IS_PRODUCTION'):
    @app.before_request
    def reject_e2e_requests():
        if utils.is_testing_request(request):
            return 'No E2E tests are allowed in production', 400

@app.before_request
def before_request_proxy_testing():
    if utils.is_testing_request(request):
        if os.getenv('IS_TEST_ENV'):
            session['test_session'] = 'test'

# HTTP -> HTTPS redirect
# https://stackoverflow.com/questions/32237379/python-flask-redirect-to-https-from-http/32238093
if os.getenv('REDIRECT_HTTP_TO_HTTPS'):
    @app.before_request
    def before_request_https():
        if request.url.startswith('http://'):
            url = request.url.replace('http://', 'https://', 1)
            # We use a 302 in case we need to revert the redirect.
            return redirect(url, code=302)

# Unique random key for sessions.
# For settings with multiple workers, an environment variable is required, otherwise cookies will be constantly removed and re-set by different workers.
if utils.is_production():
    if not os.getenv('SECRET_KEY'):
        raise RuntimeError('The SECRET KEY must be provided for non-dev environments.')

    app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')

else:
    app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', uuid.uuid4().hex)

if utils.is_heroku():
    app.config.update(
        SESSION_COOKIE_SECURE=True,
        SESSION_COOKIE_HTTPONLY=True,
        SESSION_COOKIE_SAMESITE='Lax',
    )

# Set security attributes for cookies in a central place - but not when running locally, so that session cookies work well without HTTPS

Compress(app)
Commonmark(app)
parse_logger = jsonbin.MultiParseLogger(
    jsonbin.JsonBinLogger.from_env_vars(),
    jsonbin.S3ParseLogger.from_env_vars())
querylog.LOG_QUEUE.set_transmitter(aws_helpers.s3_querylog_transmitter_from_env())

# Check that requested language is supported, otherwise return 404
@app.before_request
def check_language():
    if requested_lang() not in ALL_LANGUAGES.keys():
        return "Language " + requested_lang() + " not supported", 404

if utils.is_heroku() and not os.getenv('HEROKU_RELEASE_CREATED_AT'):
    logging.warning('Cannot determine release; enable Dyno metadata by running "heroku labs:enable runtime-dyno-metadata -a <APP_NAME>"')


@app.before_request
def before_request_begin_logging():
    querylog.begin_global_log_record(path=request.path, method=request.method)

@app.after_request
def after_request_log_status(response):
    querylog.log_value(http_code=response.status_code)
    return response

@app.after_request
def set_security_headers(response):
    security_headers = {
        'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
        'X-Frame-Options': None if re.match('.*/quiz', request.url) else 'DENY',
        'X-XSS-Protection': '1; mode=block',
    }
    response.headers.update(security_headers)
    return response

@app.teardown_request
def teardown_request_finish_logging(exc):
    querylog.finish_global_log_record(exc)

# If present, PROXY_TO_TEST_HOST should be the 'http[s]://hostname[:port]' of the target environment
if os.getenv('PROXY_TO_TEST_HOST') and not os.getenv('IS_TEST_ENV'):
    ab_proxying.ABProxying(app, os.getenv('PROXY_TO_TEST_HOST'), app.config['SECRET_KEY'])

@app.route('/session_test', methods=['GET'])
def echo_session_vars_test():
    if not utils.is_testing_request(request):
        return 'This endpoint is only meant for E2E tests', 400
    return jsonify({'session': dict(session)})

@app.route('/session_main', methods=['GET'])
def echo_session_vars_main():
    if not utils.is_testing_request(request):
        return 'This endpoint is only meant for E2E tests', 400
    return jsonify({'session': dict(session), 'proxy_enabled': bool(os.getenv('PROXY_TO_TEST_HOST'))})

@app.route('/parse', methods=['POST'])
def parse():
    body = request.json
    if not body:
        return "body must be an object", 400
    if 'code' not in body:
        return "body.code must be a string", 400
    if 'level' not in body:
        return "body.level must be a string", 400
    if 'adventure_name' in body and not isinstance(body['adventure_name'], str):
        return "if present, body.adventure_name must be a string", 400

    code = body['code']
    level = int(body['level'])

    # Language should come principally from the request body,
    # but we'll fall back to browser default if it's missing for whatever
    # reason.
    lang = body.get('lang', requested_lang())

    # true if kid enabled the read aloud option
    read_aloud = body.get('read_aloud', False)

    response = {}
    username = current_user(request)['username'] or None

    querylog.log_value(level=level, lang=lang, session_id=session_id(), username=username)

    try:
        hedy_errors = TRANSLATIONS.get_translations(lang, 'HedyErrorMessages')
        with querylog.log_time('transpile'):
            transpile_result = hedy.transpile(code, level)
            python_code = transpile_result.code
            has_turtle = transpile_result.has_turtle

        response['has_turtle'] = has_turtle
        if has_turtle:
            response["Code"] = textwrap.dedent("""\
            # coding=utf8
            import random, time, turtle
            t = turtle.Turtle()
            t.hideturtle()
            t.speed(0)
            t.penup()
            t.goto(50,100)
            t.showturtle()
            t.pendown()
            t.speed(3)
            """) + python_code
        else:
            response["Code"] = "# coding=utf8\nimport random\n" + python_code

    except hedy.InvalidSpaceException as ex:
        traceback.print_exc()
        response = invalid_space_error_to_response(ex, hedy_errors)
    except hedy.ParseException as ex:
        traceback.print_exc()
        response = parse_error_to_response(ex, hedy_errors)
    except hedy.HedyException as ex:
        traceback.print_exc()
        response = hedy_error_to_response(ex, hedy_errors)

    except Exception as E:
        traceback.print_exc()
        print(f"error transpiling {code}")
        response["Error"] = str(E)
    querylog.log_value(server_error=response.get('Error'))
    parse_logger.log({
        'session': session_id(),
        'date': str(datetime.datetime.now()),
        'level': level,
        'lang': lang,
        'code': code,
        'server_error': response.get('Error'),
        'version': version(),
        'username': username,
        'read_aloud': read_aloud,
        'is_test': 1 if os.getenv('IS_TEST_ENV') else None,
        'adventure_name': body.get('adventure_name', None)
    })

    return jsonify(response)

def invalid_space_error_to_response(ex, translations):
    warning = translate_error(ex.error_code, translations, vars(ex))
    code = "# coding=utf8\n" + ex.fixed_code
    return {"Code": code, "Warning": warning}

def parse_error_to_response(ex, translations):
    if ex.character_found is not None:
        # Localize the names of characters. If we can't do that, just show the original character.
        ex.character_found = translations.get(ex.character_found, ex.character_found)
    elif ex.keyword_found is not None:
        # If we find an invalid keyword, place it in the same location in the error message but without translating
        ex.character_found = ex.keyword_found
    error_message = translate_error(ex.error_code, translations, vars(ex))
    location = ex.location if hasattr(ex, "location") else None
    return {"Error": error_message, "Location": location}

def hedy_error_to_response(ex, translations):
    error_message = translate_error(ex.error_code, translations, ex.arguments)
    location = ex.location if hasattr(ex, "location") else None
    return {"Error": error_message, "Location": location}

def translate_error(code, translations, arguments):
    error_template = translations[code]
    return error_template.format(**arguments)

@app.route('/report_error', methods=['POST'])
def report_error():
    post_body = request.json

    parse_logger.log({
        'session': session_id(),
        'date': str(datetime.datetime.now()),
        'level': post_body.get('level'),
        'code': post_body.get('code'),
        'client_error': post_body.get('client_error'),
        'version': version(),
        'username': current_user(request)['username'] or None,
        'is_test': 1 if os.getenv('IS_TEST_ENV') else None
    })

    return 'logged'

@app.route('/client_exception', methods=['POST'])
def report_client_exception():
    post_body = request.json

    querylog.log_value(
        session=session_id(),
        date=str(datetime.datetime.now()),
        client_error=post_body,
        version=version(),
        username=current_user(request)['username'] or None,
        is_test=1 if os.getenv('IS_TEST_ENV') else None
    )

    # Return a 500 so the HTTP status codes will stand out in our monitoring/logging
    return 'logged', 500

@app.route('/version', methods=['GET'])
def version_page():
    """
    Generate a page with some diagnostic information and a useful GitHub URL on upcoming changes.

    This is an admin-only page, it does not need to be linked.
   (Also does not have any sensitive information so it's fine to be unauthenticated).
    """
    app_name = os.getenv('HEROKU_APP_NAME')

    vrz = os.getenv('HEROKU_RELEASE_CREATED_AT')
    the_date = datetime.date.fromisoformat(vrz[:10]) if vrz else datetime.date.today()

    commit = os.getenv('HEROKU_SLUG_COMMIT', '????')[0:6]

    return render_template('version-page.html',
        app_name=app_name,
        heroku_release_time=the_date,
        commit=commit)


def programs_page(request):
    username = current_user(request)['username']
    if not username:
        # redirect users to /login if they are not logged in
        url = request.url.replace('/programs', '/login')
        return redirect(url, code=302)

    from_user = request.args.get('user') or None
    if from_user and not is_admin(request):
        if not is_teacher(request):
            return "unauthorized", 403
        students = DATABASE.get_teacher_students(username)
        if from_user not in students:
            return "unauthorized", 403

    texts=TRANSLATIONS.get_translations(requested_lang(), 'Programs')
    ui=TRANSLATIONS.get_translations(requested_lang(), 'ui')
    adventures = load_adventure_for_language(requested_lang())

    result = DATABASE.programs_for_user(from_user or username)
    programs =[]
    now = timems()
    for item in result:
        program_age = now - item['date']
        if program_age < 1000 * 60 * 60:
            measure = texts['minutes']
            date = round(program_age /(1000 * 60))
        elif program_age < 1000 * 60 * 60 * 24:
            measure = texts['hours']
            date = round(program_age /(1000 * 60 * 60))
        else:
            measure = texts['days']
            date = round(program_age /(1000 * 60 * 60 * 24))

        programs.append({'id': item['id'], 'code': item['code'], 'date': texts['ago-1'] + ' ' + str(date) + ' ' + measure + ' ' + texts['ago-2'], 'level': item['level'], 'name': item['name'], 'adventure_name': item.get('adventure_name'), 'public': item.get('public')})

    return render_template('programs.html', lang=requested_lang(), menu=render_main_menu('programs'), texts=texts, ui=ui, auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'), programs=programs, username=username, is_teacher=is_teacher(request), current_page='programs', from_user=from_user, adventures=adventures)

@app.route('/quiz/start/<level>', methods=['GET'])
def get_quiz_start(level):
    if not config.get('quiz-enabled') and g.lang != 'nl':
        return utils.page_404 (TRANSLATIONS, render_main_menu('adventures'), current_user(request) ['username'], requested_lang (), 'Hedy quiz disabled!')
    else:
        g.lang = lang = requested_lang()
        g.prefix = '/hedy'

         #Sets the values of total_score and correct on the beginning of the quiz at 0
        session['total_score'] = 0
        session['correct_answer'] = 0
        return render_template('startquiz.html', level=level, next_assignment=1, menu=render_main_menu('adventures'),
                               lang=lang,
                               username=current_user(request)['username'], is_teacher=is_teacher(request),
                               auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'))


def quiz_data_file_for(level):
    return YamlFile.for_file(f'coursedata/quiz/quiz_questions_lvl{level}.yaml')

# Quiz mode
# Fill in the filename as source
@app.route('/quiz/quiz_questions/<level_source>/<question_nr>/<attempt>', methods=['GET'])
def get_quiz(level_source, question_nr, attempt):
    if not config.get('quiz-enabled') and g.lang != 'nl':
        return utils.page_404 (TRANSLATIONS, render_main_menu('adventures'), current_user(request) ['username'], requested_lang (), 'Hedy quiz disabled!')
    else:
        # Reading the yaml file
        quiz_data = quiz_data_file_for(level_source)
        if not quiz_data.exists():
            return 'No quiz yaml file found for this level', 404

        # set globals
        g.lang = lang = requested_lang()
        g.prefix = '/hedy'

        # Loop through the questions and check that the loop doesn't reach out of bounds
        q_nr = int(question_nr)

        if int(attempt) == 1:
            questionStatus = 'start'

        if q_nr <= len(quiz_data['questions']):
            question = quiz_data['questions'][q_nr - 1].get(q_nr)

            # Convert the indices to the corresponding characters
            char_array =[]
            for i in range(len(question['mp_choice_options'])):
                char_array.append(chr(ord('@') + (i + 1)))

            i = 0
            question_obj = []
            for options in question['mp_choice_options']:
                option_obj = {}
                for options_key, options_value in options.items():
                    for option in options_value:
                        for key, value in option.items():
                            if value:
                                option_obj[key] = value.replace("\n", '\\n')
                            option_obj['char_index'] = char_array[i]
                    i += 1
                question_obj.append(option_obj)
                
            html_obj = render_template('quiz_question.html',
                                   quiz=quiz_data,
                                   level_source=level_source,
                                   questionStatus= questionStatus,
                                   questions=quiz_data['questions'],
                                   question_options=question_obj,
                                   question=quiz_data['questions'][q_nr - 1].get(q_nr),
                                   question_nr=q_nr,
                                   correct=session.get('correct_answer'),
                                   attempt = attempt,
                                   char_array=char_array,
                                   menu=render_main_menu('adventures'), lang=lang,
                                   username=current_user(request)['username'],
                                   is_teacher=is_teacher(request),
                                   auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'))
            return html_obj.replace("\\n", '<br />')
        else:
            return render_template('endquiz.html', correct=session.get('correct_answer'),
                                   total_score=session.get('total_score'),
                                   menu=render_main_menu('adventures'), lang=lang,
                                   quiz=quiz_data, level=int(level_source) + 1, questions=quiz_data['questions'],
                                   next_assignment=1, username=current_user(request)['username'],
                                   is_teacher=is_teacher(request),
                                   auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'))

@app.route('/quiz/submit_answer/<level_source>/<question_nr>/<attempt>', methods=["POST"])
def submit_answer(level_source, question_nr, attempt):
    if not config.get('quiz-enabled') and g.lang != 'nl':
        return utils.page_404 (TRANSLATIONS, render_main_menu('adventures'), current_user(request) ['username'], requested_lang (), 'Hedy quiz disabled!')
    else:
        # Get the chosen option from the request form with radio buttons
        chosen_option = request.form["radio_option"]

        # Reading yaml file
        quiz_data = quiz_data_file_for(level_source)
        if not quiz_data.exists():
            return 'No quiz yaml file found for this level', 404

        # Convert question_nr to an integer
        q_nr = int(question_nr)

        session['quiz-attempt'] = int(attempt)
        questionStatus = 'false'
        if int(attempt) == 1:
            questionStatus = 'start'
        # Convert the corresponding chosen option to the index of an option
        question = quiz_data['questions'][q_nr - 1].get(q_nr)
        index_option = ord(chosen_option.split("-")[1]) - 65
        session['chosen_option'] =chosen_option.split("-")[1]
        # If the correct answer is chosen, update the total score and the number of correct answered questions
        if question['correct_answer'] in chosen_option:
            if session.get('total_score'):
                session['total_score'] = session.get('total_score') +(config.get('quiz-max-attempts') -  session.get('quiz-attempt')  )* 0.5 * question['question_score']
            else:
                session['total_score'] =(config.get('quiz-max-attempts') - session.get('quiz-attempt')  )* 0.5 * question['question_score']
            if session.get('correct_answer'):
                session['correct_answer'] = session.get('correct_answer') + 1
            else:
                session['correct_answer'] = 1
        # Loop through the questions and check that the loop doesn't reach out of bounds
        q_nr = int(question_nr)
        if q_nr <= len(quiz_data['questions']) :
            question = quiz_data['questions'][q_nr - 1].get(q_nr)
            # Convert the indices to the corresponding characters

            # Convert the indices to the corresponding characters
            char_array = []
            for i in range(len(question['mp_choice_options'])):
                char_array.append(chr(ord('@') + (i + 1)))

            i = 0
            question_obj = []
            for options in question['mp_choice_options']:
                option_obj = {}
                for options_key, options_value in options.items():
                    for option in options_value:
                        for key, value in option.items():
                            if value:
                                option_obj[key] = value.replace("\n", '\\n')
                            option_obj['char_index'] = char_array[i]
                    i += 1
                question_obj.append(option_obj)
            if question['correct_answer'] in chosen_option:
                return render_template('feedback.html', quiz=quiz_data, question=question,
                                       questions=quiz_data['questions'],
                                       question_options=question_obj,
                                       level_source=level_source,
                                       question_nr=q_nr,
                                       correct=session.get('correct_answer'),
                                       option=chosen_option,
                                       index_option=index_option,
                                       menu=render_main_menu('adventures'), lang=lang,
                                       username=current_user(request)['username'],
                                       auth=TRANSLATIONS.data[requested_lang()]['Auth'])
            elif session.get('quiz-attempt')  <= config.get('quiz-max-attempts'):

                html_obj =  render_template('quiz_question.html',
                                       quiz=quiz_data,
                                       level_source=level_source,
                                       questionStatus=questionStatus,
                                       questions=quiz_data['questions'],
                                       question_options=question_obj,
                                       question=quiz_data['questions'][q_nr - 1].get(q_nr),
                                       chosen_option=chosen_option,
                                       question_nr=q_nr,
                                       correct=session.get('correct_answer'),
                                       attempt=attempt,
                                       char_array=char_array,
                                       menu=render_main_menu('adventures'), lang=lang,
                                       username=current_user(request)['username'],
                                       is_teacher=is_teacher(request),
                                       auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'))
                return html_obj.replace("\\n", '<br />')
            elif session.get('quiz-attempt') > config.get('quiz-max-attempts'):
                return render_template('feedback.html',
                                       quiz=quiz_data,
                                       question=question,
                                       question_options=question_obj,
                                       questions=quiz_data['questions'],
                                       level_source=level_source,
                                       question_nr=q_nr,
                                       correct=session.get('correct_answer'),
                                       questionStatus = questionStatus,
                                       option=chosen_option,
                                       index_option=index_option,
                                       menu=render_main_menu('adventures'), lang=lang,
                                       username=current_user(request)['username'],
                                       auth=TRANSLATIONS.data[requested_lang()]['Auth'])
        else:  # show a different page for after the last question
            return 'No end quiz page!', 404

# Adventure mode
@app.route('/hedy/adventures', methods=['GET'])
def adventures_list():
    adventures = load_adventure_for_language(requested_lang())
    menu = render_main_menu('adventures')
    return render_template('adventures.html', lang=lang, adventures=adventures, menu=menu, username=current_user(request)['username'], is_teacher=is_teacher(request), auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'))

@app.route('/hedy/adventures/<adventure_name>', methods=['GET'], defaults={'level': 1})
@app.route('/hedy/adventures/<adventure_name>/<level>', methods=['GET'])
def adventure_page(adventure_name, level):

    user = current_user(request)
    level = int(level)
    adventures = load_adventure_for_language(requested_lang())

    # If requested adventure does not exist, return 404
    if not adventure_name in adventures:
        return utils.page_404 (TRANSLATIONS, render_main_menu('adventures'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_adventure'))

    adventure = adventures[adventure_name]

    # If no level is specified(this will happen if the last element of the path(minus the query parameter) is the same as the adventure_name)
    if re.sub(r'\?.+', '', request.url.split('/')[len(request.url.split('/')) - 1]) == adventure_name:
        # If user is logged in, check if they have a program for this adventure
        # If there are many, note the highest level for which there is a saved program
        desired_level = 0
        if user['username']:
            existing_programs = DATABASE.programs_for_user(user['username'])
            for program in existing_programs:
                if 'adventure_name' in program and program['adventure_name'] == adventure_name and program['level'] > desired_level:
                    desired_level = program['level']
            # If the user has a saved program for this adventure, redirect them to the level with the highest adventure
            if desired_level != 0:
                return redirect(request.url.replace('/' + adventure_name, '/' + adventure_name + '/' + str(desired_level)), code=302)
        # If user is not logged in, or has no saved programs for this adventure, default to the lowest level available for the adventure
        if desired_level == 0:
            for key in adventure['levels'].keys():
                if isinstance(key, int) and(desired_level == 0 or desired_level > key):
                    desired_level = key
        level = desired_level

    # If requested level is not in adventure, return 404
    if not level in adventure['levels']:
        return utils.page_404 (TRANSLATIONS, render_main_menu('adventures'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_adventure_level'))

    adventures_for_level = load_adventures_per_level(requested_lang(), level)
    level_defaults_for_lang = LEVEL_DEFAULTS[requested_lang()]
    defaults = level_defaults_for_lang.get_defaults_for_level(level)
    max_level = level_defaults_for_lang.max_level()

    g.prefix = '/hedy'
    return hedyweb.render_code_editor_with_tabs(
        request=request,
        level_defaults=defaults,
        max_level=max_level,
        lang=requested_lang(),
        level_number=level,
        menu=render_main_menu('hedy'),
        translations=TRANSLATIONS,
        version=version(),
        adventures=adventures_for_level,
        # The relevant loaded program will be available to client-side js and it will be loaded by js.
        loaded_program='',
        adventure_name=adventure_name)

# routing to index.html
@app.route('/ontrack', methods=['GET'], defaults={'level': '1', 'step': 1})
@app.route('/onlinemasters', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/onlinemasters/<int:level>', methods=['GET'], defaults={'step': 1})
@app.route('/space_eu', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/hedy', methods=['GET'], defaults={'level': '1', 'step': 1})
@app.route('/hedy/<level>', methods=['GET'], defaults={'step': 1})
@app.route('/hedy/<level>/<step>', methods=['GET'])
def index(level, step):
    if re.match('\d', level):
        try:
            g.level = level = int(level)
        except:
            return utils.page_404 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_level'))
    else:
        return utils.page_404 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_level'))

    g.lang = requested_lang()
    g.prefix = '/hedy'

    loaded_program = ''
    adventure_name = ''

    # If step is a string that has more than two characters, it must be an id of a program
    if step and isinstance(step, str) and len(step) > 2:
        result = DATABASE.program_by_id(step)
        if not result:
            return utils.page_404 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_program'))
        # If the program is not public, allow only the owner of the program, the admin user and the teacher users to access the program
        user = current_user(request)
        public_program = 'public' in result and result['public']
        if not public_program and user['username'] != result['username'] and not is_admin(request) and not is_teacher(request):
            return utils.page_404 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_program'))
        loaded_program = {'code': result['code'], 'name': result['name'], 'adventure_name': result.get('adventure_name')}
        if 'adventure_name' in result:
            adventure_name = result['adventure_name']

    adventures = load_adventures_per_level(requested_lang(), level)
    level_defaults_for_lang = LEVEL_DEFAULTS[requested_lang()]
    defaults = level_defaults_for_lang.get_defaults_for_level(level)
    max_level = level_defaults_for_lang.max_level()

    return hedyweb.render_code_editor_with_tabs(
        request=request,
        lang=g.lang,
        level_defaults=defaults,
        max_level=max_level,
        level_number=level,
        menu=render_main_menu('hedy'),
        translations=TRANSLATIONS,
        version=version(),
        adventures=adventures,
        loaded_program=loaded_program,
        adventure_name=adventure_name)

@app.route('/hedy/<id>/view', methods=['GET'])
def view_program(id):
    g.lang = requested_lang()
    g.prefix = '/hedy'

    result = DATABASE.program_by_id(id)
    if not result:
        return utils.page_404 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_program'))

    # Default to the language of the program's author(but still respect)
    # the switch if given.
    lang = request.args.get("lang")
    if not lang:
        lang = result['lang']

    arguments_dict = {}
    arguments_dict['program_id'] = id
    arguments_dict['page_title'] = f'{result["name"]} – Hedy'
    arguments_dict['level'] = result['level']  # Necessary for running
    arguments_dict['loaded_program'] = result
    arguments_dict['editor_readonly'] = True
    arguments_dict['show_edit_button'] = True

    # Everything below this line has nothing to do with this page and it's silly
    # that every page needs to put in so much effort to re-set it
    arguments_dict['lang'] = lang
    arguments_dict['menu'] = render_main_menu('view')
    arguments_dict['auth'] = TRANSLATIONS.get_translations(lang, 'Auth')
    arguments_dict['username'] = current_user(request)['username'] or None
    arguments_dict['is_teacher'] = is_teacher(request)
    arguments_dict.update(**TRANSLATIONS.get_translations(lang, 'ui'))

    return render_template("view-program-page.html", **arguments_dict)




@app.route('/client_messages.js', methods=['GET'])
def client_messages():
    error_messages = TRANSLATIONS.get_translations(requested_lang(), "ClientErrorMessages")
    ui_messages = TRANSLATIONS.get_translations(requested_lang(), "ui")
    auth_messages = TRANSLATIONS.get_translations(requested_lang(), "Auth")

    response = make_response(render_template("client_messages.js",
        error_messages=json.dumps(error_messages),
        ui_messages=json.dumps(ui_messages),
        auth_messages=json.dumps(auth_messages)))

    if not is_debug_mode():
        # Cache for longer when not devving
        response.cache_control.max_age = 60 * 60  # Seconds

    return response

@app.errorhandler(500)
def internal_error(exception):
    import traceback
    print(traceback.format_exc())
    return utils.page_500 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang ())

@app.route('/index.html')
@app.route('/')
def default_landing_page():
    return main_page('start')

@app.route('/<page>')
def main_page(page):
    if page == 'favicon.ico':
        abort(404)

    lang = requested_lang()
    effective_lang = lang

    if page in['signup', 'login', 'my-profile', 'recover', 'reset', 'admin']:
        return auth_templates(page, lang, render_main_menu(page), request)

    if page == 'programs':
        return programs_page(request)

    # Default to English if requested language is not available
    if not path.isfile(f'main/{page}-{effective_lang}.md'):
        effective_lang = 'en'

    try:
        with open(f'main/{page}-{effective_lang}.md', 'r', encoding='utf-8') as f:
            contents = f.read()
    except IOError:
        abort(404)

    front_matter, markdown = split_markdown_front_matter(contents)

    menu = render_main_menu(page)
    if page == 'for-teachers':
        teacher_classes =[] if not current_user(request)['username'] else DATABASE.get_teacher_classes(current_user(request)['username'], True)
        return render_template('for-teachers.html', sections=split_teacher_docs(contents), lang=lang, menu=menu, username=current_user(request)['username'], is_teacher=is_teacher(request), auth=TRANSLATIONS.get_translations(lang, 'Auth'), teacher_classes=teacher_classes, **front_matter)

    return render_template('main-page.html', mkd=markdown, lang=lang, menu=menu, username=current_user(request)['username'], is_teacher=is_teacher(request), auth=TRANSLATIONS.get_translations(lang, 'Auth'), **front_matter)


def session_id():
    """Returns or sets the current session ID."""
    if 'session_id' not in session:
        if os.getenv('IS_TEST_ENV') and 'X-session_id' in request.headers:
            session['session_id'] = request.headers['X-session_id']
        else:
            session['session_id'] = uuid.uuid4().hex
    return session['session_id']

def requested_lang():
    """Return the user's requested language code.

    If not in the request parameters, use the browser's accept-languages
    header to do language negotiation.
    """
    lang = request.args.get("lang")
    if lang: return lang

    return request.accept_languages.best_match(ALL_LANGUAGES.keys(), 'en')

@app.template_global()
def current_language():
    return make_lang_obj(requested_lang())

@app.template_global()
def hedy_link(level_nr, assignment_nr, subpage=None, lang=None):
    """Make a link to a Hedy page."""
    parts =[g.prefix]
    parts.append('/' + str(level_nr))
    if str(assignment_nr) != '1' or subpage:
        parts.append('/' + str(assignment_nr if assignment_nr else '1'))
    if subpage and subpage != 'code':
        parts.append('/' + subpage)
    parts.append('?')
    parts.append('lang=' +(lang if lang else requested_lang()))
    return ''.join(parts)

@app.template_global()
def other_languages():
    cl = requested_lang()
    return[make_lang_obj(l) for l in ALL_LANGUAGES.keys() if l != cl]

@app.template_global()
def localize_link(url):
    lang = requested_lang()
    if not lang:
        return url
    if '?' in url:
        return url + '&lang=' + lang
    else:
        return url + '?lang=' + lang

def make_lang_obj(lang):
    """Make a language object for a given language."""
    return {
        'sym': ALL_LANGUAGES[lang],
        'lang': lang
    }


@app.template_global()
def modify_query(**new_values):
    args = request.args.copy()

    for key, value in new_values.items():
        args[key] = value

    return '{}?{}'.format(request.path, url_encode(args))


def no_none_sense(d):
    """Remove all None values from a dict."""
    return {k: v for k, v in d.items() if v is not None}


def split_markdown_front_matter(md):
    parts = re.split('^---', md, 1, re.M)
    if len(parts) == 1:
        return {}, md
    # safe_load returns 'None' if the string is empty
    front_matter = yaml.safe_load(parts[0]) or {}
    if not isinstance(front_matter, dict):
      # There was some kind of parsing error
      return {}, md

    return front_matter, parts[1]

def split_teacher_docs(contents):
    tags = utils.markdown_to_html_tags(contents)
    sections =[]
    for tag in tags:
        # Sections are divided by h2 tags
        if re.match('^<h2>', str(tag)):
            tag = tag.contents[0]
            # We strip `page_title: ` from the first title
            if len(sections) == 0:
                tag = tag.replace('page_title: ', '')
            sections.append({'title': tag, 'content': ''})
        else:
            sections[-1]['content'] += str(tag)

    return sections

def render_main_menu(current_page):
    """Render a list of(caption, href, selected, color) from the main menu."""
    return[dict(
        caption=item.get(requested_lang(), item.get('en', '???')),
        href='/' + item['_'],
        selected=(current_page == item['_']),
        accent_color=item.get('accent_color', 'white'),
        short_name=item['_']
    ) for item in main_menu_json['nav']]

# *** PROGRAMS ***

@app.route('/programs_list', methods=['GET'])
@requires_login
def list_programs(user):
    return {'programs': DATABASE.programs_for_user(user['username'])}

# Not very restful to use a GET to delete something, but indeed convenient; we can do it with a single link and avoiding AJAX.
@app.route('/programs/delete/<program_id>', methods=['GET'])
@requires_login
def delete_program(user, program_id):
    result = DATABASE.program_by_id(program_id)
    if not result or result['username'] != user['username']:
        return "", 404
    DATABASE.delete_program_by_id(program_id)
    DATABASE.increase_user_program_count(user['username'], -1)
    return redirect('/programs')

@app.route('/programs', methods=['POST'])
@requires_login
def save_program(user):

    body = request.json
    if not isinstance(body, dict):
        return 'body must be an object', 400
    if not isinstance(body.get('code'), str):
        return 'code must be a string', 400
    if not isinstance(body.get('name'), str):
        return 'name must be a string', 400
    if not isinstance(body.get('level'), int):
        return 'level must be an integer', 400
    if 'adventure_name' in body:
        if not isinstance(body.get('adventure_name'), str):
            return 'if present, adventure_name must be a string', 400

    # We check if a program with a name `xyz` exists in the database for the username.
    # It'd be ideal to search by username & program name, but since DynamoDB doesn't allow searching for two indexes at the same time, this would require to create a special index to that effect, which is cumbersome.
    # For now, we bring all existing programs for the user and then search within them for repeated names.
    programs = DATABASE.programs_for_user(user['username'])
    program_id = uuid.uuid4().hex
    overwrite = False
    for program in programs:
        if program['name'] == body['name']:
            overwrite = True
            program_id = program['id']
            break

    stored_program = {
        'id': program_id,
        'session': session_id(),
        'date': timems(),
        'lang': requested_lang(),
        'version': version(),
        'level': body['level'],
        'code': body['code'],
        'name': body['name'],
        'username': user['username']
    }

    if 'adventure_name' in body:
        stored_program['adventure_name'] = body['adventure_name']

    DATABASE.store_program(stored_program)
    if not overwrite:
        DATABASE.increase_user_program_count(user['username'])

    return jsonify({'name': body['name'], 'id': program_id})

@app.route('/programs/share', methods=['POST'])
@requires_login
def share_unshare_program(user):
    body = request.json
    if not isinstance(body, dict):
        return 'body must be an object', 400
    if not isinstance(body.get('id'), str):
        return 'id must be a string', 400
    if not isinstance(body.get('public'), bool):
        return 'public must be a string', 400

    result = DATABASE.program_by_id(body['id'])
    if not result or result['username'] != user['username']:
        return 'No such program!', 404

    DATABASE.set_program_public_by_id(body['id'], bool(body['public']))
    return jsonify({'id': body['id']})

@app.route('/translate/<source>/<target>')
def translate_fromto(source, target):
    source_adventures = YamlFile.for_file(f'coursedata/adventures/{source}.yaml').to_dict()
    source_levels = YamlFile.for_file(f'coursedata/level-defaults/{source}.yaml').to_dict()
    source_texts = YamlFile.for_file(f'coursedata/texts/{source}.yaml').to_dict()

    target_adventures = YamlFile.for_file(f'coursedata/adventures/{target}.yaml').to_dict()
    target_levels = YamlFile.for_file(f'coursedata/level-defaults/{target}.yaml').to_dict()
    target_texts = YamlFile.for_file(f'coursedata/texts/{target}.yaml').to_dict()

    files =[]

    files.append(translating.TranslatableFile(
      'Levels',
      f'level-defaults/{target}.yaml',
      translating.struct_to_sections(source_levels, target_levels)))

    files.append(translating.TranslatableFile(
      'Messages',
      f'texts/{target}.yaml',
      translating.struct_to_sections(source_texts, target_texts)))

    files.append(translating.TranslatableFile(
      'Adventures',
      f'adventures/{target}.yaml',
      translating.struct_to_sections(source_adventures, target_adventures)))

    return render_template('translate-fromto.html',
        source_lang=source,
        target_lang=target,
        files=files)

@app.route('/update_yaml', methods=['POST'])
def update_yaml():
    filename = path.join('coursedata', request.form['file'])
    # The file MUST point to something inside our 'coursedata' directory
    #(no exploiting bullshit here)
    filepath = path.abspath(filename)
    expected_path = path.abspath('coursedata')
    if not filepath.startswith(expected_path):
        raise RuntimeError('Are you trying to trick me?')

    data = load_yaml_rt(filepath)
    for key, value in request.form.items():
        if key.startswith('c:'):
            translating.apply_form_change(data, key[2:], translating.normalize_newlines(value))

    data = translating.normalize_yaml_blocks(data)

    return Response(dump_yaml_rt(data),
        mimetype='application/x-yaml',
        headers={'Content-disposition': 'attachment; filename=' + request.form['file'].replace('/', '-')})


# *** AUTH ***

from website import auth
auth.routes(app, DATABASE, requested_lang)

# *** TEACHER BACKEND

from website import teacher
teacher.routes(app, DATABASE, requested_lang)

# *** START SERVER ***

def on_server_start():
    """Called just before the server is started, both in developer mode and on Heroku.

    Use this to initialize objects, dependencies and connections.
    """
    pass


if __name__ == '__main__':
    # Start the server on a developer machine. Flask is initialized in DEBUG mode, so it
    # hot-reloads files. We also flip our own internal "debug mode" flag to True, so our
    # own file loading routines also hot-reload.
    utils.set_debug_mode(not os.getenv('NO_DEBUG_MODE'))

    # If we are running in a Python debugger, don't use flasks reload mode. It creates
    # subprocesses which make debugging harder.
    is_in_debugger = sys.gettrace() is not None

    on_server_start()

    # Threaded option enables multiple instances for multiple user access support
    app.run(threaded=True, debug=not is_in_debugger, port=config['port'], host="0.0.0.0")

    # See `Procfile` for how the server is started on Heroku.

app.py
import sys
from website.yaml_file import YamlFile
if(sys.version_info.major < 3 or sys.version_info.minor < 6):
    print('Hedy requires Python 3.6 or newer to run. However, your version of Python is', '.'.join([str(sys.version_info.major), str(sys.version_info.minor), str(sys.version_info.micro)]))
    quit()

# coding=utf-8
import datetime
import collections
import hedy
import json
import logging
import os
from os import path
import re
import traceback
import uuid
from ruamel import yaml
from flask_commonmark import Commonmark
from werkzeug.urls import url_encode
from config import config
from website.auth import auth_templates, current_user, requires_login, is_admin, is_teacher
from utils import timems, load_yaml_rt, dump_yaml_rt, version, is_debug_mode
import utils
import textwrap

# app.py
from flask import Flask, request, jsonify, session, abort, g, redirect, Response, make_response
from flask_helpers import render_template
from flask_compress import Compress

# Hedy-specific modules
import hedy_content
import hedyweb
from website import querylog, aws_helpers, jsonbin, translating, ab_proxying, cdn, database

# Set the current directory to the root Hedy folder
os.chdir(os.path.join(os.getcwd(), __file__.replace(os.path.basename(__file__), '')))

# Define and load all available language data
ALL_LANGUAGES = {
    'en': 'English',
    'nl': 'Nederlands',
    'es': 'Español',
    'fr': 'Français',
    'pt_pt': 'Português(pt)',
    'pt_br': 'Português(br)',
    'de': 'Deutsch',
    'it': 'Italiano',
    'sw': 'Swahili',
    'hu': 'Magyar',
    'el': 'Ελληνικά',
    'zh': "简体中文",
    'cs': 'Čeština',
    'bn': 'বাংলা',
    'hi': 'हिंदी',
    'id': 'Bahasa Indonesia',
    'fy': 'Frysk'
}
# Define fall back languages for adventures
FALL_BACK_ADVENTURE = {
    'fy': 'nl',
    'pt_br': 'pt_pt'
}

LEVEL_DEFAULTS = collections.defaultdict(hedy_content.NoSuchDefaults)
for lang in ALL_LANGUAGES.keys():
    LEVEL_DEFAULTS[lang] = hedy_content.LevelDefaults(lang)

ADVENTURES = collections.defaultdict(hedy_content.NoSuchAdventure)
for lang in ALL_LANGUAGES.keys():
    ADVENTURES[lang] = hedy_content.Adventures(lang)

TRANSLATIONS = hedyweb.Translations()

DATABASE = database.Database()

def load_adventure_for_language(lang):
    adventures_for_lang = ADVENTURES[lang]

    if not adventures_for_lang.has_adventures():
        # The default fall back language is English
        fall_back = FALL_BACK_ADVENTURE.get(lang, "en")        
        adventures_for_lang = ADVENTURES[fall_back]
    return adventures_for_lang.adventures_file['adventures']

def load_adventures_per_level(lang, level):

    loaded_programs = {}
    # If user is logged in, we iterate their programs that belong to the current level. Out of these, we keep the latest created program for both the level mode(no adventure) and for each of the adventures.
    if current_user(request)['username']:
        user_programs = DATABASE.programs_for_user(current_user(request)['username'])
        for program in user_programs:
            if program['level'] != level:
                continue
            program_key = 'level' if not program.get('adventure_name') else program['adventure_name']
            if not program_key in loaded_programs:
                loaded_programs[program_key] = program
            elif loaded_programs[program_key]['date'] < program['date']:
                loaded_programs[program_key] = program

    all_adventures =[]

    adventures = load_adventure_for_language(lang)

    for short_name, adventure in adventures.items():
        if not level in adventure['levels']:
            continue
        # end adventure is the quiz
        # if quizzes are not enabled, do not load it
        if short_name == 'end' and not config['quiz-enabled']:
            continue
        all_adventures.append({
            'short_name': short_name,
            'name': adventure['name'],
            'image': adventure.get('image', None),
            'default_save_name': adventure['default_save_name'],
            'text': adventure['levels'][level].get('story_text', 'No Story Text'),
            'start_code': adventure['levels'][level].get('start_code', ''),
            'loaded_program': '' if not loaded_programs.get(short_name) else {
                'name': loaded_programs.get(short_name)['name'],
                'code': loaded_programs.get(short_name)['code']
             }
        })
    # We create a 'level' pseudo assignment to store the loaded program for level mode, if any.
    all_adventures.append({
        'short_name': 'level',
        'loaded_program': '' if not loaded_programs.get('level') else {
            'name': loaded_programs.get('level')['name'],
            'code': loaded_programs.get('level')['code']
         }
    })
    return all_adventures

# Load main menu(do it once, can be cached)
with open(f'main/menu.json', 'r', encoding='utf-8') as f:
    main_menu_json = json.load(f)

logging.basicConfig(
    level=logging.DEBUG,
    format='[%(asctime)s] %(levelname)-8s: %(message)s')


app = Flask(__name__, static_url_path='')
# Ignore trailing slashes in URLs
app.url_map.strict_slashes = False

cdn.Cdn(app, os.getenv('CDN_PREFIX'), os.getenv('HEROKU_SLUG_COMMIT', 'dev'))

# Set session id if not already set. This must be done as one of the first things,
# so the function should be defined high up.
@app.before_request
def set_session_cookie():
    session_id()

if os.getenv('IS_PRODUCTION'):
    @app.before_request
    def reject_e2e_requests():
        if utils.is_testing_request(request):
            return 'No E2E tests are allowed in production', 400

@app.before_request
def before_request_proxy_testing():
    if utils.is_testing_request(request):
        if os.getenv('IS_TEST_ENV'):
            session['test_session'] = 'test'

# HTTP -> HTTPS redirect
# https://stackoverflow.com/questions/32237379/python-flask-redirect-to-https-from-http/32238093
if os.getenv('REDIRECT_HTTP_TO_HTTPS'):
    @app.before_request
    def before_request_https():
        if request.url.startswith('http://'):
            url = request.url.replace('http://', 'https://', 1)
            # We use a 302 in case we need to revert the redirect.
            return redirect(url, code=302)

# Unique random key for sessions.
# For settings with multiple workers, an environment variable is required, otherwise cookies will be constantly removed and re-set by different workers.
if utils.is_production():
    if not os.getenv('SECRET_KEY'):
        raise RuntimeError('The SECRET KEY must be provided for non-dev environments.')

    app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')

else:
    app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', uuid.uuid4().hex)

if utils.is_heroku():
    app.config.update(
        SESSION_COOKIE_SECURE=True,
        SESSION_COOKIE_HTTPONLY=True,
        SESSION_COOKIE_SAMESITE='Lax',
    )

# Set security attributes for cookies in a central place - but not when running locally, so that session cookies work well without HTTPS

Compress(app)
Commonmark(app)
parse_logger = jsonbin.MultiParseLogger(
    jsonbin.JsonBinLogger.from_env_vars(),
    jsonbin.S3ParseLogger.from_env_vars())
querylog.LOG_QUEUE.set_transmitter(aws_helpers.s3_querylog_transmitter_from_env())

# Check that requested language is supported, otherwise return 404
@app.before_request
def check_language():
    if requested_lang() not in ALL_LANGUAGES.keys():
        return "Language " + requested_lang() + " not supported", 404

if utils.is_heroku() and not os.getenv('HEROKU_RELEASE_CREATED_AT'):
    logging.warning('Cannot determine release; enable Dyno metadata by running "heroku labs:enable runtime-dyno-metadata -a <APP_NAME>"')


@app.before_request
def before_request_begin_logging():
    querylog.begin_global_log_record(path=request.path, method=request.method)

@app.after_request
def after_request_log_status(response):
    querylog.log_value(http_code=response.status_code)
    return response

@app.after_request
def set_security_headers(response):
    security_headers = {
        'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
        'X-Frame-Options': None if re.match('.*/quiz', request.url) else 'DENY',
        'X-XSS-Protection': '1; mode=block',
    }
    response.headers.update(security_headers)
    return response

@app.teardown_request
def teardown_request_finish_logging(exc):
    querylog.finish_global_log_record(exc)

# If present, PROXY_TO_TEST_HOST should be the 'http[s]://hostname[:port]' of the target environment
if os.getenv('PROXY_TO_TEST_HOST') and not os.getenv('IS_TEST_ENV'):
    ab_proxying.ABProxying(app, os.getenv('PROXY_TO_TEST_HOST'), app.config['SECRET_KEY'])

@app.route('/session_test', methods=['GET'])
def echo_session_vars_test():
    if not utils.is_testing_request(request):
        return 'This endpoint is only meant for E2E tests', 400
    return jsonify({'session': dict(session)})

@app.route('/session_main', methods=['GET'])
def echo_session_vars_main():
    if not utils.is_testing_request(request):
        return 'This endpoint is only meant for E2E tests', 400
    return jsonify({'session': dict(session), 'proxy_enabled': bool(os.getenv('PROXY_TO_TEST_HOST'))})

@app.route('/parse', methods=['POST'])
def parse():
    body = request.json
    if not body:
        return "body must be an object", 400
    if 'code' not in body:
        return "body.code must be a string", 400
    if 'level' not in body:
        return "body.level must be a string", 400
    if 'adventure_name' in body and not isinstance(body['adventure_name'], str):
        return "if present, body.adventure_name must be a string", 400

    code = body['code']
    level = int(body['level'])

    # Language should come principally from the request body,
    # but we'll fall back to browser default if it's missing for whatever
    # reason.
    lang = body.get('lang', requested_lang())

    # true if kid enabled the read aloud option
    read_aloud = body.get('read_aloud', False)

    response = {}
    username = current_user(request)['username'] or None

    querylog.log_value(level=level, lang=lang, session_id=session_id(), username=username)

    try:
        hedy_errors = TRANSLATIONS.get_translations(lang, 'HedyErrorMessages')
        with querylog.log_time('transpile'):
            transpile_result = hedy.transpile(code, level)
            python_code = transpile_result.code
            has_turtle = transpile_result.has_turtle

        response['has_turtle'] = has_turtle
        if has_turtle:
            response["Code"] = textwrap.dedent("""\
            # coding=utf8
            import random, time, turtle
            t = turtle.Turtle()
            t.hideturtle()
            t.speed(0)
            t.penup()
            t.goto(50,100)
            t.showturtle()
            t.pendown()
            t.speed(3)
            """) + python_code
        else:
            response["Code"] = "# coding=utf8\nimport random\n" + python_code

    except hedy.InvalidSpaceException as ex:
        traceback.print_exc()
        response = invalid_space_error_to_response(ex, hedy_errors)
    except hedy.ParseException as ex:
        traceback.print_exc()
        response = parse_error_to_response(ex, hedy_errors)
    except hedy.HedyException as ex:
        traceback.print_exc()
        response = hedy_error_to_response(ex, hedy_errors)

    except Exception as E:
        traceback.print_exc()
        print(f"error transpiling {code}")
        response["Error"] = str(E)
    querylog.log_value(server_error=response.get('Error'))
    parse_logger.log({
        'session': session_id(),
        'date': str(datetime.datetime.now()),
        'level': level,
        'lang': lang,
        'code': code,
        'server_error': response.get('Error'),
        'version': version(),
        'username': username,
        'read_aloud': read_aloud,
        'is_test': 1 if os.getenv('IS_TEST_ENV') else None,
        'adventure_name': body.get('adventure_name', None)
    })

    return jsonify(response)

def invalid_space_error_to_response(ex, translations):
    warning = translate_error(ex.error_code, translations, vars(ex))
    code = "# coding=utf8\n" + ex.fixed_code
    return {"Code": code, "Warning": warning}

def parse_error_to_response(ex, translations):
    if ex.character_found is not None:
        # Localize the names of characters. If we can't do that, just show the original character.
        ex.character_found = translations.get(ex.character_found, ex.character_found)
    elif ex.keyword_found is not None:
        # If we find an invalid keyword, place it in the same location in the error message but without translating
        ex.character_found = ex.keyword_found
    error_message = translate_error(ex.error_code, translations, vars(ex))
    location = ex.location if hasattr(ex, "location") else None
    return {"Error": error_message, "Location": location}

def hedy_error_to_response(ex, translations):
    error_message = translate_error(ex.error_code, translations, ex.arguments)
    location = ex.location if hasattr(ex, "location") else None
    return {"Error": error_message, "Location": location}

def translate_error(code, translations, arguments):
    error_template = translations[code]
    return error_template.format(**arguments)

@app.route('/report_error', methods=['POST'])
def report_error():
    post_body = request.json

    parse_logger.log({
        'session': session_id(),
        'date': str(datetime.datetime.now()),
        'level': post_body.get('level'),
        'code': post_body.get('code'),
        'client_error': post_body.get('client_error'),
        'version': version(),
        'username': current_user(request)['username'] or None,
        'is_test': 1 if os.getenv('IS_TEST_ENV') else None
    })

    return 'logged'

@app.route('/client_exception', methods=['POST'])
def report_client_exception():
    post_body = request.json

    querylog.log_value(
        session=session_id(),
        date=str(datetime.datetime.now()),
        client_error=post_body,
        version=version(),
        username=current_user(request)['username'] or None,
        is_test=1 if os.getenv('IS_TEST_ENV') else None
    )

    # Return a 500 so the HTTP status codes will stand out in our monitoring/logging
    return 'logged', 500

@app.route('/version', methods=['GET'])
def version_page():
    """
    Generate a page with some diagnostic information and a useful GitHub URL on upcoming changes.

    This is an admin-only page, it does not need to be linked.
   (Also does not have any sensitive information so it's fine to be unauthenticated).
    """
    app_name = os.getenv('HEROKU_APP_NAME')

    vrz = os.getenv('HEROKU_RELEASE_CREATED_AT')
    the_date = datetime.date.fromisoformat(vrz[:10]) if vrz else datetime.date.today()

    commit = os.getenv('HEROKU_SLUG_COMMIT', '????')[0:6]

    return render_template('version-page.html',
        app_name=app_name,
        heroku_release_time=the_date,
        commit=commit)


def programs_page(request):
    username = current_user(request)['username']
    if not username:
        # redirect users to /login if they are not logged in
        url = request.url.replace('/programs', '/login')
        return redirect(url, code=302)

    from_user = request.args.get('user') or None
    if from_user and not is_admin(request):
        if not is_teacher(request):
            return "unauthorized", 403
        students = DATABASE.get_teacher_students(username)
        if from_user not in students:
            return "unauthorized", 403

    texts=TRANSLATIONS.get_translations(requested_lang(), 'Programs')
    ui=TRANSLATIONS.get_translations(requested_lang(), 'ui')
    adventures = load_adventure_for_language(requested_lang())

    result = DATABASE.programs_for_user(from_user or username)
    programs =[]
    now = timems()
    for item in result:
        program_age = now - item['date']
        if program_age < 1000 * 60 * 60:
            measure = texts['minutes']
            date = round(program_age /(1000 * 60))
        elif program_age < 1000 * 60 * 60 * 24:
            measure = texts['hours']
            date = round(program_age /(1000 * 60 * 60))
        else:
            measure = texts['days']
            date = round(program_age /(1000 * 60 * 60 * 24))

        programs.append({'id': item['id'], 'code': item['code'], 'date': texts['ago-1'] + ' ' + str(date) + ' ' + measure + ' ' + texts['ago-2'], 'level': item['level'], 'name': item['name'], 'adventure_name': item.get('adventure_name'), 'public': item.get('public')})

    return render_template('programs.html', lang=requested_lang(), menu=render_main_menu('programs'), texts=texts, ui=ui, auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'), programs=programs, username=username, is_teacher=is_teacher(request), current_page='programs', from_user=from_user, adventures=adventures)

@app.route('/quiz/start/<level>', methods=['GET'])
def get_quiz_start(level):
    if not config.get('quiz-enabled') and g.lang != 'nl':
        return utils.page_404 (TRANSLATIONS, render_main_menu('adventures'), current_user(request) ['username'], requested_lang (), 'Hedy quiz disabled!')
    else:
        g.lang = lang = requested_lang()
        g.prefix = '/hedy'

         #Sets the values of total_score and correct on the beginning of the quiz at 0
        session['total_score'] = 0
        session['correct_answer'] = 0
        return render_template('startquiz.html', level=level, next_assignment=1, menu=render_main_menu('adventures'),
                               lang=lang,
                               username=current_user(request)['username'], is_teacher=is_teacher(request),
                               auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'))


def quiz_data_file_for(level):
    return YamlFile.for_file(f'coursedata/quiz/quiz_questions_lvl{level}.yaml')

# Quiz mode
# Fill in the filename as source
@app.route('/quiz/quiz_questions/<level_source>/<question_nr>/<attempt>', methods=['GET'])
def get_quiz(level_source, question_nr, attempt):
    if not config.get('quiz-enabled') and g.lang != 'nl':
        return utils.page_404 (TRANSLATIONS, render_main_menu('adventures'), current_user(request) ['username'], requested_lang (), 'Hedy quiz disabled!')
    else:
        # Reading the yaml file
        quiz_data = quiz_data_file_for(level_source)
        if not quiz_data.exists():
            return 'No quiz yaml file found for this level', 404

        # set globals
        g.lang = lang = requested_lang()
        g.prefix = '/hedy'

        # Loop through the questions and check that the loop doesn't reach out of bounds
        q_nr = int(question_nr)

        if int(attempt) == 1:
            questionStatus = 'start'

        if q_nr <= len(quiz_data['questions']):
            question = quiz_data['questions'][q_nr - 1].get(q_nr)

            # Convert the indices to the corresponding characters
            char_array =[]
            for i in range(len(question['mp_choice_options'])):
                char_array.append(chr(ord('@') + (i + 1)))

            i = 0
            question_obj = []
            for options in question['mp_choice_options']:
                option_obj = {}
                for options_key, options_value in options.items():
                    for option in options_value:
                        for key, value in option.items():
                            if value:
                                option_obj[key] = value.replace("\n", '\\n')
                            option_obj['char_index'] = char_array[i]
                    i += 1
                question_obj.append(option_obj)
                
            html_obj = render_template('quiz_question.html',
                                   quiz=quiz_data,
                                   level_source=level_source,
                                   questionStatus= questionStatus,
                                   questions=quiz_data['questions'],
                                   question_options=question_obj,
                                   question=quiz_data['questions'][q_nr - 1].get(q_nr),
                                   question_nr=q_nr,
                                   correct=session.get('correct_answer'),
                                   attempt = attempt,
                                   char_array=char_array,
                                   menu=render_main_menu('adventures'), lang=lang,
                                   username=current_user(request)['username'],
                                   is_teacher=is_teacher(request),
                                   auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'))
            return html_obj.replace("\\n", '<br />')
        else:
            return render_template('endquiz.html', correct=session.get('correct_answer'),
                                   total_score=session.get('total_score'),
                                   menu=render_main_menu('adventures'), lang=lang,
                                   quiz=quiz_data, level=int(level_source) + 1, questions=quiz_data['questions'],
                                   next_assignment=1, username=current_user(request)['username'],
                                   is_teacher=is_teacher(request),
                                   auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'))

@app.route('/quiz/submit_answer/<level_source>/<question_nr>/<attempt>', methods=["POST"])
def submit_answer(level_source, question_nr, attempt):
    if not config.get('quiz-enabled') and g.lang != 'nl':
        return utils.page_404 (TRANSLATIONS, render_main_menu('adventures'), current_user(request) ['username'], requested_lang (), 'Hedy quiz disabled!')
    else:
        # Get the chosen option from the request form with radio buttons
        chosen_option = request.form["radio_option"]

        # Reading yaml file
        quiz_data = quiz_data_file_for(level_source)
        if not quiz_data.exists():
            return 'No quiz yaml file found for this level', 404

        # Convert question_nr to an integer
        q_nr = int(question_nr)

        session['quiz-attempt'] = int(attempt)
        questionStatus = 'false'
        if int(attempt) == 1:
            questionStatus = 'start'
        # Convert the corresponding chosen option to the index of an option
        question = quiz_data['questions'][q_nr - 1].get(q_nr)
        index_option = ord(chosen_option.split("-")[1]) - 65
        session['chosen_option'] =chosen_option.split("-")[1]
        # If the correct answer is chosen, update the total score and the number of correct answered questions
        if question['correct_answer'] in chosen_option:
            if session.get('total_score'):
                session['total_score'] = session.get('total_score') +(config.get('quiz-max-attempts') -  session.get('quiz-attempt')  )* 0.5 * question['question_score']
            else:
                session['total_score'] =(config.get('quiz-max-attempts') - session.get('quiz-attempt')  )* 0.5 * question['question_score']
            if session.get('correct_answer'):
                session['correct_answer'] = session.get('correct_answer') + 1
            else:
                session['correct_answer'] = 1
        # Loop through the questions and check that the loop doesn't reach out of bounds
        q_nr = int(question_nr)
        if q_nr <= len(quiz_data['questions']) :
            question = quiz_data['questions'][q_nr - 1].get(q_nr)
            # Convert the indices to the corresponding characters

            # Convert the indices to the corresponding characters
            char_array = []
            for i in range(len(question['mp_choice_options'])):
                char_array.append(chr(ord('@') + (i + 1)))

            i = 0
            question_obj = []
            for options in question['mp_choice_options']:
                option_obj = {}
                for options_key, options_value in options.items():
                    for option in options_value:
                        for key, value in option.items():
                            if value:
                                option_obj[key] = value.replace("\n", '\\n')
                            option_obj['char_index'] = char_array[i]
                    i += 1
                question_obj.append(option_obj)
            if question['correct_answer'] in chosen_option:
                return render_template('feedback.html', quiz=quiz_data, question=question,
                                       questions=quiz_data['questions'],
                                       question_options=question_obj,
                                       level_source=level_source,
                                       question_nr=q_nr,
                                       correct=session.get('correct_answer'),
                                       option=chosen_option,
                                       index_option=index_option,
                                       menu=render_main_menu('adventures'), lang=lang,
                                       username=current_user(request)['username'],
                                       auth=TRANSLATIONS.data[requested_lang()]['Auth'])
            elif session.get('quiz-attempt')  <= config.get('quiz-max-attempts'):

                html_obj =  render_template('quiz_question.html',
                                       quiz=quiz_data,
                                       level_source=level_source,
                                       questionStatus=questionStatus,
                                       questions=quiz_data['questions'],
                                       question_options=question_obj,
                                       question=quiz_data['questions'][q_nr - 1].get(q_nr),
                                       chosen_option=chosen_option,
                                       question_nr=q_nr,
                                       correct=session.get('correct_answer'),
                                       attempt=attempt,
                                       char_array=char_array,
                                       menu=render_main_menu('adventures'), lang=lang,
                                       username=current_user(request)['username'],
                                       is_teacher=is_teacher(request),
                                       auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'))
                return html_obj.replace("\\n", '<br />')
            elif session.get('quiz-attempt') > config.get('quiz-max-attempts'):
                return render_template('feedback.html',
                                       quiz=quiz_data,
                                       question=question,
                                       question_options=question_obj,
                                       questions=quiz_data['questions'],
                                       level_source=level_source,
                                       question_nr=q_nr,
                                       correct=session.get('correct_answer'),
                                       questionStatus = questionStatus,
                                       option=chosen_option,
                                       index_option=index_option,
                                       menu=render_main_menu('adventures'), lang=lang,
                                       username=current_user(request)['username'],
                                       auth=TRANSLATIONS.data[requested_lang()]['Auth'])
        else:  # show a different page for after the last question
            return 'No end quiz page!', 404

# Adventure mode
@app.route('/hedy/adventures', methods=['GET'])
def adventures_list():
    adventures = load_adventure_for_language(requested_lang())
    menu = render_main_menu('adventures')
    return render_template('adventures.html', lang=lang, adventures=adventures, menu=menu, username=current_user(request)['username'], is_teacher=is_teacher(request), auth=TRANSLATIONS.get_translations(requested_lang(), 'Auth'))

@app.route('/hedy/adventures/<adventure_name>', methods=['GET'], defaults={'level': 1})
@app.route('/hedy/adventures/<adventure_name>/<level>', methods=['GET'])
def adventure_page(adventure_name, level):

    user = current_user(request)
    level = int(level)
    adventures = load_adventure_for_language(requested_lang())

    # If requested adventure does not exist, return 404
    if not adventure_name in adventures:
        return utils.page_404 (TRANSLATIONS, render_main_menu('adventures'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_adventure'))

    adventure = adventures[adventure_name]

    # If no level is specified(this will happen if the last element of the path(minus the query parameter) is the same as the adventure_name)
    if re.sub(r'\?.+', '', request.url.split('/')[len(request.url.split('/')) - 1]) == adventure_name:
        # If user is logged in, check if they have a program for this adventure
        # If there are many, note the highest level for which there is a saved program
        desired_level = 0
        if user['username']:
            existing_programs = DATABASE.programs_for_user(user['username'])
            for program in existing_programs:
                if 'adventure_name' in program and program['adventure_name'] == adventure_name and program['level'] > desired_level:
                    desired_level = program['level']
            # If the user has a saved program for this adventure, redirect them to the level with the highest adventure
            if desired_level != 0:
                return redirect(request.url.replace('/' + adventure_name, '/' + adventure_name + '/' + str(desired_level)), code=302)
        # If user is not logged in, or has no saved programs for this adventure, default to the lowest level available for the adventure
        if desired_level == 0:
            for key in adventure['levels'].keys():
                if isinstance(key, int) and(desired_level == 0 or desired_level > key):
                    desired_level = key
        level = desired_level

    # If requested level is not in adventure, return 404
    if not level in adventure['levels']:
        return utils.page_404 (TRANSLATIONS, render_main_menu('adventures'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_adventure_level'))

    adventures_for_level = load_adventures_per_level(requested_lang(), level)
    level_defaults_for_lang = LEVEL_DEFAULTS[requested_lang()]
    defaults = level_defaults_for_lang.get_defaults_for_level(level)
    max_level = level_defaults_for_lang.max_level()

    g.prefix = '/hedy'
    return hedyweb.render_code_editor_with_tabs(
        request=request,
        level_defaults=defaults,
        max_level=max_level,
        lang=requested_lang(),
        level_number=level,
        menu=render_main_menu('hedy'),
        translations=TRANSLATIONS,
        version=version(),
        adventures=adventures_for_level,
        # The relevant loaded program will be available to client-side js and it will be loaded by js.
        loaded_program='',
        adventure_name=adventure_name)

# routing to index.html
@app.route('/ontrack', methods=['GET'], defaults={'level': '1', 'step': 1})
@app.route('/onlinemasters', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/onlinemasters/<int:level>', methods=['GET'], defaults={'step': 1})
@app.route('/space_eu', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/hedy', methods=['GET'], defaults={'level': '1', 'step': 1})
@app.route('/hedy/<level>', methods=['GET'], defaults={'step': 1})
@app.route('/hedy/<level>/<step>', methods=['GET'])
def index(level, step):
    if re.match('\d', level):
        try:
            g.level = level = int(level)
        except:
            return utils.page_404 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_level'))
    else:
        return utils.page_404 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_level'))

    g.lang = requested_lang()
    g.prefix = '/hedy'

    loaded_program = ''
    adventure_name = ''

    # If step is a string that has more than two characters, it must be an id of a program
    if step and isinstance(step, str) and len(step) > 2:
        result = DATABASE.program_by_id(step)
        if not result:
            return utils.page_404 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_program'))
        # If the program is not public, allow only the owner of the program, the admin user and the teacher users to access the program
        user = current_user(request)
        public_program = 'public' in result and result['public']
        if not public_program and user['username'] != result['username'] and not is_admin(request) and not is_teacher(request):
            return utils.page_404 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_program'))
        loaded_program = {'code': result['code'], 'name': result['name'], 'adventure_name': result.get('adventure_name')}
        if 'adventure_name' in result:
            adventure_name = result['adventure_name']

    adventures = load_adventures_per_level(requested_lang(), level)
    level_defaults_for_lang = LEVEL_DEFAULTS[requested_lang()]
    defaults = level_defaults_for_lang.get_defaults_for_level(level)
    max_level = level_defaults_for_lang.max_level()

    return hedyweb.render_code_editor_with_tabs(
        request=request,
        lang=g.lang,
        level_defaults=defaults,
        max_level=max_level,
        level_number=level,
        menu=render_main_menu('hedy'),
        translations=TRANSLATIONS,
        version=version(),
        adventures=adventures,
        loaded_program=loaded_program,
        adventure_name=adventure_name)

@app.route('/hedy/<id>/view', methods=['GET'])
def view_program(id):
    g.lang = requested_lang()
    g.prefix = '/hedy'

    result = DATABASE.program_by_id(id)
    if not result:
        return utils.page_404 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang (), TRANSLATIONS.get_translations (requested_lang (), 'ui').get ('no_such_program'))

    # Default to the language of the program's author(but still respect)
    # the switch if given.
    lang = request.args.get("lang")
    if not lang:
        lang = result['lang']

    arguments_dict = {}
    arguments_dict['program_id'] = id
    arguments_dict['page_title'] = f'{result["name"]} – Hedy'
    arguments_dict['level'] = result['level']  # Necessary for running
    arguments_dict['loaded_program'] = result
    arguments_dict['editor_readonly'] = True
    arguments_dict['show_edit_button'] = True

    # Everything below this line has nothing to do with this page and it's silly
    # that every page needs to put in so much effort to re-set it
    arguments_dict['lang'] = lang
    arguments_dict['menu'] = render_main_menu('view')
    arguments_dict['auth'] = TRANSLATIONS.get_translations(lang, 'Auth')
    arguments_dict['username'] = current_user(request)['username'] or None
    arguments_dict['is_teacher'] = is_teacher(request)
    arguments_dict.update(**TRANSLATIONS.get_translations(lang, 'ui'))

    return render_template("view-program-page.html", **arguments_dict)




@app.route('/client_messages.js', methods=['GET'])
def client_messages():
    error_messages = TRANSLATIONS.get_translations(requested_lang(), "ClientErrorMessages")
    ui_messages = TRANSLATIONS.get_translations(requested_lang(), "ui")
    auth_messages = TRANSLATIONS.get_translations(requested_lang(), "Auth")

    response = make_response(render_template("client_messages.js",
        error_messages=json.dumps(error_messages),
        ui_messages=json.dumps(ui_messages),
        auth_messages=json.dumps(auth_messages)))

    if not is_debug_mode():
        # Cache for longer when not devving
        response.cache_control.max_age = 60 * 60  # Seconds

    return response

@app.errorhandler(500)
def internal_error(exception):
    import traceback
    print(traceback.format_exc())
    return utils.page_500 (TRANSLATIONS, render_main_menu('hedy'), current_user(request) ['username'], requested_lang ())

@app.route('/index.html')
@app.route('/')
def default_landing_page():
    return main_page('start')

@app.route('/<page>')
def main_page(page):
    if page == 'favicon.ico':
        abort(404)

    lang = requested_lang()
    effective_lang = lang

    if page in['signup', 'login', 'my-profile', 'recover', 'reset', 'admin']:
        return auth_templates(page, lang, render_main_menu(page), request)

    if page == 'programs':
        return programs_page(request)

    # Default to English if requested language is not available
    if not path.isfile(f'main/{page}-{effective_lang}.md'):
        effective_lang = 'en'

    try:
        with open(f'main/{page}-{effective_lang}.md', 'r', encoding='utf-8') as f:
            contents = f.read()
    except IOError:
        abort(404)

    front_matter, markdown = split_markdown_front_matter(contents)

    menu = render_main_menu(page)
    if page == 'for-teachers':
        teacher_classes =[] if not current_user(request)['username'] else DATABASE.get_teacher_classes(current_user(request)['username'], True)
        return render_template('for-teachers.html', sections=split_teacher_docs(contents), lang=lang, menu=menu, username=current_user(request)['username'], is_teacher=is_teacher(request), auth=TRANSLATIONS.get_translations(lang, 'Auth'), teacher_classes=teacher_classes, **front_matter)

    return render_template('main-page.html', mkd=markdown, lang=lang, menu=menu, username=current_user(request)['username'], is_teacher=is_teacher(request), auth=TRANSLATIONS.get_translations(lang, 'Auth'), **front_matter)


def session_id():
    """Returns or sets the current session ID."""
    if 'session_id' not in session:
        if os.getenv('IS_TEST_ENV') and 'X-session_id' in request.headers:
            session['session_id'] = request.headers['X-session_id']
        else:
            session['session_id'] = uuid.uuid4().hex
    return session['session_id']

def requested_lang():
    """Return the user's requested language code.

    If not in the request parameters, use the browser's accept-languages
    header to do language negotiation.
    """
    lang = request.args.get("lang")
    if lang: return lang

    return request.accept_languages.best_match(ALL_LANGUAGES.keys(), 'en')

@app.template_global()
def current_language():
    return make_lang_obj(requested_lang())

@app.template_global()
def hedy_link(level_nr, assignment_nr, subpage=None, lang=None):
    """Make a link to a Hedy page."""
    parts =[g.prefix]
    parts.append('/' + str(level_nr))
    if str(assignment_nr) != '1' or subpage:
        parts.append('/' + str(assignment_nr if assignment_nr else '1'))
    if subpage and subpage != 'code':
        parts.append('/' + subpage)
    parts.append('?')
    parts.append('lang=' +(lang if lang else requested_lang()))
    return ''.join(parts)

@app.template_global()
def other_languages():
    cl = requested_lang()
    return[make_lang_obj(l) for l in ALL_LANGUAGES.keys() if l != cl]

@app.template_global()
def localize_link(url):
    lang = requested_lang()
    if not lang:
        return url
    if '?' in url:
        return url + '&lang=' + lang
    else:
        return url + '?lang=' + lang

def make_lang_obj(lang):
    """Make a language object for a given language."""
    return {
        'sym': ALL_LANGUAGES[lang],
        'lang': lang
    }


@app.template_global()
def modify_query(**new_values):
    args = request.args.copy()

    for key, value in new_values.items():
        args[key] = value

    return '{}?{}'.format(request.path, url_encode(args))


def no_none_sense(d):
    """Remove all None values from a dict."""
    return {k: v for k, v in d.items() if v is not None}


def split_markdown_front_matter(md):
    parts = re.split('^---', md, 1, re.M)
    if len(parts) == 1:
        return {}, md
    # safe_load returns 'None' if the string is empty
    front_matter = yaml.safe_load(parts[0]) or {}
    if not isinstance(front_matter, dict):
      # There was some kind of parsing error
      return {}, md

    return front_matter, parts[1]

def split_teacher_docs(contents):
    tags = utils.markdown_to_html_tags(contents)
    sections =[]
    for tag in tags:
        # Sections are divided by h2 tags
        if re.match('^<h2>', str(tag)):
            tag = tag.contents[0]
            # We strip `page_title: ` from the first title
            if len(sections) == 0:
                tag = tag.replace('page_title: ', '')
            sections.append({'title': tag, 'content': ''})
        else:
            sections[-1]['content'] += str(tag)
    return sections

def render_main_menu(current_page):
    """Render a list of(caption, href, selected, color) from the main menu."""
    return[dict(
        caption=item.get(requested_lang(), item.get('en', '???')),
        href='/' + item['_'],
        selected=(current_page == item['_']),
        accent_color=item.get('accent_color', 'white'),
        short_name=item['_']
    ) for item in main_menu_json['nav']]

# *** PROGRAMS ***

@app.route('/programs_list', methods=['GET'])
@requires_login
def list_programs(user):
    return {'programs': DATABASE.programs_for_user(user['username'])}

# Not very restful to use a GET to delete something, but indeed convenient; we can do it with a single link and avoiding AJAX.
@app.route('/programs/delete/<program_id>', methods=['GET'])
@requires_login
def delete_program(user, program_id):
    result = DATABASE.program_by_id(program_id)
    if not result or result['username'] != user['username']:
        return "", 404
    DATABASE.delete_program_by_id(program_id)
    DATABASE.increase_user_program_count(user['username'], -1)
    return redirect('/programs')

@app.route('/programs', methods=['POST'])
@requires_login
def save_program(user):

    body = request.json
    if not isinstance(body, dict):
        return 'body must be an object', 400
    if not isinstance(body.get('code'), str):
        return 'code must be a string', 400
    if not isinstance(body.get('name'), str):
        return 'name must be a string', 400
    if not isinstance(body.get('level'), int):
        return 'level must be an integer', 400
    if 'adventure_name' in body:
        if not isinstance(body.get('adventure_name'), str):
            return 'if present, adventure_name must be a string', 400

    # We check if a program with a name `xyz` exists in the database for the username.
    # It'd be ideal to search by username & program name, but since DynamoDB doesn't allow searching for two indexes at the same time, this would require to create a special index to that effect, which is cumbersome.
    # For now, we bring all existing programs for the user and then search within them for repeated names.
    programs = DATABASE.programs_for_user(user['username'])
    program_id = uuid.uuid4().hex
    overwrite = False
    for program in programs:
        if program['name'] == body['name']:
            overwrite = True
            program_id = program['id']
            break

    stored_program = {
        'id': program_id,
        'session': session_id(),
        'date': timems(),
        'lang': requested_lang(),
        'version': version(),
        'level': body['level'],
        'code': body['code'],
        'name': body['name'],
        'username': user['username']
    }

    if 'adventure_name' in body:
        stored_program['adventure_name'] = body['adventure_name']

    DATABASE.store_program(stored_program)
    if not overwrite:
        DATABASE.increase_user_program_count(user['username'])

    return jsonify({'name': body['name'], 'id': program_id})

@app.route('/programs/share', methods=['POST'])
@requires_login
def share_unshare_program(user):
    body = request.json
    if not isinstance(body, dict):
        return 'body must be an object', 400
    if not isinstance(body.get('id'), str):
        return 'id must be a string', 400
    if not isinstance(body.get('public'), bool):
        return 'public must be a string', 400

    result = DATABASE.program_by_id(body['id'])
    if not result or result['username'] != user['username']:
        return 'No such program!', 404

    DATABASE.set_program_public_by_id(body['id'], bool(body['public']))
    return jsonify({'id': body['id']})

@app.route('/translate/<source>/<target>')
def translate_fromto(source, target):
    source_adventures = YamlFile.for_file(f'coursedata/adventures/{source}.yaml').to_dict()
    source_levels = YamlFile.for_file(f'coursedata/level-defaults/{source}.yaml').to_dict()
    source_texts = YamlFile.for_file(f'coursedata/texts/{source}.yaml').to_dict()

    target_adventures = YamlFile.for_file(f'coursedata/adventures/{target}.yaml').to_dict()
    target_levels = YamlFile.for_file(f'coursedata/level-defaults/{target}.yaml').to_dict()
    target_texts = YamlFile.for_file(f'coursedata/texts/{target}.yaml').to_dict()

    files =[]

    files.append(translating.TranslatableFile(
      'Levels',
      f'level-defaults/{target}.yaml',
      translating.struct_to_sections(source_levels, target_levels)))

    files.append(translating.TranslatableFile(
      'Messages',
      f'texts/{target}.yaml',
      translating.struct_to_sections(source_texts, target_texts)))

    files.append(translating.TranslatableFile(
      'Adventures',
      f'adventures/{target}.yaml',
      translating.struct_to_sections(source_adventures, target_adventures)))

    return render_template('translate-fromto.html',
        source_lang=source,
        target_lang=target,
        files=files)

@app.route('/update_yaml', methods=['POST'])
def update_yaml():
    filename = path.join('coursedata', request.form['file'])
    # The file MUST point to something inside our 'coursedata' directory
    #(no exploiting bullshit here)
    filepath = path.abspath(filename)
    expected_path = path.abspath('coursedata')
    if not filepath.startswith(expected_path):
        raise RuntimeError('Are you trying to trick me?')

    data = load_yaml_rt(filepath)
    for key, value in request.form.items():
        if key.startswith('c:'):
            translating.apply_form_change(data, key[2:], translating.normalize_newlines(value))

    data = translating.normalize_yaml_blocks(data)

    return Response(dump_yaml_rt(data),
        mimetype='application/x-yaml',
        headers={'Content-disposition': 'attachment; filename=' + request.form['file'].replace('/', '-')})


# *** AUTH ***

from website import auth
auth.routes(app, DATABASE, requested_lang)

# *** TEACHER BACKEND

from website import teacher
teacher.routes(app, DATABASE, requested_lang)

# *** START SERVER ***

def on_server_start():
    """Called just before the server is started, both in developer mode and on Heroku.

    Use this to initialize objects, dependencies and connections.
    """
    pass


if __name__ == '__main__':
    # Start the server on a developer machine. Flask is initialized in DEBUG mode, so it
    # hot-reloads files. We also flip our own internal "debug mode" flag to True, so our
    # own file loading routines also hot-reload.
    utils.set_debug_mode(not os.getenv('NO_DEBUG_MODE'))

    # If we are running in a Python debugger, don't use flasks reload mode. It creates
    # subprocesses which make debugging harder.
    is_in_debugger = sys.gettrace() is not None

    on_server_start()

    # Threaded option enables multiple instances for multiple user access support
    app.run(threaded=True, debug=not is_in_debugger, port=config['port'], host="0.0.0.0")

    # See `Procfile` for how the server is started on Heroku.

app.py
diff --git a/app.py b/app.py
index 26be1e659a7..c485c8b42c8 100644
--- a/app.py
+++ b/app.py
@@ -964,7 +964,6 @@ def split_teacher_docs(contents):
             sections.append({'title': tag, 'content': ''})
         else:
             sections[-1]['content'] += str(tag)
-
     return sections
 
 def render_main_menu(current_page):
diff --git a/main/for-teachers-de.md b/main/for-teachers-de.md
index 9d46ed31263..2f32979b58e 100644
--- a/main/for-teachers-de.md
+++ b/main/for-teachers-de.md
@@ -177,20 +177,20 @@ Z.B. programmieren sie: `print Hello, I'm Hedy` statt `print Hello I'm Hedy`
 
 Z.B. programmieren sie: 
 
-```
+<pre>
 tiere is Hund, Katze, Kuh
 print die besten tiere sind...
 print tiere at random
-```
+</pre>
 
 Stattdessen sollten sie ein anderes Wort als tiere in der zweiten Zeile verwenden. 
 Zum Beispiel: 
 
-```
+<pre>
 tiere is Hund, Katze, Kuh
 print das beste tier ist…
 print tiere at random
-```
+</pre>
 
 ### Lehrplan 2
 
@@ -209,28 +209,28 @@ All diese Zahlen sind Variablen.
 
 Eine Variable muss nicht unbedingt eine Zahl sein, sie kann auch (eine Liste von) Wörtern sein, wie Ihr Name. Sie können dieses Beispiel in Hedy zur Demonstration verwenden: 
 
-```
+<pre>
 name is Hedy
 print Hallo name!
-```
+</pre>
 
 Dies zeigt, dass Sie den Wert einer Variablen in Ihrem Code bestimmen können. 
 Sie können aber auch den Spieler fragen, welchen Wert die Variable haben soll. 
 Wie im nächsten Beispiel:
 
-```
+<pre>
 name is ask Wie heißt du?
 print Hallo name!
-```
+</pre>
 
 Erweitern Sie das Beispiel, indem Sie die Variable `alter` hinzufügen: 
 
-```
+<pre>
 name is ask Wie heißt du?
 print Hallo name!
 alter is ask Wie alt sind Sie?
 print Sie sind alter Jahre alt
-```
+</pre>
 
 #### Probieren Sie es aus! (10 Minuten) 
 Lassen Sie Ihre Schüler versuchen, diese Befehle selbst zu benutzen, oder lassen Sie sie sich ihre eigenen Versionen ausdenken. 
@@ -238,27 +238,27 @@ Lassen Sie Ihre Schüler versuchen, diese Befehle selbst zu benutzen, oder lasse
 #### Nach dem Zufallsprinzip (15 Minuten) 
 Das Lustige an Variablen ist, dass Sie Hedy einen Zufallswert aus Ihrer Liste auswählen lassen können. Zeigen Sie Ihren Schülern dieses Beispiel und lassen Sie sie anschließend an ihren eigenen Programmierungen arbeiten: 
 
-```
+<pre>
 auswahl is Stein, Papier, Schere
 print auswahl at random
-```
+</pre>
 
-```
+<pre>
 vereine is FC Barcelona, Bayern München, Manchester United
 print Der beste Fußballverein ist...
 print vereine at random
-```
+</pre>
 
 #### Aufgabe: Die Wahrsagerin (20 min) 
 
-```
+<pre>
 print Ich bin Hedy, die Wahrsagerin!
 frage is Was willst du wissen?
 print Das ist es, was du wissen willst: frage
 antworten is ja, nein, vielleicht 
 print Meine Kristallkugel sagt...
 print antworten at random
-```
+</pre>
 
 #### Auswertung (5 min)
 Werten Sie die Lektion kurz mit Ihren Schülern aus, um die Lektion abzurunden. 
@@ -299,11 +299,11 @@ Erklären Sie, dass in Level 3 nach der Verwendung des `print`-Befehls Anführun
 Das folgende Beispiel zeigt, wie wichtig bzw. praktisch es ist, Anführungszeichen zu verwenden. 
 Hinweis: Zeigen Sie Ihren Schülern zuerst den Code in Level 2! 
 
-```
+<pre>
 Haustier is Hund, Katze, Hase
 print Du bekommst dies Haustier: 
 print Haustier at random
-```
+</pre>
 
 Sie werden sehen, dass das Wort `Haustier` in Zeile 2 nicht funktioniert. 
 Es wurde in den Wert der Variable Haustier geändert, obwohl Sie es als normales Wort verwenden wollten. 
@@ -311,25 +311,25 @@ In Level 3 wird dieses Problem durch die Verwendung der Anführungszeichen gelö
 Die Anführungszeichen sagen Hedy, dass alles, was zwischen den Anführungszeichen steht, ein normales Wort sein soll, das gedruckt werden soll. 
 Alles außerhalb der Anführungszeichen ist eine Variable oder ein Befehl. Zeigen Sie dieses Beispiel in Level 3. 
 
-```
+<pre>
 Haustier is Hund, Katze, Hase
 print 'Du bekommst dies Haustier: '
 print Haustier at random
-```
+</pre>
 
 Beachten Sie, dass die Anführungszeichen verwendet werden, um Hedy mitzuteilen, welcher Text gedruckt werden soll. 
 Das bedeutet, dass Sie die Anführungszeichen nicht für andere Zwecke verwenden können, wie z. B. zum Zusammenziehen von Wörtern. 
 Zeigen Sie Ihren Schülern, dass dies bedeutet, dass dieser Code wegen des Apostrophs in Zeile 2 nicht funktionieren wird. 
 
-```
+<pre>
 print 'Wie geht's?'
-```
+</pre>
 
 Korrigieren Sie es, indem Sie das Apostroph entfernen. 
 
-```
+<pre>
 print 'Wie gehts?'
-```
+</pre>
 
 Bevor sich die Schüler an die Arbeit machen, achten Sie auf die häufig gemachten Fehler: 
 Es sollte immer ein Anführungszeichen vor und eines nach dem Text stehen, den Sie ausgeben wollen. Prüfen Sie also immer, ob alle `print`-Befehle 2 Anführungszeichen haben. 
@@ -372,77 +372,77 @@ Dies sind die häufigsten Fehler, die von Schülern im Level 4 gemacht werden:
 
 - Fehler: Die Schüler setzen `else` in die nächste Zeile, aber es sollte in der gleichen Zeile stehen wie der `if`-Befehl. 
 Beispiel: 
-```
+<pre>
 if name is Hedy print 'Toll!'
 else print 'Buh!'
-```
+</pre>
 Solution: `else` should be in the same line as `if`. 
-```
+<pre>
 if name is Hedy print 'Toll!' else print 'Buh!'
-```
+</pre>
 
 - Fehler: Schüler verwenden Apostrophe nach einem `print`-Befehl. 
-```
+<pre>
 print 'Wie geht's?'
-```
+</pre>
 Lösung: Verwenden Sie keine Apostrophe.
-```
+<pre>
 print 'Wie gehts?'
-```
+</pre>
 
 - Fehler: Die Schüler vergessen, den `print`-Befehl zu verwenden, wenn sie einen `if`-Befehl verwenden.
-```
+<pre>
 if name is Hedy 'Toll!' else 'Buh!'
-```
+</pre>
 Lösung: Verwenden Sie `print` zweimal, wenn Sie sowohl `if` als auch `else` verwenden. 
-``` 
+<pre>
 if name is Hedy print 'Toll!' else print 'Buh!'
-```
+</pre>
 
 - Fehler: Schüler verwenden unterschiedliche Namen für dieselbe Variable.
-```
+<pre>
 Pferd is ask Wie heißt dein Pferd?
 if name is Spirit print 'Toll!' else print 'Buh!'
-```
+</pre>
 Lösung: Verwenden Sie immer den gleichen Namen für Ihre Variable (prüfen Sie auch, ob der Variablenname im Singular oder Plural steht (Antwort/Antworten) und achten sie auf Groß- und Kleinschreibung). 
-```
+<pre>
 Pferd is ask Wie heißt dein Pferd?
 if Pferd is Spirit print 'Toll!' else print 'Buh!'
-```
+</pre>
 
 - Fehler: Schüler vergessen, bei der Verwendung von `if` und `else` die beiden Anführungszeichen nach `print` zu setzen. 
-```
+<pre>
 if name is Hedy print Toll! else print 'Buh!
-```
+</pre>
 Lösung: Verwenden Sie immer zwei Anführungszeichen (sowohl vor als auch hinter dem Text) für jeden `print`-Befehl in Ihrem Code. 
-```
+<pre>
 if name is Hedy print 'Toll!' else print 'Buh!'
-```
+</pre>
 
 - Fehler: Schüler verwenden Anführungszeichen um eine Variable. 
-```
+<pre>
 if 'name' is 'Hedy' print 'Toll!' else print 'Buh!'
-```
+</pre>
 Lösung: Verwenden Sie keine Anführungszeichen um eine Variable. 
-```
+<pre>
 if name is Hedy print 'Toll!' else print 'Buh!'
-```
+</pre>
 
 - Fehler: Schüler vergeben Namen für Variablen, die aus mehreren Wörtern bestehen. 
-```
+<pre>
 Gewaehlte Tuer is ask Welche Tür wählst du?
-```
+</pre>
 Lösung: Ein Variablenname sollte immer nur aus einem Wort bestehen, d.h. kein Leerzeichen enthalten.  
-```
+<pre>
 gewaehltetuer is ask Welche Tür wählst du?
-```
+</pre>
 
 - Fehler: Schüler wollen, dass mehrere Antworten richtig sind, wenn sie if verwenden. 
-```
+<pre>
 if name is Jonas, David, Said print 'Du bist lustig!' else print 'Du bist nicht lustig!'
-```
+</pre>
 Lösung: Dies ist leider nicht erlaubt. Sie können nur einen Namen für jeden if-Befehl wählen. Andererseits können Sie mehrere if-Befehle in einem Code verwenden, wie im Beispiel unten: 
-```
+<pre>
 name is ask Wie heißt du?
 
 if name is Jonas print 'Du bist lustig!'
@@ -450,18 +450,18 @@ if name is Jonas print 'Du bist lustig!'
 if name is David print 'Du bist lustig!'
 
 if name is Said print 'Du bist lustig!'
-```
+</pre>
 
 - Fehler: Die Schüler geben in `ask` die gleiche Antwort wie den Variablennamen. Zum Beispiel hat dieser Schüler das Kennwort 'Passwort' gemacht.  
-```
+<pre>
 Passwort is ask Wie lautet das geheime Passwort?
 if Passwort is Passwort print 'Hereinspaziert!' else print 'Zugriff verweigert!' 
-```
+</pre>
 Lösung: Wählen Sie einen anderen Namen für die Variable.
-```
+<pre>
 kennwort is ask Wie lautet das geheime Passwort?
 if kennwort is Passwort print 'Hereinspaziert!' else print 'Zugriff verweigert!' 
-```
+</pre>
 
 ### Lehrplan 4
 Durch die Hinzufügung des `if`-Befehls haben sich in Level 4 eine Menge Möglichkeiten eröffnet. Deshalb empfehlen wir, zwei Unterrichtseinheiten in Level 4 zu verbringen, bevor Sie mit Level 5 fortfahren. Es bietet den Schülern die Möglichkeit, sich mit den `if`-Konstruktionen wirklich vertraut zu machen, bevor sie zum nächsten Level übergehen. 
@@ -474,10 +474,10 @@ Wiederholen Sie kurz die vorangegangene Lektion und erinnern Sie sich auch an di
 
 #### Anweisung: `if`-Konstruktionen (10 Minuten)
 Die `if`-Konstruktion ermöglicht es Hedy, auf zwei verschiedene Arten auf die Antworten des Spielers zu reagieren. Demonstrieren Sie das an diesem Beispiel: 
-```
+<pre>
 name is Wie heißt du?
 if name is Hedy print 'Hereinspaziert!' else print 'Zugriff verweigert!'
-```
+</pre>
 
 #### Machen wir uns an die Arbeit: Die Wahrsagerin (20 Minuten) 
 Die Schüler können die Aufgabe der Wahrsagerin selbstständig bearbeiten, oder Sie können die Aufgaben Schritt für Schritt gemeinsam mit Ihren Schülern bearbeiten. 
@@ -540,19 +540,19 @@ Eine gute Möglichkeit, die Stunde zu beginnen, besteht darin, alle verschiedene
 Mit dem Befehl `repeat` können Sie eine Codezeile mehrfach wiederholen. 
 Um das Lied "Baby Shark" zu programmieren, hätten wir in früheren Levels eine Zeile mehrfach wiederholen müssen: 
 
-```
+<pre>
 print 'Baby Shark tututututudu'
 print 'Baby Shark tututututudu'
 print 'Baby Shark tututututudu'
 print 'Baby Shark'
-```
+</pre>
 
 Jetzt, mit `repeat`, ist es viel einfacher, Code wie diesen zu erstellen: 
 
-```
+<pre>
 repeat 3 times print 'Baby Shark tututututudu'
 print 'Baby Shark'
-```
+</pre>
 
 #### Auf geht's: Hedy die Sängerin!  (20 - 30 Minuten) 
 Nach dieser kurzen Einweisung können die Schüler die anderen Lieder in den Übungen selbständig bearbeiten. 
diff --git a/main/for-teachers-en.md b/main/for-teachers-en.md
index 89cf3b46215..cf7b8527bea 100644
--- a/main/for-teachers-en.md
+++ b/main/for-teachers-en.md
@@ -45,7 +45,7 @@ In My programs you can find all the codes that you've saved.
 
 You can save your projects easily by giving your project a name in the white bar and pressing the green button Save Code.
 
-![](RackMultipart20210420-4-19s02r2_html_5657fc35812eb727.png)
+<!-- ![](RackMultipart20210420-4-19s02r2_html_5657fc35812eb727.png) -->
 
 Let the programming fun begin! 
 
@@ -102,18 +102,18 @@ E.g. they program: `print Hello, I'm Hedy` instead of `print Hello I'm Hedy`
 
 E.g. They program: 
 
-```
+<pre>
 animals is dog, cat, cow
 print the best animals are…
 print animals at random
-```
+</pre>
 Instead they should change the word animals in the second line into something other than animals. For example: 
 
-```
+<pre>
 animals is dog, cat, cow
 print the best animal is…
 print animals at random
-```
+</pre>
 
 ### Frequently made mistakes in level 3
 
@@ -130,77 +130,77 @@ These are the frequently made mistakes by students in level 4:
 
 - Mistake: Students put else on the next line, but it should be in the same line as the if command. 
 Code: 
-```
+<pre>
 if name is Hedy print 'nice'
 else print 'boo!'
-```
+</pre>
 Solution: Else should be in the same line as if. 
 
-```
+<pre>
 if name is Hedy print 'nice' else print 'boo!'
-```
+</pre>
 - Mistake: Students use apostrophes after a print command. 
-```
+<pre>
 print 'I'm Hedy'
-```
+</pre>
 Solution: Don't use apostrophes.
-```
+<pre>
 print 'Im Hedy'
-```
+</pre>
 - Mistake: Students forget to use the print command when using an if command.
-```
+<pre>
 if name is Hedy 'nice' else 'boo!'
-```
+</pre>
 Solution: Use print twice when using both if and else. 
-``` 
+<pre>
 if name is Hedy print 'nice' else print 'boo!'
-```
+</pre>
 
 - Mistake: Students use different names for the same variable.
-```
+<pre>
 horse is ask What is your horse called?
 if me is Bonfire print 'nice' else print 'boo!'
-```
+</pre>
 
 Solution: Always use the same name for your variable (also check whether the variable name is singular or plural (answer/answers)). 
-```
+<pre>
 horse is ask what is your horse called?
 if horse is Bonfire print 'nice' else print 'boo!'
-```
+</pre>
 
 - Mistake: Students forget to type both of the quotation marks after print, when using if and else. 
-```
+<pre>
 if name is Hedy print nice else print 'boo!
-```
+</pre>
 Solution: Always use two quotation marks (both in front of and behind the text) for each print command in your code. 
-```
+<pre>
 if name is Hedy print 'nice' else print 'boo!'
-```
+</pre>
 
 - Mistake: Students use quotation marks around a variable. 
-```
+<pre>
 if 'name' is 'Hedy' print 'nice' else print 'boo!'
-```
+</pre>
 Solution: Don't use quotation marks around a variable. 
-```
+<pre>
 if name is Hedy print 'nice' else print 'boo!'
-```
+</pre>
 
 - Mistake: Students give names to variables that consist of multiple words. 
-```
+<pre>
 Chosen door is ask Which door do you choose?
-```
+</pre>
 Solution: A variable name should always just be one word. 
-```
+<pre>
 chosendoor is ask Which door do you choose?
-```
+</pre>
 
 - Mistake: Students want multiple answers to be correct, when using if. 
-```
+<pre>
 if name is Jesse, David, Souf print 'you are funny' else print 'you are not funny'
-```
+</pre>
 Solution: Unfortunately, this is not allowed. You can only pick one name for each if command. On the other hand, you can use multiple if commands in one code, like the example beneath: 
-```
+<pre>
 name is ask What is your name?
 
 if name is Jesse print 'you are funny'
@@ -208,17 +208,17 @@ if name is Jesse print 'you are funny'
 if name is David print 'you are funny'
 
 if name is Souf print 'you are funny'
-```
+</pre>
 - Mistake: Students give the same answer in ask as the variable name. For example this student made the password 'password'. 
-```
+<pre>
 password is ask What is the secret password?
 If password is password print 'You can enter!' else print 'Access denied!' 
-```
+</pre>
 Solution: Choose a different name for the variable.
-```
+<pre>
 secretpassword is ask What is the secret password?
 If secretpassword is password print 'You can enter!' else print 'Access denied!' 
-```
+</pre>
 
 ### Frequently made mistakes in level 5
 
diff --git a/main/for-teachers-id.md b/main/for-teachers-id.md
index 923660dd84b..ac8a745711a 100644
--- a/main/for-teachers-id.md
+++ b/main/for-teachers-id.md
@@ -102,18 +102,18 @@ Misal mereka menulis`print Halo, aku Hedy` instead of `print Halo aku Hedy`
 
 Misal mereka menulis:
 
-```
+<pre>
 hewan is anjing, kucing, sapi
 print hewan terbaik adalah…
 print hewan at random
-```
+</pre>
 Seharusnya mereka mengganti kata hewan di baris kedua menjadi kata lain misalnya:
 
-```
+<pre>
 hewan is anjing, kucing, sapi
 print binatang terbaik adalah…
 print hewan at random
-```
+</pre>
 
 ### Kesalahan-kesalahan yang sering terjadi di level 3
 
@@ -130,77 +130,77 @@ Berikut adalah kesalahan-kesalahan yang sering dilakukan para siswa pada level 4
 
 - Kesalahan: Siswa menuliskan `else` di baris berikutnya, walaupun itu seharusnya satu baris dengan perintah `if`
 Kode: 
-```
+<pre>
 if nama is Hedy print 'bagus'
 else print 'huu!'
-```
+</pre>
 Solusi: `else` harus berada di baris yang sama dengan if.
 
-```
+<pre>
 if nama is Hedy print 'bagus' else print 'huu!'
-```
+</pre>
 - Kesalahan: Siswa menggunakan kutip satu di dalam teks yang akan ditampilkan `print`. 
-```
+<pre>
 print 'Aku Hedy pernah belajar 'Python''
-```
+</pre>
 Solusi: Jangan menggunakan kutip satu dalam teks yang akan ditampilkan.
-```
+<pre>
 print 'Aku Hedy pernah belajar Python'
-```
+</pre>
 - Kesalahan: Siswa lupa menggunakan perintah `print` ketika sedang menggunakan perintah `if`.
-```
+<pre>
 if nama is Hedy 'bagus' else 'huu!'
-```
+</pre>
 Solusi: Gunakan `print` setiap kali menampilkan teks didalam perintah `if`. 
-``` 
+<pre>
 if nama is Hedy print 'bagus' else print 'huu!'
-```
+</pre>
 
 - Kesalahan: Siswa menggunakan nama yang berbeda untuk sebuah variabel.
-```
+<pre>
 kuda is ask Apa nama kuda kamu?
 if aku is api print 'bagus' else print 'huu!'
-```
+</pre>
 
 Solusi: Selalu gunakan nama yang sama untuk variabel (cek juga apakah variabelnya menggunakan penulisan yang sama, misal 'nama' dan 'name'). 
-```
+<pre>
 kuda is ask Apa nama kuda kamu?
 if kuda is api print 'bagus' else print 'huu!'
-```
+</pre>
 
 - Kesalahan: Siswa lupa menuliskan kutip satu pembuka dan penutup pada teks yang akan ditampilkan perintah `print`, ketika menggunakan perintah `if` dan `else`. 
-```
+<pre>
 if nama is Hedy print bagus else print 'huu!
-```
+</pre>
 Solusi: Selalu gunakan dua tanda kutip (awal dan akhir teks) untuk setiap perintah `print` di kode. 
-```
+<pre>
 if name is Hedy print 'nice' else print 'boo!'
-```
+</pre>
 
 - Kesalahan: Siswa menggunakan kutip satu untuk nama variabel. 
-```
+<pre>
 if 'name' is 'Hedy' print 'nice' else print 'boo!'
-```
+</pre>
 Solusi: Jangan gunakan kutip satu untuk nama variabel. 
-```
+<pre>
 if nama is Hedy print 'bagus' else print 'huu!'
-```
+</pre>
 
 - Kesalahan: Siswa memberikan nama lebih dari satu kata untuk sebuah variabel (termasuk menggunakan tanda penghubung). 
-```
+<pre>
 pintu terpilih is ask Pintu mana yang mau kamu pilih?
-```
+</pre>
 Solusi: Variabel harus diberi nama satu kata dan tidak menggunakan tanda penghubung  
-```
+<pre>
 pintuterpilih is ask Pintu mana yang mau kamu pilih?
-```
+</pre>
 
 - Kesalahan: Siswa menginginkan beberapa jawaban sebagai jawaban benar, ketika menggunakan `if`. 
-```
+<pre>
 if nama is Jesse, David, Souf print 'kamu lucu' else print 'kamu tidak lucu'
-```
+</pre>
 Solusi: Sayangnya, hal ini tidak dimungkinkan. Kamu hanya bisa memilih satu nama untuk setiap perintah `if`. Sebagai alternatif, kamu dapat menggunakan beberapa perintah `if` seperti berikut:
-```
+<pre>
 nama is ask Siapa nama kamu?
 
 if nama is Jesse print 'kamu lucu'
@@ -208,17 +208,17 @@ if nama is Jesse print 'kamu lucu'
 if nama is David print 'kamu lucu'
 
 if nama is Souf print 'kamu lucu'
-```
+</pre>
 - Kesalahan: Siswa memberikan jawaban di perintah 'ask' yang sama dengan nama variabel. Sebagai contoh siswa ini membuat passwordnya 'password'. 
-```
+<pre>
 password is ask Apa password rahasianya?
 If password is password print 'Kamu bisa masuk!' else print 'Akses ditolak!' 
-```
+</pre>
 Solusi: Pilih nama yang berbeda untuk variabel.
-```
+<pre>
 passwordrahasia is ask Apa password rahasianya?
 If passwordrahasia is password print 'Kamu bisa masuk!' else print 'Akses ditolak!' 
-```
+</pre>
 
 ### Kesalahan-kesalahan yang sering terjadi di level 5
 
diff --git a/main/for-teachers-nl.md b/main/for-teachers-nl.md
index bff8cbece48..33bd4eed74c 100644
--- a/main/for-teachers-nl.md
+++ b/main/for-teachers-nl.md
@@ -50,7 +50,7 @@ Bij mijn programma's vind je alle codes terug die je hebt opgeslagen.
 
 Je kunt je projecten simpel opslaan door je project een naam te geven in het witte balkje en dan op de groene knop Code opslaan ernaast te klikken.
 
-![](RackMultipart20210420-4-19s02r2_html_5657fc35812eb727.png)
+<!-- ![](RackMultipart20210420-4-19s02r2_html_5657fc35812eb727.png) -->
 
 Veel programmeer plezier!
 
@@ -109,17 +109,17 @@ Bijvoorbeeld: `dieren is hond kat` oplossing: `dieren is hond, kat`
 Bijvoorbeeld: `print Hallo, ik ben Hedy.` oplossing: `print Hallo ik ben Hedy`
 - Leerlingen gebruiken de naam van hun variabele ook als woord in een print commando, maar dat kan pas vanaf level 3.
 Bijvoorbeeld: 
-```
+<pre>
 dier is hond, kat, koe
 print Het leukste dier is…
 print dier at random
-```
+</pre>
 In regel 2 van deze code staat het woord dier, terwijl dat ook de naam van de variabele is. Los dit op door de variabele een andere naam te geven, bijvoorbeeld:
-```
+<pre>
 dieren is hond, kat, koe
 print Het leukste dier is...
 print dieren at random
-```
+</pre>
 
 ### Veelgemaakte fouten in level 3
 
@@ -132,17 +132,17 @@ Bijvoorbeeld: `print 'Hallo ` Oplossing: `print 'Hallo'`
 - Leerlingen vergeten dat ook bij ask aanhalingstekens gebruikt moeten worden. 
 Bijvoorbeeld: `naam is ask Hoe heet je?` oplossing `naam is ask 'Hoe heet je?'`
 - Leerlingen gebruiken hoge komma's na een print commando. Bijvoorbeeld:
-```
+<pre>
 print 'Er lopen twee oma's door het park'
-```
+</pre>
 Oplossing: Gebruik de andere hoge komma (naast de 1 toets met het ~)
-```
+<pre>
 print 'Er lopen twee oma`s door het park'
-```
+</pre>
 Oplossing 2: Gebruik geen hoge komma en spel het woord fout.
-```
+<pre>
 print 'Er lopen twee omas door het park'
-```
+</pre>
 
 ### Veelgemaakte fouten in level 4
 
@@ -150,67 +150,67 @@ In level 4 zijn dit veelgemaakte fouten door de leerlingen:
 
 - Fout: Leerlingen vergeten het woord print bij een if commando.
 
-```
+<pre>
 if naam is Hedy 'leuk' 
 else 'minder leuk!'
-```
+</pre>
 Oplossing: gebruik twee keer een print commando na een if commando
-``` 
+<pre>
 if naam is Hedy print 'leuk' 
 else print 'minder leuk!'
-```
+</pre>
 
 - Fout: Leerlingen gebruiken twee verschillende namen voor dezelfde variabele.
-```
+<pre>
 paard is ask 'Hoe heet jouw paard?'
 if naam is Bonfire print 'leuk' 
 else print 'minder leuk!'
-```
+</pre>
 
 Oplossing: Gebruik altijd dezelfde naam voor een variabele. Controleer ook of de variabelenaam enkelvoud of meervoud is, daar lees je gemakkelijk overheen (antwoord/antwoorden). 
-```
+<pre>
 paard is ask 'Hoe heet jouw paard?'
 if paard is Bonfire print 'leuk' 
 else print 'minder leuk!'
-```
+</pre>
 
 - Fout: Leerlingen vergeten beide aanhalingstekens bij de print commando's.
-```
+<pre>
 if naam is Hedy print leuk 
 else print 'minder leuk!
-```
+</pre>
 Oplossing: Gebruik altijd twee aanhalingstekens per printcommando, een vooraan en een achteraan.
-```
+<pre>
 if naam is Hedy print 'leuk' 
 else print 'minder leuk!'
-```
+</pre>
 
 - Fout: Leerlingen gebruiken aanhalingstekens rond de naam van de variabele.
-```
+<pre>
 if 'naam' is 'Hedy' print 'leuk' 
 else print 'minder leuk!'
-```
+</pre>
 Oplossing: rond een variabele moeten geen aanhalingstekens.
-```
+<pre>
 if naam is Hedy print 'leuk' 
 else print 'minder leuk!'
-```
+</pre>
 
 - Fout: Leerlingen geven variabelen namen met meerdere woorden.
-```
+<pre>
 gekozen deur is ask 'Welke deur kies jij?'
-```
+</pre>
 Oplossing: Een variabelenaam mag maar een woord zijn.
-```
+<pre>
 gekozendeur is ask 'Welke deur kies jij?'
-```
+</pre>
 
 - Fout: Leerlingen willen dat bij de if meerdere antwoorden goed zijn.
-```
+<pre>
 if naam is Jesse, David, Souf print 'jij bent grappig' else print 'jij bent niet grappig'
-```
+</pre>
 Oplossing: Dit mag helaas niet, je mag maar een naam kiezen per if commando. Je kunt wel meerdere if commando's toevoegen zoals het voorbeeld hieronder:
-```
+<pre>
 naam is ask hoe heet jij?
 
 if naam is Jesse print 'leuk'
@@ -218,38 +218,38 @@ if naam is Jesse print 'leuk'
 if naam is David print 'leuk'
 
 if naam is Souf print 'leuk'
-```
+</pre>
 Alternatieve oplossing: Je kunt ook een lijstje maken met vrienden en Hedy laten controleren of het antwoord in het lijstje staat met in. 
-```
+<pre>
 vrienden is Jesse, David, Souf
 naam is ask 'Wie ben jij?'
 if naam in vrienden print 'leuk'
 else print 'minder leuk'
-```
+</pre>
 - Fout: De leerlingen vullen bij ask hetzelfde in als bij de variabelenaam, dus de variabele wordt de zelfde waarde als de variabelenaam. In het voorbeeld hieronder was bijvoorbeeld het wachtwoord 'wachtwoord'
-```
+<pre>
 wachtwoord is ask 'Wat is het wachtwoord?'
 if wachtwoord is wachtwoord print 'Je mag er door!' 
 else print 'Verboden toegang!'
-```
+</pre>
 Oplossing: Kies een andere naam voor je variabele
-```
+<pre>
 geheimwachtwoord is ask 'Wat is het wachtwoord?'
 if geheimwachtwoord is wachtwoord print 'Je mag er door!' else print 'Verboden toegang!'
-```
+</pre>
 
 - Fout: Leerlingen gebruiken hoge komma's na een print commando.
-```
+<pre>
 print 'Er lopen twee oma's door het park'
-```
+</pre>
 Oplossing: Gebruik de andere hoge komma (naast de 1 toets met het ~)
-```
+<pre>
 print 'Er lopen twee oma`s door het park'
-```
+</pre>
 Oplossing 2: Gebruik geen hoge komma en spel het woord fout.
-```
+<pre>
 print 'Er lopen twee omas door het park'
-```
+</pre>
 
 ### Veelgemaakte fouten in level 5
 
diff --git a/templates/for-teachers.html b/templates/for-teachers.html
index f7ce7781e0b..38d408ed5e9 100644
--- a/templates/for-teachers.html
+++ b/templates/for-teachers.html
@@ -1,7 +1,7 @@
 {% extends "auth.html" %}
 
 {% block main %}
-<div id="teacher_classes" class="pl-8">
+<div id="teacher_classes" class="flex flex-col items-center pb-4 border-b-2">
   <h2>{{auth.teacher_classes}}</h2>
   <table class="table-auto border-separate space-y-6 border border-black mt-4 mb-4">
     <thead class="bg-gray-200">
@@ -16,31 +16,41 @@ <h2>{{auth.teacher_classes}}</h2>
       {% endfor %}
     </tbody>
   </table>
-  <br>
-  <button class="green-btn" onclick="hedyApp.create_class ()">{{auth.create_class}}</button>
-  <br><br><br>
+  <button class="green-btn mt-4" onclick="hedyApp.create_class ()">{{auth.create_class}}</button>
 </div>
-<div class="px-8">
-  <h1>Hedy docs</h1>
-  <div class="markdown">
-    <a class="github-fork-ribbon left-top" href="https://github.com/felienne/hedy" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
-    {% for section in sections %}
-      {# Always show the first section #}
-      {% if loop.index == 1 %}
-        <h2 class="underline">{{section.title}}</h2>
-        <div class="turn-pre-into-ace">{{section.content|safe}}</div>
-      {% else %}
-        <h2 class="underline cursor-pointer" onclick="showSection ({{loop.index}})">{{section.title}}</h2>
-        <div class="section hidden turn-pre-into-ace" id="section-{{loop.index}}">{{section.content|safe}}</div>
-      {% endif %}
-    {% endfor %}
-  </div>
+<div class="w-full text-center mt-4">
+<h2>Hedy Docs</h2>
+<div class="mb-4">{{sections[0].content|safe}}</div>
+<div class="flex flex-row justify-center mb-4 pb-4 border-b-2">
+{% for section in sections[1:] %}
+    <button class="green-btn text-xl m-2 p-4 section-button" id="button-{{loop.index}}" onclick="showSection ({{loop.index}})">{{section.title}}</button>
+{% endfor %}
 </div>
+</div>
+{% for section in sections[1:] %}
+    <div class="section hidden turn-pre-into-ace" id="section-{{loop.index}}">{{section.content|safe}}</div>
+{% endfor %}
+
 <script>
 var showSection = function (index) {
-   if ($ ('#section-' + index).is (':visible')) return $ ('.section').hide ();
+   $(".section-button").each(function(){
+       if ($(this).hasClass('blue-btn')) {
+           $(this).removeClass("blue-btn");
+           $(this).addClass("green-btn");
+       }
+   });
+   if ($ ('#section-' + index).is (':visible')) {
+       $("#button-" + index).removeClass("blue-btn");
+       $("#button-" + index).addClass("green-btn");
+       return $ ('.section').hide ();
+   }
+   $("#button-" + index).removeClass("green-btn");
+   $("#button-" + index).addClass("blue-btn");
    $ ('.section').hide ();
    $ ('#section-' + index).slideToggle (400);
+   $('html, body').animate({
+        scrollTop: $ ('#section-' + index).offset().top
+    }, 2000);
 }
 </script>
 {% endblock %}

Replace parts of 'for teachers' page with this presentation
I've made a presentation to make the teacher page look better. This page can be embedded into the English for teachers page. 

This presentation can replace : For teachers — Hedy, Introduction Hedy, Preparations and Teaching with Hedy.
**MIND**: The frequently made mistakes should stay on the page as is, because have not found a proper way to make it look better and still be as handy. 
MIND: The 'My classes' feature should - of course- also stay on this page. 

Here is the link to the presentation: https://view.genial.ly/61680ae0e003960dea8a5e8b/interactive-content-teacher-manual-hedy

And here is the code for embedding it: 
iframe: 
<div style="width: 100%;"><div style="position: relative; padding-bottom: 56.25%; padding-top: 0; height: 0;"><iframe frameborder="0" width="1200" height="675" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://view.genial.ly/61680ae0e003960dea8a5e8b" type="text/html" allowscriptaccess="always" allowfullscreen="true" scrolling="yes" allownetworking="all"></iframe> </div> </div>

script: 
<div class="container-wrapper-genially" style="position: relative; min-height: 400px; max-width: 100%;"><video class="loader-genially" autoplay="autoplay" loop="loop" playsinline="playsInline" muted="muted" style="position: absolute;top: 45%;left: 50%;transform: translate(-50%, -50%);width: 80px;height: 80px;margin-bottom: 10%"><source src="https://static.genial.ly/resources/panel-loader-low.mp4" type="video/mp4" />Your browser does not support the video tag.</video><div id="61680ae0e003960dea8a5e8b" class="genially-embed" style="margin: 0px auto; position: relative; height: auto; width: 100%;"></div></div><script>(function (d) { var js, id = "genially-embed-js", ref = d.getElementsByTagName("script")[0]; if (d.getElementById(id)) { return; } js = d.createElement("script"); js.id = id; js.async = true; js.src = "https://view.genial.ly/static/embed/embed.js"; ref.parentNode.insertBefore(js, ref); }(document));</script>

Replace parts of 'for teachers' page with this presentation
I've made a presentation to make the teacher page look better. This page can be embedded into the English for teachers page. 

This presentation can replace : For teachers — Hedy, Introduction Hedy, Preparations and Teaching with Hedy.
**MIND**: The frequently made mistakes should stay on the page as is, because have not found a proper way to make it look better and still be as handy. 
MIND: The 'My classes' feature should - of course- also stay on this page. 

Here is the link to the presentation: https://view.genial.ly/61680ae0e003960dea8a5e8b/interactive-content-teacher-manual-hedy

And here is the code for embedding it: 
iframe: 
<div style="width: 100%;"><div style="position: relative; padding-bottom: 56.25%; padding-top: 0; height: 0;"><iframe frameborder="0" width="1200" height="675" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://view.genial.ly/61680ae0e003960dea8a5e8b" type="text/html" allowscriptaccess="always" allowfullscreen="true" scrolling="yes" allownetworking="all"></iframe> </div> </div>

script: 
<div class="container-wrapper-genially" style="position: relative; min-height: 400px; max-width: 100%;"><video class="loader-genially" autoplay="autoplay" loop="loop" playsinline="playsInline" muted="muted" style="position: absolute;top: 45%;left: 50%;transform: translate(-50%, -50%);width: 80px;height: 80px;margin-bottom: 10%"><source src="https://static.genial.ly/resources/panel-loader-low.mp4" type="video/mp4" />Your browser does not support the video tag.</video><div id="61680ae0e003960dea8a5e8b" class="genially-embed" style="margin: 0px auto; position: relative; height: auto; width: 100%;"></div></div><script>(function (d) { var js, id = "genially-embed-js", ref = d.getElementsByTagName("script")[0]; if (d.getElementById(id)) { return; } js = d.createElement("script"); js.id = id; js.async = true; js.src = "https://view.genial.ly/static/embed/embed.js"; ref.parentNode.insertBefore(js, ref); }(document));</script>

