diff --git a/BUGS.md b/BUGS.md index 03ec903..eb3cfd9 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,9 +1,9 @@ # BUGS +- generators e correct scripts que durem muito tempo bloqueiam o eventloop do tornado. - change password modal nao aparece no ipad (safari e firefox) - on start topic, logs show questionhandler.get() twice. -- generators e correct scripts que durem muito tempo podem bloquear o loop do tornado? - detect questions in questions.yaml without ref -> error ou generate default. - Criar outra estrutura organizada em capítulos (conjuntos de tópicos). Permitir capítulos de capítulos, etc. talvez usar grafos de grafos... - session management. close after inactive time. diff --git a/addstudent.py b/addstudent.py deleted file mode 100755 index d330ffe..0000000 --- a/addstudent.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 - -import csv -import argparse -import re -import string -from sys import exit - -import bcrypt -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from models import Base, Student - -# =========================================================================== -# Parse command line options -# =========================================================================== -argparser = argparse.ArgumentParser(description='Add new student to database or update its password') -argparser.add_argument('--db', default='students.db', type=str, help='database filename') -argparser.add_argument('uid', type=str, default='', help='Student ID') -argparser.add_argument('password', type=str, default='', help='Student password') -argparser.add_argument('name', nargs='*', type=str, default='', help='Student name') -args = argparser.parse_args() - -# =======================================================x==================== -engine = create_engine(f'sqlite:///{args.db}', echo=False) -Base.metadata.create_all(engine) # Criate schema if needed -Session = sessionmaker(bind=engine) - -uid = args.uid -password = bcrypt.hashpw(args.password.encode('utf-8'), bcrypt.gensalt()) -name = ' '.join(args.name) - -# --- start db session --- -session = Session() - -u = session.query(Student).get(uid) - -if u is None: - session.add(Student(id=uid, name=name, password=password)) - print(f'New user added to "{args.db}".') - -else: - print('Student already exists.') - if input(' Update password? [y/n] ') in ('y', 'yes', 'Y', 'Yes', 'YES'): - u.password = password - print(' Password updated.') - else: - print(' Nothing done.') - -session.commit() -session.close() -# --- end db session --- diff --git a/demo/solar_system/questions.yaml b/demo/solar_system/questions.yaml index cc21d04..9b1a702 100644 --- a/demo/solar_system/questions.yaml +++ b/demo/solar_system/questions.yaml @@ -40,8 +40,9 @@ type: textarea title: Sistema solar text: Escreva o nome dos três planetas mais próximos do Sol. (Exemplo `A, B e C`) - correct: correct-first_3_planets.py + # correct: correct-first_3_planets.py + correct: correct-timeout.py # opcional answer: Vulcano, Krypton, Plutão lines: 3 - timeout: 5 + timeout: 50 diff --git a/initdb.py b/initdb.py index b14d026..fc33923 100755 --- a/initdb.py +++ b/initdb.py @@ -31,113 +31,166 @@ async def hash_all_passwords(executor, students): await asyncio.wait(tasks) # block until all tasks are done print() +# =========================================================================== # 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 def parse_commandline_arguments(): argparser = argparse.ArgumentParser( - description='Create new database from a CSV file (SIIUE format)') + 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 filename') + help='database file') - argparser.add_argument('--demo', + argparser.add_argument('-A', '--admin', action='store_true', - help='initialize database with a few fake students') + 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='', type=str, - help='default password') + help='set password for new and updated users') - argparser.add_argument('csvfile', - nargs='?', - type=str, - default='', - help='CSV filename') + argparser.add_argument('-V', '--verbose', + action='store_true', + help='show all students in database') return argparser.parse_args() + # =========================================================================== 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: CSV file "{filename}" not found.') - exit(1) - students = [{ - 'uid': s['N.º'], - 'name': fix(s['Nome']) - } for s in csvreader] + print(f'!!! Error. File "{filename}" not found !!!') + students = [] + else: + students = [{ + 'uid': s['N.º'], + 'name': fix(s['Nome']) + } for s in csvreader] return students + # =========================================================================== -args = parse_commandline_arguments() - -if args.csvfile: - students = get_students_from_csv(args.csvfile) -elif args.demo: - students = [ - {'uid': '1915', 'name': 'Alan Turing'}, - {'uid': '1938', 'name': 'Donald Knuth'}, - {'uid': '1815', 'name': 'Ada Lovelace'}, - {'uid': '1969', 'name': 'Linus Torvalds'}, - {'uid': '1955', 'name': 'Tim Burners-Lee'}, - {'uid': '1916', 'name': 'Claude Shannon'}, - {'uid': '1903', 'name': 'John von Neumann'}] -students.append({'uid': '0', 'name': 'Admin'}) - -if args.pw: - for s in students: - s['pw'] = args.pw - -print(f'Generating {len(students)} bcrypt password hashes.') -executor = ThreadPoolExecutor() -event_loop = asyncio.get_event_loop() -event_loop.run_until_complete(hash_all_passwords(executor, students)) -event_loop.close() - -print(f'Creating 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) - -try: - # --- start db session --- +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]) + + session.commit() + + except sa.exc.IntegrityError: + print('!!! Integrity error. User(s) already in database. None inserted !!!\n') + session.rollback() + + +# =========================================================================== +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'Users registered:') + if n == 0: + print(' -- none --') + else: + 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).') + if args.pw: # maybe set default password + for s in students: + s['pw'] = args.pw + + executor = ThreadPoolExecutor() + event_loop = asyncio.get_event_loop() + event_loop.run_until_complete(hash_all_passwords(executor, students)) + event_loop.close() + + # --- 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() - session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) - for s in students]) - - n = session.query(Student).count() - print(f'{n} user(s):') - - 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}') - -except sa.exc.IntegrityError: - print('!!! Integrity error !!!') - session.rollback() - -except Exception as e: - print(f'Error: Database "{args.db}" already exists?') - session.rollback() - raise e - # exit(1) - -else: - # --- end session --- - session.commit() + if students: + print(f'Inserting {len(students)}') + insert_students_into_db(session, students) + + # print(args.update) + 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() diff --git a/tools.py b/tools.py index b4d0607..f45a110 100644 --- a/tools.py +++ b/tools.py @@ -154,6 +154,8 @@ def load_yaml(filename, default=None): # --------------------------------------------------------------------------- # Runs a script and returns its stdout parsed as yaml, or None on error. +# The script is run in another process but this function blocks waiting +# for its termination. # --------------------------------------------------------------------------- def run_script(script, stdin='', timeout=5): script = path.expanduser(script) -- libgit2 0.21.2