Martin Czygan avatar Martin Czygan committed 3bdd56d

document tests; sqla models; validators; api stub

Comments (0)

Files changed (27)

audrid/__init__.py

+# coding: utf-8
+"""
+The main application.
+Include MethodRewriteMiddleware to piggyback PUT/DELETE on POST.
+"""
+from flask import Flask
+from flask.ext.sqlalchemy import SQLAlchemy
+from werkzeug import url_decode
+
+class MethodRewriteMiddleware(object):
+    def __init__(self, app):
+        self.app = app
+
+    def __call__(self, environ, start_response):
+        if 'METHOD_OVERRIDE' in environ.get('QUERY_STRING', ''):
+            args = url_decode(environ['QUERY_STRING'])
+            method = args.get('__METHOD_OVERRIDE__')
+            if method:
+                method = method.encode('ascii', 'replace')
+                environ['REQUEST_METHOD'] = method
+        return self.app(environ, start_response)
+
+app = Flask(__name__)
+app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://audrid:audrid@localhost/audrid'
+# app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/audrid.db'
+app.config['ES_HOST'] = 'localhost'
+app.config['ES_PORT'] = '9200'
+app.config['SECRET_KEY'] = 'sdasd78sadhjahsd7ll8sdL%$§%<f6776ad328989=)(&%'
+app.wsgi_app = MethodRewriteMiddleware(app.wsgi_app)
+db = SQLAlchemy(app)
+
+from views import *
+#!/usr/bin/env python
+# coding: utf-8
+
+from __future__ import print_function
+
+from audrid import (app, db, models)
+from flask.ext.script import Manager, prompt_bool
+import pyes
+import sys
+
+manager = Manager(app)
+
+@manager.command
+def create_db():
+    """ Create the database.
+    """
+    db.create_all(app=app)
+
+@manager.command
+def drop_db():
+    """ Drop the database.
+    """
+    if prompt_bool("Are you sure you want to lose all your data"):
+        db.drop_all(app=app)
+
+@manager.command
+def drop_index():
+    """ Purge ES index.
+    """
+    try:
+        es = pyes.ES('127.0.0.1:9200')
+        es.delete_index_if_exists('audrid')
+    except Exception as exc:
+        print(exc, file=sys.stderr)
+
+def create_index_mapping():
+    pass
+
+@manager.command
+def create_sample_users():  
+    """ Create admin and guest user.
+    """
+    try:
+        admin = models.User(username='admin', email='martin.czygan@gmail.com', password='admin', group='admin')
+        guest = models.User(username='guest', email='guest@example.com', password='guest')
+        db.session.add_all([admin, guest])
+        db.session.commit()
+        print('added admin:admin')
+        print('added guest:guest')
+    except Exception as exc:
+        db.session.rollback()
+        print(exc, file=sys.stderr)
+
+if __name__ == "__main__":
+    manager.run()
+# coding: utf-8
+
+"""
+Database mappings.
+"""
+
+from audrid import (app, db)
+from audrid.utils import random_id, random_id_fun
+import datetime
+import json
+
+class User(db.Model):
+    """ A application user.
+    """
+    id = db.Column(db.Integer, primary_key=True)
+    username = db.Column(db.String(80), unique=True, nullable=False)
+    password = db.Column(db.String(80), nullable=False)
+    
+    email = db.Column(db.String(120), unique=True, nullable=False)
+    group = db.Column(db.String(120), nullable=False)
+
+    active = db.Column(db.Boolean, nullable=False, default=True)
+
+    def __init__(self, username=None, email=None, password=None, group='guest', active=True):
+        self.username = username
+        self.password = password
+        self.email = email
+        self.group = group
+        self.active = active
+
+    def __repr__(self):
+        return '<User %s, %s>' % (self.username, self.email)
+
+
+class Document(db.Model):
+    """ A document. Can be a pool or an audit.
+    """
+    added_id = db.Column(db.Integer, primary_key=True)
+
+    id = db.Column(db.String(16), unique=True, nullable=False)
+    body = db.Column(db.LargeBinary, nullable=False)
+
+    deleted = db.Column(db.Boolean, nullable=False, default=False)
+
+    def __init__(self, id=None, body=None, deleted=False):
+        """ Last exit database.
+        """
+        self.id = id or random_id()
+        self.body = body
+        self.deleted = deleted
+
+    def __repr__(self):
+        return '<Document %s, length=%s>' % (self.id, len(self.body))
+
+    def to_python(self):
+        """ Return the python representation (dictionary) of this document.
+        """
+        return json.loads(self.body)
+
+class Log(db.Model):
+    """ The main audit log.
+    """
+    added_id = db.Column(db.Integer, primary_key=True)
+
+    document_id = db.Column(db.String(16), db.ForeignKey('document.id'), nullable=False)
+    body = db.Column(db.LargeBinary)
+    
+    # what, when, who
+    tag = db.Column(db.String(32), nullable=False)
+    date = db.Column(db.DateTime, nullable=False)
+    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+
+    def __init__(self, document_id=None, body=None, 
+            date=None, tag=None, user_id=None):
+        self.document_id = document_id
+        self.body = body
+        self.tag = tag
+        self.date = date
+        self.user_id = user_id
+
+    def __repr__(self):
+        return '<Log %s <%s>>' % (self.added_id, self.date)
+
+class Exam(db.Model):
+    """ Abstract exam.
+    """
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.String(128), nullable=False, unique=True)
+    description = db.Column(db.Text())
+
+    # The root pool
+    pool_id = db.Column(db.String(16), db.ForeignKey('document.id'), nullable=False)
+    
+    public = db.Column(db.Boolean, nullable=False, default=True)    
+    secret_key = db.Column(db.String(32), nullable=False, default=random_id_fun(length=32))
+
+    def __init__(self, name=None, description=None, public=True, 
+        pool_id=None, secret_key=None):
+        self.name = name
+        self.description = description
+        self.public = public
+        self.pool_id = pool_id
+        if self.secret_key:
+            self.secret_key = secret_key
+
+    def __repr__(self):
+        return '<Exam %s, %s>' % (self.id, self.name)
+
+class Audit(db.Model):
+    """ Connects a user and an exam. E.g. 'Geography 1' to user 12.
+    """
+    id = db.Column(db.Integer, primary_key=True)
+
+    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+    exam_id = db.Column(db.Integer, db.ForeignKey('exam.id'), nullable=False)
+    audit_id = db.Column(db.String(16), db.ForeignKey('document.id'), nullable=False)
+
+    started = db.Column(db.DateTime(), nullable=False)
+    finished = db.Column(db.DateTime(), nullable=True, default=None)
+
+    def __init__(self, user_id=None, exam_id=None, audit_id=None, started=None):
+        self.user_id = user_id
+        self.exam_id = exam_id
+        self.audit_id = audit_id
+        if started == None:
+            self.started = datetime.datetime.now()
+        else:
+            self.started = started
+
+    def __repr__(self):
+        return '<Audit %s, %s>' % (self.id, self.started)
+
+# coding: utf-8
+
+import datetime
+import random
+import string
+
+def random_id(length=6):
+    """ Return a random char identifier. 
+    """
+    return ''.join([ random.choice(
+        string.lowercase + string.digits) for _ in range(length) ])
+
+def random_id_fun(length=6):
+    """ Return a random id generator.
+    """
+    def inner():
+        return random_id(length=length)
+    return inner
+
+def isodate():
+    return datetime.datetime.now().isoformat()
+

