<reponame>pascalmolin/pasquizz<filename>app.py
#!/usr/bin/python
# copyright 2019 <NAME>

#####################################################################
#
# Databases
#
#####################################################################

import time
import random
import sys

if sys.version_info.major == 3:
    unicode = str
    xrange = range

VERSION='0.1'

class User(dict):
    """ name, date """
    def __init__(self, name):
        self['name'] = name
        self['date'] = time.time()

class Answers(list):
    """ user, quiz id, score, list choices """
    def __init__(self, uid, qid, quiz):
        self.score = None
        self.uid = uid
        self.qid = qid
        self.confirmed = False
        for question, choices in quiz:
            self.append(None)
    def set_choice(self, number, choice):
        self[number] = choice
    def set_score(self, score):
        self.score = score
class Choice:
    """ text, comment """
    def __init__(self, **kwargs):
        self.text = kwargs.get('text','')
        self.comment = kwargs.get('comment','')
        self.correct = kwargs.get('correct',False)
class Question(list):
    """ title, text, list of choices, index correct """
    def __init__(self, **kwargs):
        self.title = kwargs.get('title','')
        self.text =  kwargs.get('text','')
        self.image =  kwargs.get('image',None)
        self.correct = None
        for i,c in enumerate(kwargs.get('choices',[])):
            c = Choice(**c)
            self.append(c)
            if c.correct:
                assert self.correct == None, """
                    current implementation assumes there is only one
                    correct answer per question """
                self.correct = i
        assert self.correct != None, """ missing correct answer in question %s """ % self.text
    def grade(self, cid):
        """ cid a choice index, is choice number cid correct ? """
        return self.correct == cid
class Quiz(list):
    """
    list of (questions, [index of choices])
    """
    pass

class PageData(dict):
    template = 'invalid.html'
    def __init__(self, info, **kwargs):
        self.info = info
        dict.__init__(self, **kwargs)
class PageConfirm(PageData):
    template = 'confirm.html'
class PageQuestion(PageData):
    template = 'question.html'
class PageScore(PageData):
    template = 'score.html'

class QuizApp(dict):

    def __init__(self):
        self['auth'] = {}
        self['user'] = []
        self['assignment'] = []
        self['question'] = []
        self['quiz'] = []

    def number_of(self,db):
        assert db in self
        return len(self[db])

    def get(self, db, index):
        assert db in self and isinstance(self[db], list)
        assert index < len(self[db]), """ illegal index %i in table %s """ % (index, db)
        return self[db][index]

    def add(self, db, entry):
        assert db in self and isinstance(self[db], list)
        self[db].append(entry)
        return len(self[db]) - 1

    def new_user(self, name, auth):
        """ return index uid """
        assert auth not in self['auth'], "auth key %s already used"%auth
        uid = self.add('user', User(name))
        self['auth'][auth] = uid
        return uid

    def new_quiz(self,length=3,maxanswers=3):
        """ generates a random quiz, return index qid """
        quiz = Quiz()
        pool = self.number_of('question')
        if pool  < length:
            length = pool
        for iq in random.sample(xrange(pool), length):
            q = self.get('question',iq)
            # choose some choices, the first one is correct
            assert q.correct == 0, """ implementation assumes correct answer comes first, here correct = %i in question %s """ % (q.correct, q.text)
            c = random.sample(xrange(1,len(q)), maxanswers - 1)
            c.append(0)
            random.shuffle(c)
            quiz.append( (iq, c) )

        return self.add('quiz',quiz)

    def new_assignment(self, uid, qid):
        """ return index aid of new assignment """
        quiz = self.get('quiz',qid)
        answers = Answers(uid, qid, quiz)
        return self.add('assignment',answers)

    def post_answer(self, info, choice):
        """ save answer and return index of next question """
        uid, aid, number = info
        answers = self.get('assignment',aid)
        assert number < len(answers), """ wrong number %i >= len(assignment) """ % number
        if answers.confirmed:
            return None
        answers.set_choice(number, choice)
        return PageData((uid,aid,number+1))

    def post_confirmation(self, info):
        """ mark assignment as done and return score """
        uid, aid = info
        #user,answers,question = self.parse_token(token)
        answers = self.get('assignment',aid)
        answers.confirmed = True
        return self.get_score_page(info)

    def get_question_page(self, info):
        """
        info contains uid, aid, number
        returns the question data,
        If number > number of questions,
        return confirmation page or score instead.
        """
        uid, aid, number = info
        #user = self.get('user',uid)
        answers = self.get('assignment',aid)
        quiz = self.get('quiz',answers.qid)

        if number >= len(answers):
            return self.get_score_page((uid,aid))

        qid, choices_index = quiz[number]
        question = self.get('question',qid)
        choices = [ question[c] for c in choices_index ]
        return PageQuestion(info,
                   question=question,
                   number = number,
                   choices=choices,
                   selected=answers[number],
                   disabled=answers.confirmed)

    def grade(self, info):
        uid, aid = info
        user = self.get('user',uid)
        answers = self.get('assignment',aid)
        quiz = self.get('quiz',answers.qid)
        score = 0
        for choice, (qid, cid) in zip(answers,quiz):
            if app.config['verbose']:
                app.logger.info("choice %i in question %s"%(choice,(qid,cid)))
            question = self.get('question',qid)
            score += question.grade(cid[choice])
        answers.set_score(score)

    def get_score_page(self, info):
        uid, aid = info
        user = self.get('user',uid)
        answers = self.get('assignment',aid)
        if not answers.confirmed:
            return PageConfirm(info, user=user, answers=answers)
        if answers.score == None:
            self.grade(info)
        results = {
                'info': info,
                'name': user['name'],
                'score': answers.score,
                'questions': len(answers)
                }
        return PageScore(info, user=user,
                         answers=answers,
                         results=results)

    def load_questions_yaml(self, filename):
        import yaml

        with open(filename, 'r') as stream:
            try:
                for q in yaml.load_all(stream,Loader=yaml.SafeLoader):
                    self.add('question',Question(**q))
            except yaml.YAMLError as e:
                app.logger.info(e)

