Commits

Martin Czygan committed 88ab47d

api snapshot

  • Participants
  • Parent commits 3bdd56d

Comments (0)

Files changed (9)

+API documentation
+=================
+
+Objects:
+
+	* tasks
+	* pools
+	* exams
+	* audits
+	* corrections
+
+Users
+-----
+
+* `GET    /users`
+* `POST   /users`
+* `DELETE /pools/<id>`
+
+Pools
+-----
+
+* `GET    /pools`
+* `GET    /pools/<id>`
+* `POST   /pools`
+* `PUT    /pools/<id>`
+* `DELETE /pools/<id>`
+
+... and special methods for subresource tasks:
+
+* `GET    /pools/<id>/tasks`
+* `POST   /pools/<id>/tasks`
+* `PUT    /pools/<id>/tasks/<id>`
+* `DELETE /pools/<id>/tasks/<id>`
+
+Exams
+-----
+
+* `GET    /exams`
+* `POST   /exams`
+* `DELETE /exams/<id>`
+
+Audits
+------
+
+* `GET    /audits`
+* `POST   /audits`
+* `DELETE /audits/<id>`
+
+An audit has different stages:
+
+1. registered
+2. started
+3. finished
+4. corrected
+
+A user has an id, email, first name and last name and belongs to a group.
+Group membership determine permissions: guest, editor, admin.
+
+Pools and trials are documents.
+Exams and audits are relations.
+
+Pools and trials are documents, since they contain they are flexible and nested.
+
+Exams and audits are relational.
+
+
+
 
 from audrid import (app, db)
 from audrid.utils import random_id, random_id_fun
+from audrid import validators as V
 import datetime
 import json
 
+# def create_pool(pool=None, user_id=None):
+#     """ Create a task (validate, persist and audit).
+#     """
+#     pool = V.pool(pool)
+#     try:
+#         doc = Document(id=pool['id'], body=json.dumps(pool))
+#         log = Log(
+#             document_id=pool['id'], 
+#             body=json.dumps(pool), 
+#             tag='pool_created', 
+#             user_id=user_id, 
+#             date=datetime.datetime.now())
+#         db.session.add(doc)
+#         db.session.flush()
+#         db.session.add(log)
+#         db.session.commit()
+#     except Exception as exc:
+#         db.session.rollback()
+#         raise exc
+
+# def update_pool(pool=None, user_id=None):
+#     """ Update a pool.
+#     """
+#     pool = V.pool(pool)
+#     doc = Document.query.filter_by(
+#         id=pool['id']).filter_by(
+#         deleted=False).first_or_404()
+#     try:
+#         doc.body = json.dumps(pool)
+#         log = Log(
+#             document_id=pool['id'], 
+#             body=json.dumps(pool), 
+#             tag='pool_updated', 
+#             user_id=user_id, 
+#             date=datetime.datetime.now())
+#         db.session.add(doc)
+#         db.session.flush()
+#         db.session.add(log)
+#         db.session.commit()
+#     except Exception as exc:
+#         # log error
+#         db.session.rollback()
+
+# def delete_pool(pid=None, user_id=None):
+#     """ Delete a pool.
+#     """
+#     doc = Document.query.filter_by(
+#         id=pid).filter_by(
+#         deleted=False).first_or_404()
+#     try:
+#         doc.deleted = True
+#         log = Log(
+#             document_id=pid, 
+#             body=json.dumps({}), 
+#             tag='pool_deleted', 
+#             user_id=user_id, 
+#             date=datetime.datetime.now())
+#         db.session.add(log)
+#         db.session.commit()
+#     except Exception as exc:
+#         # log error
+#         db.session.rollback()
+
+
 class User(db.Model):
     """ A application user.
     """
 
     id = db.Column(db.String(16), unique=True, nullable=False)
     body = db.Column(db.LargeBinary, nullable=False)
+    type = db.Column(db.String(32), nullable=False)
 
     deleted = db.Column(db.Boolean, nullable=False, default=False)
 
-    def __init__(self, id=None, body=None, deleted=False):
+    def __init__(self, id=None, body=None, type=None, deleted=False):
         """ Last exit database.
         """
         self.id = id or random_id()
         self.body = body