audrid/validators.py

+# coding: utf-8
+"""
+Document schemes.
+"""
+
+from voluptuous import (
+    all, 
+    any, 
+    boolean,
+    length, 
+    range, 
+    required, 
+    Schema,
+    Invalid, 
+)
+
+def uniq(key='id', msg=None):
+    uniq, seen = set(), []
+    def f(v):
+        seen.append(v.get(key, None))
+        uniq.add(v.get(key, None))
+        if len(uniq) < len(seen):
+            raise Invalid('duplicate %s detected' % key, seen)
+        return v
+    return f
+
+options = Schema({
+    'case_sensititive' : boolean(),
+})
+
+cloze = Schema({  
+    required('kind') : 'cloze',
+    'options' : dict,
+    required('id'): all(unicode, length(min=6, max=10)),
+    required('task'): all(unicode, length(min=10, max=65535)),    
+    required('text'): unicode,
+    required('gaps'): dict,
+    required('answer'): dict,
+})
+
+text = Schema({
+    required('kind') : 'text',
+    'options' : dict,
+    required('id'): all(unicode, length(min=6, max=10)),
+    required('task'): all(unicode, length(min=10, max=65535)),    
+    required('answer'): unicode,
+})
+
+single = Schema({
+    required('kind') : 'single',
+    'options' : dict,    
+    required('id'): all(unicode, length(min=6, max=10)),
+    required('task'): all(unicode, length(min=10, max=65535)),
+    required('answer'): unicode,
+})
+
+choice = Schema({
+    required('id'): all(unicode, length(min=1, max=10)),
+    required('value'): unicode,
+    required('correct'): boolean(),
+})
+
+multiple_choice = Schema({
+    required('kind') : 'mc',
+    'options' : dict,
+    required('id'): all(unicode, length(min=6, max=10)),
+    required('task'): all(unicode, length(min=10, max=65535)),
+    required('choices'): [choice],
+    required('answer'): list,
+})
+
+mapping = Schema({
+    required('kind') : 'mapping',
+    'options' : dict,
+    required('id'): all(unicode, length(min=6, max=10)),
+    required('task'): all(unicode, length(min=10, max=65535)),
+    required('mapping'): dict,
+    required('answer'): dict,
+})
+
+pool = Schema({
+    required('id'): all(unicode, length(min=2, max=10)),
+    required('tasks'): [all(uniq(key='id'), any(
+        cloze,
+        mapping,
+        multiple_choice,
+        single,
+        text,
+    ))],
+})
+
+audit = Schema({
+    required('id'): all(unicode, length(min=6, max=10)),
+    required('user') : int,
+    required('pool') : pool,
+})
+
+if __name__ == '__main__':
+    t = mapping({
+        "kind"      : u"mapping", 
+        "id"        : u"country1", 
+        "task"      : u"Assign capitals to countries!", 
+        "answer"    : {},
+        "mapping"   : {
+            "Berlin" : u"Germany",
+            "Paris"  : u"France",
+            "Budapest" : u"Hungary",
+        },
+    })
+
+    u = text({
+        "kind"      : u"text", 
+        "id"        : u"text11", 
+        "task"      : u"Write a poem!", 
+        "answer"    : u"",
+    })
+
+    p = pool({
+        "id" : u"geo", 
+        "tasks" : [t]
+    })
+
+    a = audit({
+        "id" : u"67s9hp",
+        "user" : 1,
+        "pool" : p
+    })