#####################################################################
#
# Web server
#
#####################################################################

from flask import Flask, request, render_template, abort, redirect, url_for, send_file, send_from_directory

from itsdangerous import (URLSafeTimedSerializer, Serializer, BadSignature, SignatureExpired, NoneAlgorithm)
quiz = QuizApp()
app = Flask('quiz')
app.config['verbose'] = False
app.config['SECRET_KEY'] = '<random>'
app.config['allow_register'] = False
app.config['number_questions'] = 5
app.config['number_choices'] = 3
app.config['token_method'] = 'default'
app.config['expiration_time'] = 0
app.config['lang'] = 'fr'
METHOD='GET'

def get_serializer(salt=''):
    if app.config['token_method'] == 'none':
        return Serializer(app.config['SECRET_KEY'],salt=salt,signer=None)
    elif app.config['token_method'] == 'default':
        return URLSafeTimedSerializer(app.config['SECRET_KEY'],salt=salt)
    else:
        return URLSafeTimedSerializer(app.config['SECRET_KEY'],salt=salt)

@app.template_filter('token')
def tokenize(data, salt='quiz',expiration=600):
    """
    creates a token for the given data
    """
    s = get_serializer(salt)
    if app.config['verbose']:
        app.logger.info("tokenize %s"%(data,))
    return s.dumps(data)

@app.template_filter('url_protected')
def url_protected(route,data,**kwargs):
    token = tokenize(data,salt=route)
    return url_for(route,token=token,**kwargs)

def load_token(token, salt=''):
    s = get_serializer(salt)
    try:
        if app.config['verbose']:
            app.logger.info("parse token %s"%(token,))
        data = s.loads(token)
    except SignatureExpired:
        # FIXME: return expiration page
        if app.config['verbose']:
            app.logger.info("signature expired")
        return None # valid token, but expired
    except BadSignature:
        if app.config['verbose']:
            app.logger.info("bad signature")
        return None # invalid token
    if app.config['verbose']:
        app.logger.info("got %s"%(data,))
    return data

