diff --git a/BUGS.md b/BUGS.md index 3ef68f4..33cba30 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,6 +1,7 @@ # BUGS +- o eventloop está a bloquear. correção do teste é blocking. usar threadpoolexecutor? - se submissão falhar (aluno desconectado da rede) nao pode sair da página para nao perder o teste. possiveis solucoes: - botao submeter valida se esta online com um post willing_to_submit, se estiver online, mostra mensagem de confirmacao, caso contrario avisa que nao esta online. - periodicamente, cada user faz um post keep-alive. warning nos logs se offline. diff --git a/initdb.py b/initdb.py index 5e7a163..0f78dd4 100755 --- a/initdb.py +++ b/initdb.py @@ -1,86 +1,179 @@ -#!/usr/bin/env python3.6 -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 +# base import csv import argparse import re import string -import sys -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from concurrent.futures import ThreadPoolExecutor +# installed packages +import bcrypt +import sqlalchemy as sa + +# this project from models import Base, Student, Test, Question + +# =========================================================================== +# Parse command line options +def parse_commandline_arguments(): + argparser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description='Insert new users into a database. Users can be imported from CSV files in the SIIUE format or defined in the command line. If the database does not exist, a new one is created.') + + argparser.add_argument('csvfile', + nargs='*', + type=str, + default='', + help='CSV file to import (SIIUE)') + + argparser.add_argument('--db', + default='students.db', + type=str, + help='database file') + + argparser.add_argument('-A', '--admin', + action='store_true', + help='insert the admin user') + + argparser.add_argument('-a', '--add', + nargs=2, + action='append', + metavar=('uid', 'name'), + help='add new user') + + argparser.add_argument('-u', '--update', + nargs='+', + metavar='uid', + default=[], + help='users to update') + + argparser.add_argument('--pw', + default=None, + type=str, + help='set password for new and updated users') + + argparser.add_argument('-V', '--verbose', + action='store_true', + help='show all students in database') + + return argparser.parse_args() + + +# =========================================================================== # 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()) +def get_students_from_csv(filename): + try: + csvreader = csv.DictReader(open(filename, encoding='iso-8859-1'), delimiter=';', quotechar='"', skipinitialspace=True) + except EnvironmentError: + print(f'!!! Error. File "{filename}" not found !!!') + students = [] + else: + students = [{ + 'uid': s['N.º'], + 'name': string.capwords(re.sub('\(.*\)', '', s['Nome']).strip()) + } for s in csvreader] + + return students + # =========================================================================== -# 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('--demo', action='store_true', help='initialize database with a few fake students') -argparser.add_argument('csvfile', nargs='?', type=str, default='', help='CSV filename') -args = argparser.parse_args() +# replace password by hash for a single student +def hashpw(student, pw=None): + print('.', end='', flush=True) + pw = (pw or student.get('pw', None) or student['uid']).encode('utf-8') + student['pw'] = bcrypt.hashpw(pw, bcrypt.gensalt()) + # =========================================================================== -engine = create_engine('sqlite:///{}'.format(args.db), echo=False) +def insert_students_into_db(session, students): + try: + # --- start db session --- + session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) + for s in students]) -# Criate schema if needed -Base.metadata.create_all(engine) + session.commit() -Session = sessionmaker(bind=engine) + except sa.exc.IntegrityError: + print('!!! Integrity error. User(s) already in database. None inserted !!!\n') + session.rollback() -# --- start session --- -try: - session = Session() - # add administrator - session.add(Student(id='0', name='Professor', password='')) - - # add students - if args.csvfile: - # from csv file if available - try: - csvreader = csv.DictReader(open(args.csvfile, encoding='iso-8859-1'), delimiter=';', quotechar='"', skipinitialspace=True) - except OSError: - print(f'Error: Can\'t open CSV file "{args.csvfile}".') - session.rollback() - sys.exit(1) +# =========================================================================== +def show_students_in_database(session, verbose=False): + try: + users = session.query(Student).order_by(Student.id).all() + except: + raise + else: + n = len(users) + print(f'Registered users:') + if n == 0: + print(' -- none --') else: - session.add_all([Student(id=r['N.º'], name=fix(r['Nome']), password='') for r in csvreader]) - elif args.demo: - # add a few fake students - fakes = [ - ['1915', 'Alan Turing'], - ['1938', 'Donald Knuth'], - ['1815', 'Ada Lovelace'], - ['1969', 'Linus Torvalds'], - ['1955', 'Tim Burners-Lee'], - ['1916', 'Claude Shannon'], - ['1903', 'John von Neumann'], - ] - session.add_all([Student(id=i, name=name, password='') for i,name in fakes]) - - session.commit() - -except Exception: - print('Error: Database already exists.') - session.rollback() - sys.exit(1) - -else: - n = session.query(Student).count() - print(f'New database created: {args.db}\n{n} user(s) inserted:') - - users = session.query(Student).order_by(Student.id).all() - print(f' {users[0].id:8} - {users[0].name} (administrator)') - if n > 1: - print(f' {users[1].id:8} - {users[1].name}') - if n > 3: - print(' ... ...') - if n > 2: - print(f' {users[-1].id:8} - {users[-1].name}') - -# --- end session --- + if verbose: + for u in users: + print(f'{u.id:>12} {u.name}') + else: + print(f'{users[0].id:>12} {users[0].name}') + if n > 1: + print(f'{users[1].id:>12} {users[1].name}') + if n > 3: + print(' | |') + if n > 2: + print(f'{users[-1].id:>12} {users[-1].name}') + print(f'Total: {n}.') + + +# =========================================================================== +if __name__=='__main__': + args = parse_commandline_arguments() + + # --- make list of students to insert/update + students = [] + + for csvfile in args.csvfile: + print('Adding users from:', csvfile) + students.extend(get_students_from_csv(csvfile)) + + if args.admin: + print('Adding user: 0, Admin.') + students.append({'uid': '0', 'name': 'Admin'}) + + if args.add: + for uid, name in args.add: + print(f'Adding user: {uid}, {name}.') + students.append({'uid': uid, 'name': name}) + + # --- password hashing + if students: + print(f'Generating password hashes (bcrypt).') + hash_func = lambda s: hashpw(s, args.pw) + with ThreadPoolExecutor() as executor: + executor.map(hash_func, students) # hashing in parallel + + print() + + # --- database stuff + print(f'Using database: ', args.db) + engine = sa.create_engine(f'sqlite:///{args.db}', echo=False) + Base.metadata.create_all(engine) # Criate schema if needed + Session = sa.orm.sessionmaker(bind=engine) + session = Session() + + if students: + print(f'Inserting {len(students)}') + insert_students_into_db(session, students) + + for s in args.update: + print(f'Updating password of: {s}') + u = session.query(Student).get(s) + pw =(args.pw or s).encode('utf-8') + u.password = bcrypt.hashpw(pw, bcrypt.gensalt()) + session.commit() + + show_students_in_database(session, args.verbose) + + session.close() -- libgit2 0.21.2