Commit 01b8619759789cd614bade0b9ebdd5cffc26e466

Authored by Miguel Barão
1 parent a4e0e360
Exists in master and in 1 other branch dev

- using sqlalchemy

1 1
2 # BUGS 2 # BUGS
3 3
4 -- usar thread.Lock para aceder a variaveis de estado. 4 +- 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.
  5 +- usar thread.Lock para aceder a variaveis de estado?
5 - permitir adicionar imagens nas perguntas. 6 - permitir adicionar imagens nas perguntas.
6 - debug mode: log levels not working 7 - debug mode: log levels not working
7 -- replace sys.exit calls  
8 -- if does not find questions, aborts silently 8 +- Se aluno fizer logout, o teste não é gravado e ficamos sem registo do teste que o aluno viu.
9 9
10 # TODO 10 # TODO
11 11
12 - implementar practice mode. 12 - implementar practice mode.
13 -- SQLAlchemy em vez da classe database.  
14 - single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). 13 - single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?).
15 - aviso na pagina principal para quem usa browser da treta 14 - aviso na pagina principal para quem usa browser da treta
16 - permitir varios testes, aluno escolhe qual o teste que quer fazer. 15 - permitir varios testes, aluno escolhe qual o teste que quer fazer.
@@ -24,6 +23,10 @@ @@ -24,6 +23,10 @@
24 23
25 # FIXED 24 # FIXED
26 25
  26 +- sqlalchemy queixa-se de threads.
  27 +- SQLAlchemy em vez da classe database.
  28 +- replace sys.exit calls
  29 +- if does not find questions, aborts silently
27 - argumentos da linha de comando a funcionar. 30 - argumentos da linha de comando a funcionar.
28 - configuracao dos logs cherrypy para se darem bem com os outros 31 - configuracao dos logs cherrypy para se darem bem com os outros
29 - browser e ip usados gravado no test. 32 - browser e ip usados gravado no test.
@@ -83,7 +83,7 @@ class App(object): @@ -83,7 +83,7 @@ class App(object):
83 return False 83 return False
84 else: 84 else:
85 logger.info('Student {}: logged out.'.format(uid)) 85 logger.info('Student {}: logged out.'.format(uid))
86 - del self.online[uid] 86 + del self.online[uid] # Nao está a gravar o teste como desistencia...
87 return True 87 return True
88 88
89 def generate_test(self, uid): 89 def generate_test(self, uid):
@@ -180,7 +180,7 @@ class App(object): @@ -180,7 +180,7 @@ class App(object):
180 logger.info('Student {}: denied to login'.format(uid)) 180 logger.info('Student {}: denied to login'.format(uid))
181 181
182 def reset_password(self, uid): 182 def reset_password(self, uid):
183 - self.db.reset_password(uid) 183 + self.db.update_password(uid, pw='')
184 logger.info('Student {}: password reset to ""'.format(uid)) 184 logger.info('Student {}: password reset to ""'.format(uid))
185 185
186 def set_user_agent(self, uid, user_agent=''): 186 def set_user_agent(self, uid, user_agent=''):
1 1
2 -import sqlite3 2 +import logging
  3 +from sqlalchemy import create_engine
  4 +from sqlalchemy.orm import sessionmaker, scoped_session
  5 +from orm import Base, Student, Test, Question
3 6
  7 +logger = logging.getLogger(__name__)
  8 +
  9 +#----------------------------------------------------------------------------
4 class Database(object): 10 class Database(object):
5 def __init__(self, db): 11 def __init__(self, db):
6 self.db = db # sqlite3 filename 12 self.db = db # sqlite3 filename
7 13
8 - def get_count_students(self):  
9 - with sqlite3.connect(self.db) as c:  
10 - sql = 'SELECT COUNT(*) FROM students'  
11 - return c.execute(sql).fetchone()[0] 14 + engine = create_engine('sqlite:///{}'.format(db), echo=False)
  15 + Base.metadata.create_all(engine) # Criate schema if needed
  16 + self.Session = scoped_session(sessionmaker(bind=engine))
12 17
13 - def reset_password(self, uid):  
14 - with sqlite3.connect(self.db) as c:  
15 - sql = 'UPDATE students SET password="" WHERE number=?'  
16 - c.execute(sql, [uid]) 18 + #-------------------------------------------------------------------------
  19 + def get_count_students(self):
  20 + s = self.Session()
  21 + return s.query(Student).filter(Student.id != '0').count()
  22 +
  23 + # with sqlite3.connect(self.db) as c:
  24 + # sql = 'SELECT COUNT(*) FROM students'
  25 + # return c.execute(sql).fetchone()[0]
  26 +
  27 + #-------------------------------------------------------------------------
  28 + def update_password(self, uid, pw=''):
  29 + s = self.Session()
  30 + try:
  31 + u = s.query(Student).filter(Student.id == uid).one()
  32 + except:
  33 + pass
  34 + else:
  35 + u.password = pw
  36 + s.commit()