from functools import wraps
def token_required(f):
    """
    requires a valid token, whose salt
    is the name of the embedded function
    """
    @wraps(f)
    def decorated(*args,**kwargs):
        token = request.args.get('token',None)
        if token is None:
            return render_template("illegal.html"), 401
        if app.config['verbose']:
            app.logger.info("[page %s, read access token]"%f.__name__)
        data = load_token(token, salt = f.__name__)
        if data is None:
            app.logger.info('[INVALID TOKEN] %s'%(token,))
            return render_template("illegal.html"), 401
        try:
            return f(*args,data=data,**kwargs)
        except AssertionError as e:
            app.logger.info("[INVALID] illegal database access %s",e)
            return render_template("illegal.html"), 401
        except TypeError as e:
            app.logger.info("[TYPE ERROR] wrong entry %s"%e)
            return render_template("illegal.html"), 401
    return decorated

def render_page(data,route=None):
    if data is None:
        if app.config['verbose']:
            app.logger.info("[ILLEGAL] empty page")
        return render_template('illegal.html')
    if isinstance(data, PageData) and route is None:
        return render_template(data.template, this=data)
    elif route:
        if app.config['verbose']:
            app.logger.info("[CREATE TOKEN] for page %s using data %s"%(route,data.info))
        return redirect(url_protected(route, data.info))
    else:
        if app.config['verbose']:
            app.logger.info("[BUG] no route to render %s"%(data,))
        return render_template('illegal.html')

@app.route('/')
def index():
    if app.config['token_method'] == 'none':
        auth = quiz.number_of('auth')
        return render_template('register.html', auth=auth)
    else:
        return render_template('index.html',token_link=app.config['allow_register'])

@app.route('/token', methods = [METHOD])
@token_required
def api_token(data=None):
    """ generates valid tokens

    FIXME:
    right now this is open bar

    In the future this page must be protected
    and tokens generated by authorized people only.
    """
    auth = quiz.number_of('auth')
    return render_template('token.html',auth=auth)

@app.route('/activate', methods = [METHOD])
@token_required
def api_activate(data=None):
    return render_template('register.html', auth=data)

@app.route('/api/register', methods = [METHOD])
@token_required
def api_register(data=None):
    """ token data (registration key) """
    auth = str(data)
    name = unicode(request.args.get('name'))
    uid = quiz.new_user(name, auth)
    qid = quiz.new_quiz(app.config['number_questions'],
                          app.config['number_choices'])
    aid = quiz.new_assignment(uid, qid)
    if app.config['verbose']:
        app.logger.info("created user %i,quiz %i,assignment %i"%(uid,qid,aid))
    return render_page(PageData((uid,aid,0)), 'api_question')

@app.route('/question', methods = [METHOD])
@token_required
def api_question(data=None):
    """ token data (uid, aid, number) """
    page = quiz.get_question_page(data)
    return render_page(page)

@app.route('/score', methods = [METHOD])
@token_required
def api_score(data=None):
    """ token data (uid, aid, number) """
    page = quiz.get_score_page(data)
    return render_page(page)

@app.route('/results', methods = [METHOD])
@token_required
def api_view(data=None):
    """ token data (name, score, uid, aid) """
    return render_template("results.html", this=data)

@app.route('/api/answer', methods = [METHOD])
@token_required
def api_answer(data=None):
    """ token data (uid, aid, number) """
    choice = int(request.args.get('choice'))
    data = quiz.post_answer(data, choice)
    return render_page(data, 'api_question')

@app.route('/api/submit', methods = [METHOD])
@token_required
def api_submit(data=None):
    """ token data (uid, aid) """
    app.logger.info('received token %s' % data)
    data = quiz.post_confirmation(data)
    return render_page(data, 'api_score')

import pyqrcode
try:
    from StringIO import StringIO
except ImportError:
    from io import BytesIO as StringIO

@app.route('/api/svg',methods = [METHOD])
@token_required
def api_svg(data=None):
    try:
        stream = StringIO()
        code = pyqrcode.create(data)
        code.svg(stream, scale=1)
        stream.seek(0)
        #if sys.version_info.major == 3:
        #    stream = io.BytesIO(stream.encode())
        return send_file(stream, mimetype='image/svg+xml')
    except e:
        return e

@app.route('/images/<path:filename>')
def api_images(filename):
    return send_from_directory('images',filename)


#####################################################################
#
# Command line application
#
#####################################################################