+        self.type = type
         self.deleted = deleted
 
     def __repr__(self):
-        return '<Document %s, length=%s>' % (self.id, len(self.body))
+        return '<Document %s, type=%s, length=%s, deleted=%s>' % (
+            self.id, self.type, len(self.body), self.deleted)
 
     def to_python(self):
         """ Return the python representation (dictionary) of this document.
         self.user_id = user_id
 
     def __repr__(self):
-        return '<Log %s <%s>>' % (self.added_id, self.date)
+        return '<Log %s, %s, %s, %s>' % (self.added_id, self.document_id, self.tag, self.date)
 
 class Exam(db.Model):
     """ Abstract exam.
     public = db.Column(db.Boolean, nullable=False, default=True)    
     secret_key = db.Column(db.String(32), nullable=False, default=random_id_fun(length=32))
 
+    # deleted 
+    deleted = db.Column(db.Boolean, nullable=False, default=False)
+
     def __init__(self, name=None, description=None, public=True, 
         pool_id=None, secret_key=None):
         self.name = name
         if self.secret_key:
             self.secret_key = secret_key
 
+    def as_dict(self):
+        return {
+            "id" : self.id,
+            "name" : self.name,
+            "description" : self.description,
+            "pool_id" : self.pool_id,
+            "public" : self.public,
+            "secret_key" : self.secret_key,
+            "deleted" : self.deleted
+        }
+
     def __repr__(self):
         return '<Exam %s, %s>' % (self.id, self.name)
 
 
     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)
+    # accumulate answers here
+    trial_id = db.Column(db.String(16), db.ForeignKey('document.id'), nullable=False)
 
-    started = db.Column(db.DateTime(), nullable=False)
+    # start, end date and duration
+    started = db.Column(db.DateTime(), nullable=True)
     finished = db.Column(db.DateTime(), nullable=True, default=None)
+    duration = db.Column(db.Integer, nullable=False, default=60) # in minutes
+
+    status = db.Column(db.String(32), nullable=False, default='registered')
 