17 37
18 - def update_password(self, uid, pw):  
19 # saves pw as is (should be already hashed) 38 # saves pw as is (should be already hashed)
20 - with sqlite3.connect(self.db) as c:  
21 - sql = 'UPDATE students SET password=? WHERE number=?'  
22 - c.execute(sql, (pw, uid)) 39 + # with sqlite3.connect(self.db) as c:
  40 + # sql = 'UPDATE students SET password=? WHERE id=?'
  41 + # c.execute(sql, (pw, uid))
23 42
  43 + #-------------------------------------------------------------------------
24 def get_student(self, uid): 44 def get_student(self, uid):
25 - with sqlite3.connect(self.db) as c:  
26 - sql = 'SELECT name,password FROM students WHERE number=?'  
27 - try:  
28 - name, pw = c.execute(sql, [uid]).fetchone()  
29 - except:  
30 - return None  
31 - else:  
32 - return (name, pw)  
33 - 45 + s = self.Session()
  46 + r = s.query(Student).filter(Student.id == uid).one_or_none()
  47 + return r.name, r.password
  48 +
  49 + # with sqlite3.connect(self.db) as c:
  50 + # sql = 'SELECT name,password FROM students WHERE id=?'
  51 + # try:
  52 + # name, pw = c.execute(sql, [uid]).fetchone()
  53 + # except:
  54 + # return None
  55 + # else:
  56 + # return (name, pw)
  57 +
  58 + #-------------------------------------------------------------------------
34 def get_all_students(self): 59 def get_all_students(self):
35 - with sqlite3.connect(self.db) as c:  
36 - sql = 'SELECT number,name,password FROM students ORDER BY number ASC'  
37 - students = c.execute(sql).fetchall()  
38 - return students 60 + s = self.Session()
  61 + r = s.query(Student).all()
  62 + return [(x.id, x.name, x.password) for x in r]
  63 +
  64 + # with sqlite3.connect(self.db) as c:
  65 + # sql = 'SELECT id,name,password FROM students ORDER BY id ASC'
  66 + # students = c.execute(sql).fetchall()
  67 + # return students
39 68
40 # get all results for a particular test. If a student has submited more than 69 # get all results for a particular test. If a student has submited more than
41 # one test, returns the highest grade FIXME not tested, not used 70 # one test, returns the highest grade FIXME not tested, not used
@@ -44,30 +73,64 @@ class Database(object): @@ -44,30 +73,64 @@ class Database(object):
44 # grades = c.execute('SELECT student_id,MAX(grade) FROM tests WHERE test_id==?', [testid]) 73 # grades = c.execute('SELECT student_id,MAX(grade) FROM tests WHERE test_id==?', [testid])
45 # return grades.fetchall() 74 # return grades.fetchall()
46 75
  76 + #-------------------------------------------------------------------------
47 # get results from previous tests of a student 77 # get results from previous tests of a student
48 def get_student_grades_from_all_tests(self, uid): 78 def get_student_grades_from_all_tests(self, uid):
49 - with sqlite3.connect(self.db) as c:  
50 - grades = c.execute('SELECT test_id,grade,finish_time FROM tests WHERE student_id==?', [uid])  
51 - return grades.fetchall() 79 + s = self.Session()
  80 + r = s.query(Test).filter(Student.id == uid).all()
  81 + return [(x.id, x.grade, x.finishtime) for x in r]
  82 +
  83 + # with sqlite3.connect(self.db) as c:
  84 + # grades = c.execute('SELECT id,grade,finishtime FROM tests WHERE student_id==?', [uid])
  85 + # return grades.fetchall()
52 86
  87 + #-------------------------------------------------------------------------