import click

@click.group()
def cli():
    pass

@cli.command()
@click.option('--debug', default=False, is_flag=True, help='turn on Flask debug mode')
@click.option('--verbose', default=False, is_flag=True, help='log requests')
@click.option('--allow-register', default=False, is_flag=True, help='provide registration link at index page')
@click.option('--number_questions', default=5, help='number of questions per quiz')
@click.option('--number_choices', default=3, help='number of choices per question')
@click.option('--secret_key', default='lilalilalou', help='secret key')
@click.option('--token_method', type=click.Choice(['none', 'default']), default='default', help='token method')
@click.option('--lang', type=click.Choice(['en', 'fr']), default='en', help='language')
@click.option('--port', default=8000, help='http port')
def server(debug,
        verbose,
        allow_register,
        number_questions,
        number_choices,
        secret_key,
        token_method,
        lang,
        port):
    app.config['verbose'] = verbose
    app.config['SECRET_KEY'] = secret_key
    app.config['allow_register'] = allow_register
    app.config['number_questions'] = number_questions
    app.config['number_choices'] = number_choices
    app.config['token_method'] = token_method
    app.config['port'] = port
    app.config['lang'] = lang
    app.run(debug=debug,port=port)

@cli.command()
@click.option('--yaml',default='questions.yaml',
        help='load questions file')
def build(yaml):
    pass

if __name__ == '__main__':
    quiz.load_questions_yaml('example.yaml')
    cli()

#!/usr/bin/python
# copyright 2019 <NAME>

#####################################################################
#
# Databases
#
#####################################################################

import time
import random
import sys

if sys.version_info.major == 3:
    unicode = str
    xrange = range

VERSION='0.1'

class User(dict):
    """ name, date """
    def __init__(self, name):
        self['name'] = name
        self['date'] = time.time()

class Answers(list):
    """ user, quiz id, score, list choices """
    def __init__(self, uid, qid, quiz):
        self.score = None
        self.uid = uid
        self.qid = qid
        self.confirmed = False
        for question, choices in quiz:
            self.append(None)
    def set_choice(self, number, choice):
        self[number] = choice
    def set_score(self, score):
        self.score = score
class Choice:
    """ text, comment """
    def __init__(self, **kwargs):
        self.text = kwargs.get('text','')
        self.comment = kwargs.get('comment','')
        self.correct = kwargs.get('correct',False)
class Question(list):
    """ title, text, list of choices, index correct """
    def __init__(self, **kwargs):
        self.title = kwargs.get('title','')
        self.text =  kwargs.get('text','')
        self.image =  kwargs.get('image',None)
        self.correct = None
        for i,c in enumerate(kwargs.get('choices',[])):
            c = Choice(**c)
            self.append(c)
            if c.correct:
                assert self.correct == None, """
                    current implementation assumes there is only one
                    correct answer per question """
                self.correct = i
        assert self.correct != None, """ missing correct answer in question %s """ % self.text
    def grade(self, cid):
        """ cid a choice index, is choice number cid correct ? """
        return self.correct == cid
class Quiz(list):
    """
    list of (questions, [index of choices])
    """
    pass

class PageData(dict):
    template = 'invalid.html'
    def __init__(self, info, **kwargs):
        self.info = info
        dict.__init__(self, **kwargs)
class PageConfirm(PageData):
    template = 'confirm.html'
class PageQuestion(PageData):
    template = 'question.html'
class PageScore(PageData):
    template = 'score.html'

