From 01b8619759789cd614bade0b9ebdd5cffc26e466 Mon Sep 17 00:00:00 2001 From: Miguel Barão Date: Mon, 6 Jun 2016 14:48:18 +0100 Subject: [PATCH] - using sqlalchemy --- BUGS.md | 11 +++++++---- app.py | 4 ++-- database.py | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------- initdb.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ orm.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ serve.py | 4 ---- 6 files changed, 253 insertions(+), 56 deletions(-) create mode 100755 initdb.py create mode 100644 orm.py diff --git a/BUGS.md b/BUGS.md index e236520..2b3e2cf 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,16 +1,15 @@ # BUGS -- usar thread.Lock para aceder a variaveis de estado. +- criar sqlalchemy sessions dentro de app de modo a estarem associadas a requests. ver se é facil usar with db:(...) para criar e fechar sessão. +- usar thread.Lock para aceder a variaveis de estado? - permitir adicionar imagens nas perguntas. - debug mode: log levels not working -- replace sys.exit calls -- if does not find questions, aborts silently +- Se aluno fizer logout, o teste não é gravado e ficamos sem registo do teste que o aluno viu. # TODO - implementar practice mode. -- SQLAlchemy em vez da classe database. - single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). - aviso na pagina principal para quem usa browser da treta - permitir varios testes, aluno escolhe qual o teste que quer fazer. @@ -24,6 +23,10 @@ # FIXED +- sqlalchemy queixa-se de threads. +- SQLAlchemy em vez da classe database. +- replace sys.exit calls +- if does not find questions, aborts silently - argumentos da linha de comando a funcionar. - configuracao dos logs cherrypy para se darem bem com os outros - browser e ip usados gravado no test. diff --git a/app.py b/app.py index feea821..f5cd037 100644 --- a/app.py +++ b/app.py @@ -83,7 +83,7 @@ class App(object): return False else: logger.info('Student {}: logged out.'.format(uid)) - del self.online[uid] + del self.online[uid] # Nao está a gravar o teste como desistencia... return True def generate_test(self, uid): @@ -180,7 +180,7 @@ class App(object): logger.info('Student {}: denied to login'.format(uid)) def reset_password(self, uid): - self.db.reset_password(uid) + self.db.update_password(uid, pw='') logger.info('Student {}: password reset to ""'.format(uid)) def set_user_agent(self, uid, user_agent=''): diff --git a/database.py b/database.py index d45132e..88057ae 100644 --- a/database.py +++ b/database.py @@ -1,41 +1,70 @@ -import sqlite3 +import logging +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, scoped_session +from orm import Base, Student, Test, Question +logger = logging.getLogger(__name__) + +#---------------------------------------------------------------------------- class Database(object): def __init__(self, db): self.db = db # sqlite3 filename - def get_count_students(self): - with sqlite3.connect(self.db) as c: - sql = 'SELECT COUNT(*) FROM students' - return c.execute(sql).fetchone()[0] + engine = create_engine('sqlite:///{}'.format(db), echo=False) + Base.metadata.create_all(engine) # Criate schema if needed + self.Session = scoped_session(sessionmaker(bind=engine)) - def reset_password(self, uid): - with sqlite3.connect(self.db) as c: - sql = 'UPDATE students SET password="" WHERE number=?' - c.execute(sql, [uid]) + #------------------------------------------------------------------------- + def get_count_students(self): + s = self.Session() + return s.query(Student).filter(Student.id != '0').count() + + # with sqlite3.connect(self.db) as c: + # sql = 'SELECT COUNT(*) FROM students' + # return c.execute(sql).fetchone()[0] + + #------------------------------------------------------------------------- + def update_password(self, uid, pw=''): + s = self.Session() + try: + u = s.query(Student).filter(Student.id == uid).one() + except: + pass + else: + u.password = pw + s.commit() - def update_password(self, uid, pw): # saves pw as is (should be already hashed) - with sqlite3.connect(self.db) as c: - sql = 'UPDATE students SET password=? WHERE number=?' - c.execute(sql, (pw, uid)) + # with sqlite3.connect(self.db) as c: + # sql = 'UPDATE students SET password=? WHERE id=?' + # c.execute(sql, (pw, uid)) + #------------------------------------------------------------------------- def get_student(self, uid): - with sqlite3.connect(self.db) as c: - sql = 'SELECT name,password FROM students WHERE number=?' - try: - name, pw = c.execute(sql, [uid]).fetchone() - except: - return None - else: - return (name, pw) - + s = self.Session() + r = s.query(Student).filter(Student.id == uid).one_or_none() + return r.name, r.password + + # with sqlite3.connect(self.db) as c: + # sql = 'SELECT name,password FROM students WHERE id=?' + # try: + # name, pw = c.execute(sql, [uid]).fetchone() + # except: + # return None + # else: + # return (name, pw) + + #------------------------------------------------------------------------- def get_all_students(self): - with sqlite3.connect(self.db) as c: - sql = 'SELECT number,name,password FROM students ORDER BY number ASC' - students = c.execute(sql).fetchall() - return students + s = self.Session() + r = s.query(Student).all() + return [(x.id, x.name, x.password) for x in r] + + # with sqlite3.connect(self.db) as c: + # sql = 'SELECT id,name,password FROM students ORDER BY id ASC' + # students = c.execute(sql).fetchall() + # return students # get all results for a particular test. If a student has submited more than # one test, returns the highest grade FIXME not tested, not used @@ -44,30 +73,64 @@ class Database(object): # grades = c.execute('SELECT student_id,MAX(grade) FROM tests WHERE test_id==?', [testid]) # return grades.fetchall() + #------------------------------------------------------------------------- # get results from previous tests of a student def get_student_grades_from_all_tests(self, uid): - with sqlite3.connect(self.db) as c: - grades = c.execute('SELECT test_id,grade,finish_time FROM tests WHERE student_id==?', [uid]) - return grades.fetchall() + s = self.Session() + r = s.query(Test).filter(Student.id == uid).all() + return [(x.id, x.grade, x.finishtime) for x in r] + + # with sqlite3.connect(self.db) as c: + # grades = c.execute('SELECT id,grade,finishtime FROM tests WHERE student_id==?', [uid]) + # return grades.fetchall() + #------------------------------------------------------------------------- def get_student_grades_from_test(self, uid, testid): - with sqlite3.connect(self.db) as c: - grades = c.execute('SELECT grade,finish_time FROM tests WHERE student_id==? and test_id==?', [uid, testid]) - return grades.fetchall() - - def save_test(self, t): - with sqlite3.connect(self.db) as c: - # save final grade of the test - sql = 'INSERT INTO tests VALUES (?,?,?,?,?)' - test = (t['ref'], t['student']['number'], t['grade'], str(t['start_time']), str(t['finish_time'])) - c.execute(sql, test) - - def save_questions(self, t): - with sqlite3.connect(self.db) as c: - # save grades of all the questions (omits questions without grade) - sql = 'INSERT INTO questions VALUES (?,?,?,?,?)' - questions = [(t['ref'], q['ref'], t['student']['number'], q['grade'], str(t['finish_time'])) for q in t['questions'] if 'grade' in q] - c.executemany(sql, questions) + s = self.Session() + r = s.query(Test).filter(Test.student_id==uid and Test.id==testid).all() + return [(x.grade, x.finishtime) for x in r] + + # with sqlite3.connect(self.db) as c: + # grades = c.execute('SELECT grade,finishtime FROM tests WHERE student_id==? and id==?', [uid, testid]) + # return grades.fetchall() + + #------------------------------------------------------------------------- + def save_test(self, test): + t = Test( + ref=test['ref'], + grade=test['grade'], + starttime=str(test['start_time']), + finishtime=str(test['finish_time']), + student_id=test['student']['number'] + ) + s = self.Session() + s.add(t) + s.commit() + + # with sqlite3.connect(self.db) as c: + # # save final grade of the test + # sql = 'INSERT INTO tests VALUES (?,?,?,?,?)' + # test = (t['ref'], t['student']['number'], t['grade'], str(t['start_time']), str(t['finish_time'])) + # c.execute(sql, test) + + #------------------------------------------------------------------------- + def save_questions(self, test): + s = self.Session() + questions = [Question( + ref=q['ref'], + grade=q['grade'], + starttime='', + finishtime=str(test['finish_time']), + student_id=test['student']['number'], + test_id=test['ref']) for q in test['questions'] if 'grade' in q] + s.add_all(questions) + s.commit() + + # with sqlite3.connect(self.db) as c: + # # save grades of all the questions (omits questions without grade) + # sql = 'INSERT INTO questions VALUES (?,?,?,?,?)' + # questions = [(t['ref'], q['ref'], t['student']['number'], q['grade'], str(t['finish_time'])) for q in t['questions'] if 'grade' in q] + # c.executemany(sql, questions) diff --git a/initdb.py b/initdb.py new file mode 100755 index 0000000..28bd5e9 --- /dev/null +++ b/initdb.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import csv +import argparse +import re +import string +import sys +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from orm import Base, Student, Test, Question + +# SIIUE names have alien strings like "(TE)" and are sometimes capitalized +# We remove them so that students dont keep asking what it means +def fix(name): + return string.capwords(re.sub('\(.*\)', '', name).strip()) + +# =========================================================================== +# Parse command line options +argparser = argparse.ArgumentParser(description='Create new database from a CSV file (SIIUE format)') +argparser.add_argument('--db', default='students.db', type=str, help='database filename') +argparser.add_argument('csvfile', nargs='?', type=str, default='', help='CSV filename') +args = argparser.parse_args() + +# =========================================================================== +engine = create_engine('sqlite:///{}'.format(args.db), echo=False) + +# Criate schema if needed +Base.metadata.create_all(engine) + +Session = sessionmaker(bind=engine) + +# --- start session --- +try: + session = Session() + + # add administrator and fake student accounts + session.add_all([ + Student(id='0', name='Professor', password=''), + Student(id='student1', name='Student1', password=''), + Student(id='student2', name='Student2', password=''), + Student(id='student3', name='Student3', password=''), + Student(id='student4', name='Student4', password=''), + Student(id='student5', name='Student5', password=''), + ]) + + # add enrolled students from csv file + if args.csvfile: + try: + csvreader = csv.DictReader(open(args.csvfile, encoding='iso-8859-1'), delimiter=';', quotechar='"') + except EnvironmentError: + print('CSV file "{0}" not found!'.format(args.csvfile)) + else: + session.add_all([Student(id=r['N.º'], name=fix(r['Nome']), password='') for r in csvreader]) + + session.commit() +except Exception: + session.rollback() + print('Erro: Dados já existentes na base de dados?') + sys.exit(1) +# --- end session --- diff --git a/orm.py b/orm.py new file mode 100644 index 0000000..4dbaa89 --- /dev/null +++ b/orm.py @@ -0,0 +1,73 @@ + + +from sqlalchemy import Table, Column, ForeignKey, Integer, Float, String, DateTime +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship + + +# =========================================================================== +# Declare ORM +Base = declarative_base() + +# --------------------------------------------------------------------------- +class Student(Base): + __tablename__ = 'students' + id = Column(String, primary_key=True) + name = Column(String) + password = Column(String) + + # --- + tests = relationship('Test', back_populates='student') + questions = relationship('Question', back_populates='student') + + # def __repr__(self): + # return 'Student:\n id: "{0}"\n name: "{1}"\n password: "{2}"'.format(self.id, self.name, self.password) + + +# --------------------------------------------------------------------------- +class Test(Base): + __tablename__ = 'tests' + id = Column(Integer, primary_key=True) # auto_increment + ref = Column(String) + grade = Column(Float) + starttime = Column(String) + finishtime = Column(String) + student_id = Column(String, ForeignKey('students.id')) + + # --- + student = relationship('Student', back_populates='tests') + questions = relationship('Question', back_populates='test') + + # def __repr__(self): + # return 'Test:\n id: "{0}"\n ref="{1}"\n grade="{2}"\n starttime="{3}"\n finishtime="{4}"\n student_id="{5}"'.format(self.id, self.ref, self.grade, self.starttime, self.finishtime, self.student_id) + + +# --------------------------------------------------------------------------- +class Question(Base): + __tablename__ = 'questions' + id = Column(Integer, primary_key=True) # auto_increment + ref = Column(String) + grade = Column(Float) + starttime = Column(String) + finishtime = Column(String) + student_id = Column(String, ForeignKey('students.id')) + test_id = Column(String, ForeignKey('tests.id')) + + # --- + student = relationship('Student', back_populates='questions') + test = relationship('Test', back_populates='questions') + +# def __repr__(self): +# return ''' +# Question: +# id: "{0}" +# ref: "{1}" +# grade: "{2}" +# starttime: "{3}" +# finishtime: "{4}" +# student_id: "{5}" +# test_id: "{6}" +# '''.fotmat(self.id, self.ref, self.grade, self.starttime, self.finishtime, self.student_id, self.test_id) + + +# --------------------------------------------------------------------------- diff --git a/serve.py b/serve.py index cc07670..641a403 100755 --- a/serve.py +++ b/serve.py @@ -6,7 +6,6 @@ from os import path import sys import argparse import logging.config -import html import json try: @@ -241,9 +240,6 @@ if __name__ == '__main__': # --- Setup logging logging.config.dictConfig(load_yaml(LOGGER_CONF)) - # with open(LOGGER_CONF,'r') as f: - # logging.config.dictConfig(yaml.load(f)) - # --- start application from app import App -- libgit2 0.21.2