53 def get_student_grades_from_test(self, uid, testid): 88 def get_student_grades_from_test(self, uid, testid):
54 - with sqlite3.connect(self.db) as c:  
55 - grades = c.execute('SELECT grade,finish_time FROM tests WHERE student_id==? and test_id==?', [uid, testid])  
56 - return grades.fetchall()  
57 -  
58 - def save_test(self, t):  
59 - with sqlite3.connect(self.db) as c:  
60 - # save final grade of the test  
61 - sql = 'INSERT INTO tests VALUES (?,?,?,?,?)'  
62 - test = (t['ref'], t['student']['number'], t['grade'], str(t['start_time']), str(t['finish_time']))  
63 - c.execute(sql, test)  
64 -  
65 - def save_questions(self, t):  
66 - with sqlite3.connect(self.db) as c:  
67 - # save grades of all the questions (omits questions without grade)  
68 - sql = 'INSERT INTO questions VALUES (?,?,?,?,?)'  
69 - questions = [(t['ref'], q['ref'], t['student']['number'], q['grade'], str(t['finish_time'])) for q in t['questions'] if 'grade' in q]  
70 - c.executemany(sql, questions) 89 + s = self.Session()
  90 + r = s.query(Test).filter(Test.student_id==uid and Test.id==testid).all()
  91 + return [(x.grade, x.finishtime) for x in r]
  92 +
  93 + # with sqlite3.connect(self.db) as c:
  94 + # grades = c.execute('SELECT grade,finishtime FROM tests WHERE student_id==? and id==?', [uid, testid])
  95 + # return grades.fetchall()
  96 +
  97 + #-------------------------------------------------------------------------
  98 + def save_test(self, test):
  99 + t = Test(
  100 + ref=test['ref'],
  101 + grade=test['grade'],
  102 + starttime=str(test['start_time']),
  103 + finishtime=str(test['finish_time']),
  104 + student_id=test['student']['number']
  105 + )
  106 + s = self.Session()
  107 + s.add(t)
  108 + s.commit()
  109 +
  110 + # with sqlite3.connect(self.db) as c:
  111 + # # save final grade of the test
  112 + # sql = 'INSERT INTO tests VALUES (?,?,?,?,?)'
  113 + # test = (t['ref'], t['student']['number'], t['grade'], str(t['start_time']), str(t['finish_time']))
  114 + # c.execute(sql, test)
  115 +
  116 + #-------------------------------------------------------------------------
  117 + def save_questions(self, test):
  118 + s = self.Session()
  119 + questions = [Question(
  120 + ref=q['ref'],
  121 + grade=q['grade'],
  122 + starttime='',
  123 + finishtime=str(test['finish_time']),
  124 + student_id=test['student']['number'],
  125 + test_id=test['ref']) for q in test['questions'] if 'grade' in q]
  126 + s.add_all(questions)
  127 + s.commit()
  128 +
  129 + # with sqlite3.connect(self.db) as c:
  130 + # # save grades of all the questions (omits questions without grade)
  131 + # sql = 'INSERT INTO questions VALUES (?,?,?,?,?)'
  132 + # questions = [(t['ref'], q['ref'], t['student']['number'], q['grade'], str(t['finish_time'])) for q in t['questions'] if 'grade' in q]
  133 + # c.executemany(sql, questions)
71 134
72 135
73 136
initdb.py 0 → 100755
@@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
  1 +#!/usr/bin/env python3
  2 +# -*- coding: utf-8 -*-
  3 +
  4 +import csv
  5 +import argparse
  6 +import re
  7 +import string
  8 +import sys
  9 +from sqlalchemy import create_engine
  10 +from sqlalchemy.orm import sessionmaker
  11 +
  12 +from orm import Base, Student, Test, Question
  13 +
  14 +# SIIUE names have alien strings like "(TE)" and are sometimes capitalized
  15 +# We remove them so that students dont keep asking what it means
  16 +def fix(name):
  17 + return string.capwords(re.sub('\(.*\)', '', name).strip())
  18 +
  19 +# ===========================================================================
  20 +# Parse command line options
  21 +argparser = argparse.ArgumentParser(description='Create new database from a CSV file (SIIUE format)')
  22 +argparser.add_argument('--db', default='students.db', type=str, help='database filename')
  23 +argparser.add_argument('csvfile', nargs='?', type=str, default='', help='CSV filename')
  24 +args = argparser.parse_args()
  25 +
  26 +# ===========================================================================
  27 +engine = create_engine('sqlite:///{}'.format(args.db), echo=False)
  28 +
  29 +# Criate schema if needed
  30 +Base.metadata.create_all(engine)
  31 +
  32 +Session = sessionmaker(bind=engine)
  33 +
  34 +# --- start session ---
  35 +try:
  36 + session = Session()
  37 +
  38 + # add administrator and fake student accounts
  39 + session.add_all([
  40 + Student(id='0', name='Professor', password=''),
  41 + Student(id='student1', name='Student1', password=''),
  42 + Student(id='student2', name='Student2', password=''),
  43 + Student(id='student3', name='Student3', password=''),
  44 + Student(id='student4', name='Student4', password=''),
  45 + Student(id='student5', name='Student5', password=''),
  46 + ])
  47 +
  48 + # add enrolled students from csv file
  49 + if args.csvfile:
  50 + try:
  51 + csvreader = csv.DictReader(open(args.csvfile, encoding='iso-8859-1'), delimiter=';', quotechar='"')
  52 + except EnvironmentError:
  53 + print('CSV file "{0}" not found!'.format(args.csvfile))
  54 + else:
  55 + session.add_all([Student(id=r['N.º'], name=fix(r['Nome']), password='') for r in csvreader])
  56 +
  57 + session.commit()
  58 +except Exception:
  59 + session.rollback()
  60 + print('Erro: Dados já existentes na base de dados?')
  61 + sys.exit(1)
  62 +# --- end session ---