class QuizApp(dict):

    def __init__(self):
        self['auth'] = {}
        self['user'] = []
        self['assignment'] = []
        self['question'] = []
        self['quiz'] = []

    def number_of(self,db):
        assert db in self
        return len(self[db])

    def get(self, db, index):
        assert db in self and isinstance(self[db], list)
        assert index < len(self[db]), """ illegal index %i in table %s """ % (index, db)
        return self[db][index]

    def add(self, db, entry):
        assert db in self and isinstance(self[db], list)
        self[db].append(entry)
        return len(self[db]) - 1

    def new_user(self, name, auth):
        """ return index uid """
        assert auth not in self['auth'], "auth key %s already used"%auth
        uid = self.add('user', User(name))
        self['auth'][auth] = uid
        return uid

    def new_quiz(self,length=3,maxanswers=3):
        """ generates a random quiz, return index qid """
        quiz = Quiz()
        pool = self.number_of('question')
        if pool  < length:
            length = pool
        for iq in random.sample(xrange(pool), length):
            q = self.get('question',iq)
            # choose some choices, the first one is correct
            assert q.correct == 0, """ implementation assumes correct answer comes first, here correct = %i in question %s """ % (q.correct, q.text)
            c = random.sample(xrange(1,len(q)), maxanswers - 1)
            c.append(0)
            random.shuffle(c)
            quiz.append( (iq, c) )

        return self.add('quiz',quiz)

    def new_assignment(self, uid, qid):
        """ return index aid of new assignment """
        quiz = self.get('quiz',qid)
        answers = Answers(uid, qid, quiz)
        return self.add('assignment',answers)

    def post_answer(self, info, choice):
        """ save answer and return index of next question """
        uid, aid, number = info
        answers = self.get('assignment',aid)
        assert number < len(answers), """ wrong number %i >= len(assignment) """ % number
        if answers.confirmed:
            return None
        answers.set_choice(number, choice)
        return PageData((uid,aid,number+1))

    def post_confirmation(self, info):
        """ mark assignment as done and return score """
        uid, aid = info
        #user,answers,question = self.parse_token(token)
        answers = self.get('assignment',aid)
        answers.confirmed = True
        return self.get_score_page(info)

    def get_question_page(self, info):
        """
        info contains uid, aid, number
        returns the question data,
        If number > number of questions,
        return confirmation page or score instead.
        """
        uid, aid, number = info
        #user = self.get('user',uid)
        answers = self.get('assignment',aid)
        quiz = self.get('quiz',answers.qid)

        if number >= len(answers):
            return self.get_score_page((uid,aid))

        qid, choices_index = quiz[number]
        question = self.get('question',qid)
        choices = [ question[c] for c in choices_index ]
        return PageQuestion(info,
                   question=question,
                   number = number,
                   choices=choices,
                   selected=answers[number],
                   disabled=answers.confirmed)

    def grade(self, info):
        uid, aid = info
        user = self.get('user',uid)
        answers = self.get('assignment',aid)
        quiz = self.get('quiz',answers.qid)
        score = 0
        for choice, (qid, cid) in zip(answers,quiz):
            if app.config['verbose']:
                app.logger.info("choice %i in question %s"%(choice,(qid,cid)))
            question = self.get('question',qid)
            score += question.grade(cid[choice])
        answers.set_score(score)

    def get_score_page(self, info):
        uid, aid = info
        user = self.get('user',uid)
        answers = self.get('assignment',aid)
        if not answers.confirmed:
            return PageConfirm(info, user=user, answers=answers)
        if answers.score == None:
            self.grade(info)
        results = {
                'info': info,
                'name': user['name'],
                'score': answers.score,
                'questions': len(answers)
                }
        return PageScore(info, user=user,
                         answers=answers,
                         results=results)

    def load_questions_yaml(self, filename):
        import yaml

        with open(filename, 'r') as stream:
            try:
                for q in yaml.load_all(stream,Loader=yaml.SafeLoader):
                    self.add('question',Question(**q))
            except yaml.YAMLError as e:
                app.logger.info(e)

#####################################################################
#
# Web server
#
#####################################################################

from flask import Flask, request, render_template, abort, redirect, url_for, send_file, send_from_directory

from itsdangerous import (URLSafeTimedSerializer, Serializer, BadSignature, SignatureExpired, NoneAlgorithm)
quiz = QuizApp()
app = Flask('quiz')
app.config['verbose'] = False
app.config['SECRET_KEY'] = '<random>'
app.config['allow_register'] = False
app.config['number_questions'] = 5
app.config['number_choices'] = 3
app.config['token_method'] = 'default'
app.config['expiration_time'] = 0
app.config['lang'] = 'fr'
METHOD='GET'

def get_serializer(salt=''):
    if app.config['token_method'] == 'none':
        return Serializer(app.config['SECRET_KEY'],salt=salt,signer=None)
    elif app.config['token_method'] == 'default':
        return URLSafeTimedSerializer(app.config['SECRET_KEY'],salt=salt)
    else:
        return URLSafeTimedSerializer(app.config['SECRET_KEY'],salt=salt)