audrid/views/__init__.py

+from api import *

audrid/views/api.py

+# coding: utf-8
+"""
+API methods.
+"""
+
+from audrid import app
+from flask import (jsonify)
+
+@app.route('/api/info', methods=['GET'])
+def info():
+    return jsonify(info='Audrid API', version="1")

tests/documents/cloze0.json

+{
+    "id" : "cloze0",
+    "task" : "Please fill out the blanks",
+    "text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent metus libero, dignissim at porttitor vitae...",
+    "kind" : "cloze",
+    "gaps" : {
+        "1" : "World"
+    },
+    "answer" : {}
+}

tests/documents/cloze1.json

+{
+    "id" : "cloze1",
+    "task" : "Please fill out the blanks",
+    "text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent metus libero, dignissim at porttitor vitae...",
+    "gaps" : {
+        "1" : "World"
+    },
+    "answer" : {}
+}

tests/documents/cloze2.json

+{
+    "id" : "cloze2",
+    "task" : "Please fill out the blanks",
+    "text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent metus libero, dignissim at porttitor vitae...",
+    "gaps" : [],
+    "answer" : {}
+}

tests/documents/cloze3.json

+{
+    "id" : "cloze3",
+    "task" : "Please fill out the blanks",
+    "text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent metus libero, dignissim at porttitor vitae...",
+    "kind" : "cloze",
+    "gaps" : {
+        "1" : "World"
+    }
+}