orm.py 0 → 100644
@@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
  1 +
  2 +
  3 +from sqlalchemy import Table, Column, ForeignKey, Integer, Float, String, DateTime
  4 +from sqlalchemy.ext.declarative import declarative_base
  5 +from sqlalchemy.orm import relationship
  6 +
  7 +
  8 +# ===========================================================================
  9 +# Declare ORM
  10 +Base = declarative_base()
  11 +
  12 +# ---------------------------------------------------------------------------
  13 +class Student(Base):
  14 + __tablename__ = 'students'
  15 + id = Column(String, primary_key=True)
  16 + name = Column(String)
  17 + password = Column(String)
  18 +
  19 + # ---
  20 + tests = relationship('Test', back_populates='student')
  21 + questions = relationship('Question', back_populates='student')
  22 +
  23 + # def __repr__(self):
  24 + # return 'Student:\n id: "{0}"\n name: "{1}"\n password: "{2}"'.format(self.id, self.name, self.password)
  25 +
  26 +
  27 +# ---------------------------------------------------------------------------
  28 +class Test(Base):
  29 + __tablename__ = 'tests'
  30 + id = Column(Integer, primary_key=True) # auto_increment
  31 + ref = Column(String)
  32 + grade = Column(Float)
  33 + starttime = Column(String)
  34 + finishtime = Column(String)
  35 + student_id = Column(String, ForeignKey('students.id'))
  36 +
  37 + # ---
  38 + student = relationship('Student', back_populates='tests')
  39 + questions = relationship('Question', back_populates='test')
  40 +
  41 + # def __repr__(self):
  42 + # 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)
  43 +
  44 +
  45 +# ---------------------------------------------------------------------------
  46 +class Question(Base):
  47 + __tablename__ = 'questions'
  48 + id = Column(Integer, primary_key=True) # auto_increment
  49 + ref = Column(String)
  50 + grade = Column(Float)
  51 + starttime = Column(String)
  52 + finishtime = Column(String)
  53 + student_id = Column(String, ForeignKey('students.id'))
  54 + test_id = Column(String, ForeignKey('tests.id'))
  55 +
  56 + # ---
  57 + student = relationship('Student', back_populates='questions')
  58 + test = relationship('Test', back_populates='questions')
  59 +
  60 +# def __repr__(self):
  61 +# return '''
  62 +# Question:
  63 +# id: "{0}"
  64 +# ref: "{1}"
  65 +# grade: "{2}"
  66 +# starttime: "{3}"
  67 +# finishtime: "{4}"
  68 +# student_id: "{5}"
  69 +# test_id: "{6}"
  70 +# '''.fotmat(self.id, self.ref, self.grade, self.starttime, self.finishtime, self.student_id, self.test_id)
  71 +
  72 +
  73 +# ---------------------------------------------------------------------------
@@ -6,7 +6,6 @@ from os import path @@ -6,7 +6,6 @@ from os import path
6 import sys 6 import sys
7 import argparse 7 import argparse
8 import logging.config 8 import logging.config
9 -import html  
10 import json 9 import json
11 10
12 try: 11 try:
@@ -241,9 +240,6 @@ if __name__ == '__main__': @@ -241,9 +240,6 @@ if __name__ == '__main__':
241 # --- Setup logging 240 # --- Setup logging
242 logging.config.dictConfig(load_yaml(LOGGER_CONF)) 241 logging.config.dictConfig(load_yaml(LOGGER_CONF))
243 242
244 - # with open(LOGGER_CONF,'r') as f:  
245 - # logging.config.dictConfig(yaml.load(f))  
246 -  
247 # --- start application 243 # --- start application
248 from app import App 244 from app import App
249 245