@app.template_filter('token')
def tokenize(data, salt='quiz',expiration=600):
    """
    creates a token for the given data
    """
    s = get_serializer(salt)
    if app.config['verbose']:
        app.logger.info("tokenize %s"%(data,))
    return s.dumps(data)

@app.template_filter('url_protected')
def url_protected(route,data,**kwargs):
    token = tokenize(data,salt=route)
    return url_for(route,token=token,**kwargs)

def load_token(token, salt=''):
    s = get_serializer(salt)
    try:
        if app.config['verbose']:
            app.logger.info("parse token %s"%(token,))
        data = s.loads(token)
    except SignatureExpired:
        # FIXME: return expiration page
        if app.config['verbose']:
            app.logger.info("signature expired")
        return None # valid token, but expired
    except BadSignature:
        if app.config['verbose']:
            app.logger.info("bad signature")
        return None # invalid token
    if app.config['verbose']:
        app.logger.info("got %s"%(data,))
    return data

from functools import wraps
def token_required(f):
    """
    requires a valid token, whose salt
    is the name of the embedded function
    """
    @wraps(f)
    def decorated(*args,**kwargs):
        token = request.args.get('token',None)
        if token is None:
            return render_template("illegal.html"), 401
        if app.config['verbose']:
            app.logger.info("[page %s, read access token]"%f.__name__)
        data = load_token(token, salt = f.__name__)
        if data is None:
            app.logger.info('[INVALID TOKEN] %s'%(token,))
            return render_template("illegal.html"), 401
        try:
            return f(*args,data=data,**kwargs)
        except AssertionError as e:
            app.logger.info("[INVALID] illegal database access %s",e)
            return render_template("illegal.html"), 401
        except TypeError as e:
            app.logger.info("[TYPE ERROR] wrong entry %s"%e)
            return render_template("illegal.html"), 401
    return decorated

def render_page(data,route=None):
    if data is None:
        if app.config['verbose']:
            app.logger.info("[ILLEGAL] empty page")
        return render_template('illegal.html')
    if isinstance(data, PageData) and route is None:
        return render_template(data.template, this=data)
    elif route:
        if app.config['verbose']:
            app.logger.info("[CREATE TOKEN] for page %s using data %s"%(route,data.info))
        return redirect(url_protected(route, data.info))
    else:
        if app.config['verbose']:
            app.logger.info("[BUG] no route to render %s"%(data,))
        return render_template('illegal.html')

@app.route('/')
def index():
    if app.config['token_method'] == 'none':
        auth = quiz.number_of('auth')
        return render_template('register.html', auth=auth)
    else:
        return render_template('index.html',token_link=app.config['allow_register'])

@app.route('/token', methods = [METHOD])
@token_required
def api_token(data=None):
    """ generates valid tokens

    FIXME:
    right now this is open bar

    In the future this page must be protected
    and tokens generated by authorized people only.
    """
    auth = quiz.number_of('auth')
    return render_template('token.html',auth=auth)

@app.route('/activate', methods = [METHOD])
@token_required
def api_activate(data=None):
    return render_template('register.html', auth=data)

@app.route('/api/register', methods = [METHOD])
@token_required
def api_register(data=None):
    """ token data (registration key) """
    auth = str(data)
    name = unicode(request.args.get('name'))
    uid = quiz.new_user(name, auth)
    qid = quiz.new_quiz(app.config['number_questions'],
                          app.config['number_choices'])
    aid = quiz.new_assignment(uid, qid)
    if app.config['verbose']:
        app.logger.info("created user %i,quiz %i,assignment %i"%(uid,qid,aid))
    return render_page(PageData((uid,aid,0)), 'api_question')

@app.route('/question', methods = [METHOD])
@token_required
def api_question(data=None):
    """ token data (uid, aid, number) """
    page = quiz.get_question_page(data)
    return render_page(page)

@app.route('/score', methods = [METHOD])
@token_required
def api_score(data=None):
    """ token data (uid, aid, number) """
    page = quiz.get_score_page(data)
    return render_page(page)