-    def __init__(self, user_id=None, exam_id=None, audit_id=None, started=None):
+    def __init__(self, user_id=None, exam_id=None, trial_id=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
+        self.trial_id = trial_id
 
     def __repr__(self):
-        return '<Audit %s, %s>' % (self.id, self.started)
-
+        return '<Audit %s, %s, %s>' % (self.id, self.status, self.trial_id)
+
+    def as_dict(self):
+        return {
+            "user_id" : self.user_id,
+            "exam_id" : self.exam_id,
+            "trial_id" : self.trial_id,
+            "started" : self.started,
+            "finished" : self.finished,
+            "duration" : self.duration,
+            "status" : self.status
+        }
 def random_id(length=6):
     """ Return a random char identifier. 
     """
-    return ''.join([ random.choice(
+    return u''.join([ random.choice(
         string.lowercase + string.digits) for _ in range(length) ])
 
 def random_id_fun(length=6):

audrid/validators.py

     Invalid, 
 )
 
-def uniq(key='id', msg=None):
-    uniq, seen = set(), []
+def unique(schema, key='id'):
+    """
+    Check whether a key's value is unique within a list of dictionaries.
+
+    >>> validate = Schema(unique([dict]))
+    >>> validate([{"id" : 1}, {"id" : 1}])
+    Traceback (most recent call last):
+    ...
+    InvalidList: duplicate <id> detected
+    
+    >>> validate([{"id" : 1}, {"x" : 1}, {"y" : 1}])
+    [{'id': 1}, {'x': 1}, {'y': 1}]
+
+    >>> validate([{"id" : 1}, {"id" : 2}])
+    [{'id': 1}, {'id': 2}]
+    """
     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
+        uniq, ctr = set(), 0
+        for item in v:
+            if key in item:
+                uniq.add(item.get(key, None))
+                ctr += 1
+        if len(uniq) < ctr:
+            raise Invalid('duplicate <%s> detected' % key)
+        return schema(v)
+    schema = Schema(schema)
     return f
 
 options = Schema({
     'case_sensititive' : boolean(),
 })
 
-cloze = Schema({  
+cloze = Schema({
     required('kind') : 'cloze',
     'options' : dict,
     required('id'): all(unicode, length(min=6, max=10)),
     required('answer'): dict,
 })
 
+task = Schema(any(mapping, multiple_choice, single, text, cloze))
+
 pool = Schema({
-    required('id'): all(unicode, length(min=2, max=10)),
-    required('tasks'): [all(uniq(key='id'), any(
+    required('id'): all(unicode, length(min=2, max=12)),
+    required('tasks'): unique([any(
         cloze,
         mapping,
         multiple_choice,
         single,
         text,
-    ))],
+    )]),
 })
 
-audit = Schema({
-    required('id'): all(unicode, length(min=6, max=10)),
+trial = Schema({
+    required('id'): all(unicode, length(min=6, max=12)),
     required('user') : int,
     required('pool') : pool,
 })
 
+# for the wire
+exam = Schema({
+    required('pool_id'): all(unicode, length(min=2, max=12)),
+    required('name'): unicode,
+    'description' : unicode,
+    'public' : boolean(),
+})
+
+audit = Schema({
+    required('user_id'): int,
+    required('exam_id'): int,
+})
+
 if __name__ == '__main__':
+    # Examples
     t = mapping({
         "kind"      : u"mapping", 
         "id"        : u"country1", 
 
     p = pool({
         "id" : u"geo", 
-        "tasks" : [t]
+        "tasks" : [t, u]
     })
 
-    a = audit({
+    t = trial({
         "id" : u"67s9hp",
         "user" : 1,
         "pool" : p
     })
+
+    import doctest
+    doctest.testmod()

audrid/views/api.py

 API methods.
 """
 
-from audrid import app
-from flask import (jsonify)
+from audrid import (app, db)
+from audrid.utils import (random_id)
+from audrid import validators as V
+from audrid.models import Document, Log, User, Exam, Audit
+from flask import (jsonify, request)
+from functools import wraps
+import json
+import datetime
 
-@app.route('/api/info', methods=['GET'])
-def info():
+def authorization_required(f):
+    @wraps(f)
+    def decorated_function(*args, **kwargs):
+        try:
+            username = request.authorization.username
+            user_obj = User.query.filter_by(username=username).first()
+            if not user_obj.password == request.authorization.password:
+                return jsonify({"message" : "Not authorized"}), 401
+        except Exception as exc:
+            return jsonify({"message" : "Not authorized"}), 401
+        # app.logger.debug('successfully authenticated %s' % user_obj)
+        kwargs.update({"user" : user_obj})
+        return f(*args, **kwargs)
+    return decorated_function
+
+@app.route('/api/v1/info', methods=['GET'])
+@authorization_required
+def info(user=None):
     return jsonify(info='Audrid API', version="1")
+
+@app.route('/api/v1/pools', methods=['GET'])
+@authorization_required
+def pools_get(user=None):
+    docs = Document.query.filter_by(deleted=False).filter_by(type='pool').all()
+    bodies = [ d.to_python() for d in docs ]
+    return jsonify(pools=bodies)
+
+@app.route('/api/v1/pools', methods=['POST'])
+@authorization_required
+def pools_post(user=None):
+    # validation
+    try:
+        pool = V.pool(json.loads(request.data))
+    except Exception as exc:
+        # app.logger.error(exc)
+        return jsonify(message='error during validation: %s' % exc), 400
+    
+    # create the document and log entry
+    try:
+        doc = Document(id=pool['id'], body=json.dumps(pool), type='pool')
+        db.session.add(doc)
+        db.session.flush()
+        log = Log(
+            document_id=pool['id'], 
+            body=json.dumps(pool), 
+            tag='pool_created', 
+            user_id=user.id, 
+            date=datetime.datetime.now())
+        db.session.add(log)
+        db.session.commit()        
+    except Exception as exc:
+        # app.logger.error(exc)
+        db.session.rollback()
+        return jsonify(message='storage error: %s' % exc), 400
+
+    return jsonify(message='ok')
+
+@app.route('/api/v1/pools', methods=['DELETE'])
+@authorization_required
+def pools_delete(user=None):
+    try:
+        docs = Document.query.filter_by(deleted=False).all()
+        for doc in docs:
+            doc.deleted = True
+            db.session.add(doc)
+            db.session.flush()
+            log = Log(
+                document_id=doc.id, 
+                body=json.dumps({}),
+                tag='pool_deleted', 
+                user_id=user.id, 
+                date=datetime.datetime.now())
+        db.session.commit()
+    except Exception as exc:
+        # app.logger.error(exc)
+        db.session.rollback()
+        return jsonify(message='storage error: %s' % exc), 400
+
+    return jsonify(message='ok')
+
+@app.route('/api/v1/exams', methods=['GET'])
+@authorization_required
+def exams_get_all(user=None):
+    return jsonify(exams=[ e.as_dict() for e in Exam.query.filter_by(deleted=False).all() ])
+
+@app.route('/api/v1/exams', methods=['POST'])
+@authorization_required
+def exams_post(user=None):
+    try:
+        exam_dict = V.exam(json.loads(request.data))
+    except Exception as exc:
+        return jsonify(message='error during validation: %s' % exc), 400
+    try:
+        exam = Exam(**exam_dict)
+        db.session.add(exam)
+        db.session.commit()
+    except Exception as exc:
+        return jsonify(message='storage error: %s' % exc), 400
+    return jsonify(message='ok')
+
+@app.route('/api/v1/exams', methods=['DELETE'])
+@authorization_required
+def exams_delete(user=None):
+    exams = Exam.query.filter_by(deleted=False).all()
+    for e in exams:
+        e.deleted = True
+        db.session.add(e)
+        db.session.flush()
+    db.session.commit()
+    return jsonify(message='ok')
+
+@app.route('/api/v1/audits', methods=['GET'])
+@authorization_required
+def audits_get_all(user=None):
+    return jsonify(audits=[ a.as_dict() for a in Audit.query.filter_by().all() ])
+
+@app.route('/api/v1/audits', methods=['POST'])
+@authorization_required
+def audits_post(user=None):
+    try:
+        audit_dict = V.audit(json.loads(request.data))
+    except Exception as exc:
+        return jsonify(message='error during validation: %s' % exc), 400
+    try:
+        exam = Exam.query.filter_by(id=audit_dict['exam_id']).first_or_404()
+        pool = Document.query.filter_by(id=exam.pool_id).first_or_404()
+
+        trial_id = random_id()
+        
+        doc = Document(id=trial_id, body=json.dumps({'pool' : pool.to_python(),  'id' : trial_id}), type='trial')
+        db.session.add(doc)
+        db.session.flush()
+
+        log = Log(
+            document_id=doc.id, 
+            body=json.dumps({}),
+            tag='trial_created', 
+            user_id=user.id, 
+            date=datetime.datetime.now())
+        db.session.add(log)
+        db.session.flush()
+
+        audit_dict.update({'trial_id' : trial_id})
+        audit = Audit(**audit_dict)
+        db.session.add(audit)
+        db.session.commit()
+
+    except Exception as exc:
+        app.logger.error(exc)
+        return jsonify(message='storage error: %s' % exc), 400
+    return jsonify(message='ok')
+
+@app.route('/api/v1/audits', methods=['DELETE'])
+@authorization_required
+def audits_delete(user=None):
+    try:
+        audits = Audit.query.filter_by(status='registered').all()
+        for audit in audits:
+            doc = Document.query.filter_by(id=audit.trial_id).first()
+            doc.deleted = True
+            log = Log(
+                document_id=audit.trial_id, 
+                body=json.dumps({}),
+                tag='trial_deleted', 
+                user_id=user.id, 
+                date=datetime.datetime.now())
+            db.session.add(log) 
+            db.session.add(doc)
+            db.session.delete(audit)
+        db.session.commit()
+    except Exception as exc:
+        app.logger.error(exc)
+        return jsonify(message='storage error: %s' % exc), 400
+    return jsonify(message='ok')

tests/documents/audit0.json

+{
+	"id" : "helo11",
+	"user" : 1,
+	"pool" : {
+    	"id" : "87asds8",
+    	"tasks" : []
+	}
+}

tests/test_api.py

+from audrid.models import (
+    Audit,
+    Document, 
+    Exam, 
+    Log,
+    User,
+)
+from audrid import (app, db, utils)
+import base64
+import json
+import time
+import unittest
+
+class APITests(unittest.TestCase):
+    """ Test DB.
+    """
+    def setUp(self):
+        app.config['TESTING'] = True
+        app.config['DEBUG'] = True
+        app.config['CSRF_ENABLED'] = False
+        app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
+        # app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://ezri:ezri@localhost/ezri'
+        self.app = app.test_client()
+        db.drop_all(app=app)
+        db.create_all(app=app)
+
+        admin = User(username='admin', email='martin.czygan@gmail.com', password='admin')
+        guest = User(username='guest', email='guest@example.com', password='guest')
+        db.session.add(admin)
+        db.session.add(guest)
+        db.session.commit()
+
+    def tearDown(self):
+        db.session.remove()
+        db.drop_all(app=app)
+
+    def open_with_auth(self, url, method='GET', username='admin', password='admin', data=None):
+        headers = {
+            'Authorization' : 'Basic %s' % (
+            base64.b64encode("%s:%s" % (username, password)))
+        }
+        return self.app.open(url, method=method, headers=headers, data=data)
+
+    def test_pools(self):
+        rv = self.open_with_auth('/api/v1/pools')
+        self.assertEqual(200, rv.status_code)
+        self.assertEqual({"pools" : []}, json.loads(rv.data))
+
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({}))
+        self.assertEqual(400, rv.status_code)
+
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : 'x'}))
+        self.assertEqual(400, rv.status_code)
+
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : '123456'}))
+        self.assertEqual(400, rv.status_code)
+
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : u'123456', 'tasks' : []}))
+        self.assertEqual(200, rv.status_code)
+        self.assertEqual(1, Document.query.count())
+        self.assertEqual(1, Log.query.count())
+        self.assertIsNotNone(Document.query.filter_by(id='123456').first())
+
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : u'123456', 'tasks' : []}))
+        self.assertEqual(400, rv.status_code)
+
+    def test_pools_delete(self):
+        rv = self.open_with_auth('/api/v1/pools', method='DELETE')
+        self.assertEqual(200, rv.status_code)
+        
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : u'000000', 'tasks' : []}))
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : u'000001', 'tasks' : []}))
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : u'000002', 'tasks' : []}))
+
+        self.assertEqual(0, Document.query.filter_by(deleted=True).count())
+
+        rv = self.open_with_auth('/api/v1/pools', method='DELETE')
+        self.assertEqual(200, rv.status_code)
+        self.assertEqual(3, Document.query.filter_by(deleted=True).count())
+
+    def test_exams(self):
+        rv = self.open_with_auth('/api/v1/exams', method='GET')
+        self.assertEqual(200, rv.status_code)
+        self.assertEqual({u"exams" : []}, json.loads(rv.data))
+
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : u'123456', 'tasks' : []}))
+        rv = self.open_with_auth('/api/v1/exams', method='POST', data=json.dumps({"pool" : u'123456'}))
+        self.assertEqual(400, rv.status_code)
+
+        rv = self.open_with_auth('/api/v1/exams', method='POST', data=json.dumps({"name" : u'123456'}))
+        self.assertEqual(400, rv.status_code)
+
+        rv = self.open_with_auth('/api/v1/exams', method='POST', data=json.dumps({"pool_id" : u'123456', 'name' : u'This is a test pool'}))
+        self.assertEqual(200, rv.status_code)
+
+        rv = self.open_with_auth('/api/v1/exams', method='GET')
+        self.assertEqual(200, rv.status_code)
+        self.assertTrue('exams' in json.loads(rv.data))
+        self.assertEqual(1, len(json.loads(rv.data)['exams']))
+
+    def test_exams_delete(self):
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : u'123456', 'tasks' : []}))
+        rv = self.open_with_auth('/api/v1/exams', method='POST', data=json.dumps({"pool_id" : u'123456', "name" : "Test exam 12"}))
+        self.assertEqual(1, Exam.query.filter_by(deleted=False).count())
+        rv = self.open_with_auth('/api/v1/exams', method='DELETE')
+        self.assertEqual(0, Exam.query.filter_by(deleted=False).count())
+
+    def test_audits(self):
+        rv = self.open_with_auth('/api/v1/audits', method='GET')
+        self.assertEqual(200, rv.status_code)
+        self.assertEqual({u"audits" : []}, json.loads(rv.data))
+
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : u'123456', 'tasks' : []}))
+        rv = self.open_with_auth('/api/v1/exams', method='POST', data=json.dumps({"pool_id" : u'123456', 'name' : u'This is a test pool'}))
+        rv = self.open_with_auth('/api/v1/audits', method='POST', data=json.dumps({"exam_id" : Exam.query.first().id, "user_id" : 1}))
+        self.assertEqual(200, rv.status_code)
+
+        rv = self.open_with_auth('/api/v1/audits', method='GET')
+        self.assertEqual(200, rv.status_code)
+        self.assertEqual(1 , len(json.loads(rv.data)['audits']))
+
+        self.assertEqual(2, Document.query.count())
+        self.assertEqual(2, Log.query.count())
+        self.assertEqual(1, Exam.query.count())
+        self.assertEqual(1, Audit.query.count())
+
+    def test_audits_delete(self):
+        rv = self.open_with_auth('/api/v1/pools', method='POST', data=json.dumps({'id' : u'123456', 'tasks' : []}))
+        rv = self.open_with_auth('/api/v1/exams', method='POST', data=json.dumps({"pool_id" : u'123456', 'name' : u'This is a test pool'}))
+        rv = self.open_with_auth('/api/v1/audits', method='POST', data=json.dumps({"exam_id" : Exam.query.first().id, "user_id" : 1}))
+        rv = self.open_with_auth('/api/v1/audits', method='DELETE')
+
+        self.assertEqual(2, Document.query.count())
+        self.assertEqual(3, Log.query.count())
+        self.assertEqual(1, Exam.query.count())
+        self.assertEqual(0, Audit.query.count())

tests/test_models.py

+# coding: utf-8
+
+from audrid.models import (
+    Audit,
+    Document, 
+    Exam, 
+    Log,
+    User,
+)
+from audrid import (app, db, utils)
+from audrid import validators as V
+from sqlalchemy.exc import IntegrityError
+from voluptuous import InvalidList
+from werkzeug.exceptions import NotFound
+import unittest
+import json
+
+class ModelTests(unittest.TestCase):
+
+    def setUp(self):
+        app.config['TESTING'] = True
+        app.config['CSRF_ENABLED'] = False
+        # app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
+        app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://audrid:audrid@localhost/audrid'
+        self.app = app.test_client()
+        db.drop_all(app=app)
+        db.create_all()
+
+        admin = User(username='admin', email='martin.czygan@gmail.com', password='admin', group='admin')
+        guest = User(username='guest', email='guest@example.com', password='guest')
+        db.session.add_all([admin, guest])
+        db.session.commit()
+
+    def tearDown(self):
+        db.session.remove()
+        db.drop_all()
+
+    def test_basic(self):
+        rid = utils.random_id()
+        pool = V.pool({"id" : rid, "tasks" : []})
+        doc = Document(id=rid, body=json.dumps(pool), type='pool')
+        exam = Exam(name="Test Exam %s" % rid, pool_id=rid)
+
+        db.session.add(doc)
+        db.session.commit()
+        db.session.add(exam)
+        db.session.commit()
+
+        rid = utils.random_id()
+        trial = V.trial({"user" : 2, "pool" : pool, "id" : rid})
+        doc = Document(id=rid, body=json.dumps(trial), type='trial')
+        audit = Audit(user_id=2, exam_id=exam.id, trial_id=rid)
+
+        db.session.add(doc)
+        db.session.commit()
+        db.session.add(audit)
+        db.session.commit()        
+
+    def test_can_create_a_document(self):
+        rid = utils.random_id()
+        doc = Document(id=rid, body=json.dumps({}), type='empty')
+        self.assertIsNotNone(doc)
+        db.session.add(doc)
+        db.session.commit()        
+
+    def test_cannot_create_docs_with_two_ids(self):
+        rid = utils.random_id()
+        doc1 = Document(id=rid, body=json.dumps({}), type='empty')
+        doc2 = Document(id=rid, body=json.dumps({}), type='empty')
+        db.session.add(doc1)
+        db.session.commit()        
+        db.session.add(doc2)
+        with self.assertRaises(IntegrityError):
+            db.session.commit()
+        
+    # def test_pool_helper(self):
+    #     self.assertEqual(0, Log.query.count())
+    #     t = { 
+    #             "kind" : u"cloze", 
+    #             "id" : utils.random_id(),
+    #             "task" : u"Please fill out...",
+    #             "text" : u"Hello World this is a test.",
+    #             "answer" : {},
+    #             "gaps" : {}
+    #     }
+        
+    #     pid = utils.random_id()
+
+    #     create_pool(pool={ "id" : pid, "tasks" : [t]}, user_id=1)
+    #     self.assertEqual(1, Log.query.count())
+
+    #     u = { 
+    #             "kind" : u"text", 
+    #             "id" : utils.random_id(),
+    #             "task" : u"Please fill out...",
+    #             "answer" : u""
+    #     }
+        
+    #     update_pool(pool={ "id" : pid, "tasks" : [t, u] }, user_id=1)
+    #     self.assertEqual(2, Log.query.count())
+
+    #     update_pool(pool={ "id" : pid, "tasks" : [u] }, user_id=1)
+    #     self.assertEqual(3, Log.query.count())
+    #     self.assertFalse(Document.query.filter_by(id=pid).first().deleted)
+
+    #     with self.assertRaises(InvalidList):
+    #         update_pool(pool={ "tasks" : [u] }, user_id=1)
+
+    #     delete_pool(pid=pid, user_id=1)
+    #     self.assertEqual(4, Log.query.count())
+    #     self.assertEqual(4, Log.query.filter_by(document_id=pid).count())
+    #     self.assertTrue(Document.query.filter_by(id=pid).first().deleted)
+
+    #     with self.assertRaises(NotFound):
+    #         delete_pool(pid=pid, user_id=1)
+
+    #     self.assertEqual(4, Log.query.count())
+
+    # def test_audit_helper(self):
+    #     pass

tests/test_validators.py

     multiple_choice,
     mapping,
     pool,
+    trial,
 )
 import unittest
 import json
     return open(os.path.join(os.path.abspath(
         os.path.dirname(__file__)), filename))
 
-class TaskValidatorTests(unittest.TestCase):
-
+class FilelistMixin(unittest.TestCase):
+    """
+    Provide ``should_pass_on`` and ``should_fail_on``
+    methods. 
+    """
     def should_pass_on(self, filenames, schema=None):
+        """
+        Loads each filename in ``filenames`` and validates against ``schema``.
+        """
         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):
+        """
+        Takes a list of (filename, exception) tuples and ensures,
+        that the exceptions are actually raised while validating against ``schema``.
+        """
         for fn, exc in filename_exc_tuples:
             with self.assertRaises(exc):
                 with openrel(fn) as handle:
                     schema(json.load(handle))
 
+class TaskValidatorTests(FilelistMixin):
+
     def test_cloze(self):
         valid_files = [
             "documents/cloze0.json",
         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))
+class PoolValidatorTests(FilelistMixin):
 
     def test_pool(self):
         valid_files = [
 
         self.should_pass_on(valid_files, schema=pool)
         self.should_fail_on(invalid_files, schema=pool)
+
+class AuditValidatorTests(FilelistMixin):
+
+    def test_audit(self):
+        valid_files = [
+            "documents/audit0.json",
+        ]
+        invalid_files = [
+            ("documents/empty.json", ValueError),
+        ]
+
+        self.should_pass_on(valid_files, schema=trial)
+        self.should_fail_on(invalid_files, schema=trial)