tests/documents/cloze4.json

+{
+    "id" : "cloze4",
+    "task" : "Please fill out the blanks",
+    "text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent metus libero, dignissim at porttitor vitae...",
+    "kind" : "cloze",
+    "gaps" : {
+        "1" : "World"
+    },
+    "answer" : {},
+    "answer" : []
+}
Add a comment to this file

tests/documents/empty.json

Empty file added.

tests/documents/mapping0.json

+{
+    "id" : "mapping0",
+    "task" : "Please fill out the blanks",
+    "kind" : "mapping",
+    "mapping" : {
+        "1" : "World"
+    },
+    "answer" : {}
+}

tests/documents/mapping1.json

+{
+    "id" : "mapping1",
+    "task" : "Please fill out the blanks",
+    "kind" : "mapping",
+    "mapping" : {
+        "1" : "World"
+    },
+    "answer" : "Hello"
+}

tests/documents/mc0.json

+{
+    "id" : "mc----0",
+    "task" : "Please fill out the blanks",
+    "kind" : "mc",
+    "choices" : [
+        {"id" : "1", "value" : "Hello", "correct" : true},
+        {"id" : "2", "value" : "Go", "correct" : true},
+        {"id" : "3", "value" : "No", "correct" : false}
+    ],
+    "answer" : []
+}

tests/documents/mc1.json

+{
+    "id" : "mc---1",
+    "task" : "Please fill out the blanks",
+    "kind" : "mc",
+    "choices" : {},
+    "answer" : "Hello"
+}

tests/documents/pool0.json

+{
+    "id" : "87asds8",
+    "tasks" : []
+}

tests/documents/pool1.json

+{
+    "id" : "87asds8",
+    "tasks" : {}
+}

tests/documents/pool2.json

+{
+    "id" : "87asds8",
+    "tasks" : [
+        {
+            "id" : "sadfdf",
+            "kind" : "text",
+            "task" : "Hello World. Write something.",
+            "answer" : ""
+        }
+    ]
+}

tests/documents/pool3.json

+{
+    "id" : "87asds8",
+    "tasks" : [
+        {
+            "id" : "sadfdf",
+            "kind" : "text",
+            "task" : "Hello World. Write something.",
+            "answer" : ""
+        },
+        {
+            "id" : "sadfdf",
+            "kind" : "text",
+            "task" : "Hello World. Write something.",
+            "answer" : ""
+        },
+        {
+            "id" : "sadfdf",
+            "kind" : "text",
+            "task" : "Hello World. Write something.",
+            "answer" : ""
+        },
+        {
+            "id" : "sadfdf",
+            "kind" : "text",
+            "task" : "Hello World. Write something.",
+            "answer" : ""
+        }
+    ]
+}

tests/documents/pool4.json

+{
+    "id" : "87asds8",
+    "tasks" : [
+        {
+            "id" : "sadfdf1",
+            "kind" : "text",
+            "task" : "Hello World. Write something.",
+            "answer" : ""
+        },
+        {
+            "id" : "sadfdf2",
+            "kind" : "text",
+            "task" : "Hello World. Write something.",
+            "answer" : ""
+        },
+        {
+            "id" : "sadfdf3",
+            "kind" : "text",
+            "task" : "Hello World. Write something.",
+            "answer" : ""
+        },
+        {
+            "id" : "sadfdf4",
+            "kind" : "text",
+            "task" : "Hello World. Write something.",
+            "answer" : ""
+        }
+    ]
+}

tests/documents/single0.json

+{
+    "id" : "text-0",
+    "task" : "Write a poem",
+    "answer" : "",
+    "kind" : "single"
+}

tests/documents/single1.json

+{
+    "id" : "text0",
+    "task" : "Write a poem",
+    "text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent metus libero, dignissim at porttitor vitae...",
+    "answer" : ["Wrong"],
+    "kind" : "single"
+}

tests/documents/text0.json

+{
+    "id" : "text-0",
+    "task" : "Write a poem",
+    "answer" : "",
+    "kind" : "text"
+}

tests/documents/text1.json