@app.route('/results', methods = [METHOD])
@token_required
def api_view(data=None):
    """ token data (name, score, uid, aid) """
    return render_template("results.html", this=data)

@app.route('/api/answer', methods = [METHOD])
@token_required
def api_answer(data=None):
    """ token data (uid, aid, number) """
    choice = int(request.args.get('choice'))
    data = quiz.post_answer(data, choice)
    return render_page(data, 'api_question')

@app.route('/api/submit', methods = [METHOD])
@token_required
def api_submit(data=None):
    """ token data (uid, aid) """
    app.logger.info('received token %s' % data)
    data = quiz.post_confirmation(data)
    return render_page(data, 'api_score')

import pyqrcode
try:
    from StringIO import StringIO
except ImportError:
    from io import BytesIO as StringIO

@app.route('/api/svg',methods = [METHOD])
@token_required
def api_svg(data=None):
    try:
        stream = StringIO()
        code = pyqrcode.create(data)
        code.svg(stream, scale=1)
        stream.seek(0)
        #if sys.version_info.major == 3:
        #    stream = io.BytesIO(stream.encode())
        return send_file(stream, mimetype='image/svg+xml')
    except e:
        return e

@app.route('/images/<path:filename>')
def api_images(filename):
    return send_from_directory('images',filename)


#####################################################################
#
# Command line application
#
#####################################################################

import click

@click.group()
def cli():
    pass

@cli.command()
@click.option('--debug', default=False, is_flag=True, help='turn on Flask debug mode')
@click.option('--verbose', default=False, is_flag=True, help='log requests')
@click.option('--allow-register', default=False, is_flag=True, help='provide registration link at index page')
@click.option('--number_questions', default=5, help='number of questions per quiz')
@click.option('--number_choices', default=3, help='number of choices per question')
@click.option('--secret_key', default='lilalilalou', help='secret key')
@click.option('--token_method', type=click.Choice(['none', 'default']), default='default', help='token method')
@click.option('--lang', type=click.Choice(['en', 'fr']), default='en', help='language')
@click.option('--port', default=8000, help='http port')
def server(debug,
        verbose,
        allow_register,
        number_questions,
        number_choices,
        secret_key,
        token_method,
        lang,
        port):
    app.config['verbose'] = verbose
    app.config['SECRET_KEY'] = secret_key
    app.config['allow_register'] = allow_register
    app.config['number_questions'] = number_questions
    app.config['number_choices'] = number_choices
    app.config['token_method'] = token_method
    app.config['port'] = port
    app.config['lang'] = lang
    app.run(debug=debug,port=port)

@cli.command()
@click.option('--yaml',default='questions.yaml',
        help='load questions file')
def build(yaml):
    pass

if __name__ == '__main__':
    quiz.load_questions_yaml('example.yaml')
    cli()
#!/usr/bin/python # copyright 2019 <NAME> ##################################################################### # # Databases # ##################################################################### name, date user, quiz id, score, list choices text, comment title, text, list of choices, index correct current implementation assumes there is only one                     correct answer per question missing correct answer in question %s cid a choice index, is choice number cid correct ? list of (questions, [index of choices]) illegal index %i in table %s return index uid generates a random quiz, return index qid # choose some choices, the first one is correct implementation assumes correct answer comes first, here correct = %i in question %s return index aid of new assignment save answer and return index of next question wrong number %i >= len(assignment) mark assignment as done and return score #user,answers,question = self.parse_token(token) info contains uid, aid, number         returns the question data,         If number > number of questions,         return confirmation page or score instead. #user = self.get('user',uid) ##################################################################### # # Web server # ##################################################################### creates a token for the given data # FIXME: return expiration page # valid token, but expired # invalid token requires a valid token, whose salt     is the name of the embedded function generates valid tokens      FIXME:     right now this is open bar      In the future this page must be protected     and tokens generated by authorized people only. token data (registration key) token data (uid, aid, number) token data (uid, aid, number) token data (name, score, uid, aid) token data (uid, aid, number) token data (uid, aid) #if sys.version_info.major == 3: #    stream = io.BytesIO(stream.encode()) ##################################################################### # # Command line application # #####################################################################