+{
+    "id" : "text0",
+    "task" : "Write a poem",
+    "text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent metus libero, dignissim at porttitor vitae...",
+    "answer" : {
+    	"that" : "wrong"
+    },
+    "kind" : "text"
+}

tests/test_validators.py

+# coding: utf-8
+
+from voluptuous import InvalidList
+from audrid.validators import (
+    cloze,
+    text,
+    single,
+    multiple_choice,
+    mapping,
+    pool,
+)
+import unittest
+import json
+import os
+
+def openrel(filename):
+    """ Return a handle on a filename, relative to this test file and don't care
+        where this is called from.
+    """
+    return open(os.path.join(os.path.abspath(
+        os.path.dirname(__file__)), filename))
+
+class TaskValidatorTests(unittest.TestCase):
+
+    def should_pass_on(self, filenames, schema=None):
+        for fn in filenames:
+            with openrel(fn) as handle:
+                content = json.load(handle)
+                self.assertEqual(content, schema(content))
+
+    def should_fail_on(self, filename_exc_tuples, schema=None):
+        for fn, exc in filename_exc_tuples:
+            with self.assertRaises(exc):
+                with openrel(fn) as handle:
+                    schema(json.load(handle))
+
+    def test_cloze(self):
+        valid_files = [
+            "documents/cloze0.json",
+        ]
+        invalid_files = [
+            ("documents/cloze1.json", InvalidList),
+            ("documents/cloze2.json", InvalidList),
+            ("documents/cloze3.json", InvalidList),
+            ("documents/cloze4.json", InvalidList),
+            ("documents/empty.json", ValueError),
+        ]
+
+        self.should_pass_on(valid_files, schema=cloze)
+        self.should_fail_on(invalid_files, schema=cloze)
+
+    def test_text(self):
+        valid_files = [
+            "documents/text0.json",
+        ]
+        invalid_files = [
+            ("documents/text1.json", InvalidList),
+        ]
+
+        self.should_pass_on(valid_files, schema=text)
+        self.should_fail_on(invalid_files, schema=text)
+
+    def test_mapping(self):
+        valid_files = [
+            "documents/mapping0.json",
+        ]
+        invalid_files = [
+            ("documents/mapping1.json", InvalidList),
+            ("documents/empty.json", ValueError),
+        ]
+
+        self.should_pass_on(valid_files, schema=mapping)
+        self.should_fail_on(invalid_files, schema=mapping)
+
+    def test_single(self):
+        valid_files = [
+            "documents/single0.json",
+        ]
+        invalid_files = [
+            ("documents/single1.json", InvalidList),
+            ("documents/empty.json", ValueError),
+        ]
+
+        self.should_pass_on(valid_files, schema=single)
+        self.should_fail_on(invalid_files, schema=single)
+
+    def test_multiple_choice(self):
+        self.maxDiff = None
+        valid_files = [
+            "documents/mc0.json",
+        ]
+        invalid_files = [
+            ("documents/mc1.json", InvalidList),
+            ("documents/empty.json", ValueError),
+        ]
+
+        self.should_pass_on(valid_files, schema=multiple_choice)
+        self.should_fail_on(invalid_files, schema=multiple_choice)
+
+class PoolValidatorTests(unittest.TestCase):
+
+    def should_pass_on(self, filenames, schema=None):
+        for fn in filenames:
+            with openrel(fn) as handle:
+                content = json.load(handle)
+                self.assertEqual(content, schema(content))
+
+    def should_fail_on(self, filename_exc_tuples, schema=None):
+        for fn, exc in filename_exc_tuples:
+            with self.assertRaises(exc):
+                with openrel(fn) as handle:
+                    schema(json.load(handle))
+
+    def test_pool(self):
+        valid_files = [
+            "documents/pool0.json",
+            "documents/pool2.json",
+            "documents/pool4.json",
+        ]
+        invalid_files = [
+            ("documents/pool1.json", InvalidList),
+            ("documents/pool3.json", InvalidList),
+            ("documents/empty.json", ValueError),
+        ]
+
+        self.should_pass_on(valid_files, schema=pool)
+        self.should_fail_on(invalid_files, schema=pool)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.