Commit ff655aef8a5cf33a7915d6178981565d55756c15
1 parent
1cb1329f
Exists in
master
and in
1 other branch
- large rewrite of initdb, with new options
Showing
5 changed files
with
135 additions
and
132 deletions
Show diff stats
BUGS.md
| 1 | 1 | ||
| 2 | # BUGS | 2 | # BUGS |
| 3 | 3 | ||
| 4 | +- generators e correct scripts que durem muito tempo bloqueiam o eventloop do tornado. | ||
| 4 | - change password modal nao aparece no ipad (safari e firefox) | 5 | - change password modal nao aparece no ipad (safari e firefox) |
| 5 | - on start topic, logs show questionhandler.get() twice. | 6 | - on start topic, logs show questionhandler.get() twice. |
| 6 | -- generators e correct scripts que durem muito tempo podem bloquear o loop do tornado? | ||
| 7 | - detect questions in questions.yaml without ref -> error ou generate default. | 7 | - detect questions in questions.yaml without ref -> error ou generate default. |
| 8 | - Criar outra estrutura organizada em capítulos (conjuntos de tópicos). Permitir capítulos de capítulos, etc. talvez usar grafos de grafos... | 8 | - Criar outra estrutura organizada em capítulos (conjuntos de tópicos). Permitir capítulos de capítulos, etc. talvez usar grafos de grafos... |
| 9 | - session management. close after inactive time. | 9 | - session management. close after inactive time. |
addstudent.py
| @@ -1,53 +0,0 @@ | @@ -1,53 +0,0 @@ | ||
| 1 | -#!/usr/bin/env python3 | ||
| 2 | - | ||
| 3 | -import csv | ||
| 4 | -import argparse | ||
| 5 | -import re | ||
| 6 | -import string | ||
| 7 | -from sys import exit | ||
| 8 | - | ||
| 9 | -import bcrypt | ||
| 10 | -from sqlalchemy import create_engine | ||
| 11 | -from sqlalchemy.orm import sessionmaker | ||
| 12 | - | ||
| 13 | -from models import Base, Student | ||
| 14 | - | ||
| 15 | -# =========================================================================== | ||
| 16 | -# Parse command line options | ||
| 17 | -# =========================================================================== | ||
| 18 | -argparser = argparse.ArgumentParser(description='Add new student to database or update its password') | ||
| 19 | -argparser.add_argument('--db', default='students.db', type=str, help='database filename') | ||
| 20 | -argparser.add_argument('uid', type=str, default='', help='Student ID') | ||
| 21 | -argparser.add_argument('password', type=str, default='', help='Student password') | ||
| 22 | -argparser.add_argument('name', nargs='*', type=str, default='', help='Student name') | ||
| 23 | -args = argparser.parse_args() | ||
| 24 | - | ||
| 25 | -# =======================================================x==================== | ||
| 26 | -engine = create_engine(f'sqlite:///{args.db}', echo=False) | ||
| 27 | -Base.metadata.create_all(engine) # Criate schema if needed | ||
| 28 | -Session = sessionmaker(bind=engine) | ||
| 29 | - | ||
| 30 | -uid = args.uid | ||
| 31 | -password = bcrypt.hashpw(args.password.encode('utf-8'), bcrypt.gensalt()) | ||
| 32 | -name = ' '.join(args.name) | ||
| 33 | - | ||
| 34 | -# --- start db session --- | ||
| 35 | -session = Session() | ||
| 36 | - | ||
| 37 | -u = session.query(Student).get(uid) | ||
| 38 | - | ||
| 39 | -if u is None: | ||
| 40 | - session.add(Student(id=uid, name=name, password=password)) | ||
| 41 | - print(f'New user added to "{args.db}".') | ||
| 42 | - | ||
| 43 | -else: | ||
| 44 | - print('Student already exists.') | ||
| 45 | - if input(' Update password? [y/n] ') in ('y', 'yes', 'Y', 'Yes', 'YES'): | ||
| 46 | - u.password = password | ||
| 47 | - print(' Password updated.') | ||
| 48 | - else: | ||
| 49 | - print(' Nothing done.') | ||
| 50 | - | ||
| 51 | -session.commit() | ||
| 52 | -session.close() | ||
| 53 | -# --- end db session --- |
demo/solar_system/questions.yaml
| @@ -40,8 +40,9 @@ | @@ -40,8 +40,9 @@ | ||
| 40 | type: textarea | 40 | type: textarea |
| 41 | title: Sistema solar | 41 | title: Sistema solar |
| 42 | text: Escreva o nome dos três planetas mais próximos do Sol. (Exemplo `A, B e C`) | 42 | text: Escreva o nome dos três planetas mais próximos do Sol. (Exemplo `A, B e C`) |
| 43 | - correct: correct-first_3_planets.py | 43 | + # correct: correct-first_3_planets.py |
| 44 | + correct: correct-timeout.py | ||
| 44 | # opcional | 45 | # opcional |
| 45 | answer: Vulcano, Krypton, Plutão | 46 | answer: Vulcano, Krypton, Plutão |
| 46 | lines: 3 | 47 | lines: 3 |
| 47 | - timeout: 5 | 48 | + timeout: 50 |
initdb.py
| @@ -31,113 +31,166 @@ async def hash_all_passwords(executor, students): | @@ -31,113 +31,166 @@ async def hash_all_passwords(executor, students): | ||
| 31 | await asyncio.wait(tasks) # block until all tasks are done | 31 | await asyncio.wait(tasks) # block until all tasks are done |
| 32 | print() | 32 | print() |
| 33 | 33 | ||
| 34 | +# =========================================================================== | ||
| 34 | # SIIUE names have alien strings like "(TE)" and are sometimes capitalized | 35 | # SIIUE names have alien strings like "(TE)" and are sometimes capitalized |
| 35 | # We remove them so that students dont keep asking what it means | 36 | # We remove them so that students dont keep asking what it means |
| 36 | def fix(name): | 37 | def fix(name): |
| 37 | return string.capwords(re.sub('\(.*\)', '', name).strip()) | 38 | return string.capwords(re.sub('\(.*\)', '', name).strip()) |
| 38 | 39 | ||
| 40 | + | ||
| 39 | # =========================================================================== | 41 | # =========================================================================== |
| 40 | # Parse command line options | 42 | # Parse command line options |
| 41 | def parse_commandline_arguments(): | 43 | def parse_commandline_arguments(): |
| 42 | argparser = argparse.ArgumentParser( | 44 | argparser = argparse.ArgumentParser( |
| 43 | - description='Create new database from a CSV file (SIIUE format)') | 45 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter, |
| 46 | + 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.') | ||
| 47 | + | ||
| 48 | + argparser.add_argument('csvfile', | ||
| 49 | + nargs='*', | ||
| 50 | + type=str, | ||
| 51 | + default='', | ||
| 52 | + help='CSV file to import (SIIUE)') | ||
| 44 | 53 | ||
| 45 | argparser.add_argument('--db', | 54 | argparser.add_argument('--db', |
| 46 | default='students.db', | 55 | default='students.db', |
| 47 | type=str, | 56 | type=str, |
| 48 | - help='database filename') | 57 | + help='database file') |
| 49 | 58 | ||
| 50 | - argparser.add_argument('--demo', | 59 | + argparser.add_argument('-A', '--admin', |
| 51 | action='store_true', | 60 | action='store_true', |
| 52 | - help='initialize database with a few fake students') | 61 | + help='insert the admin user') |
| 62 | + | ||
| 63 | + argparser.add_argument('-a', '--add', | ||
| 64 | + nargs=2, | ||
| 65 | + action='append', | ||
| 66 | + metavar=('uid', 'name'), | ||
| 67 | + help='add new user') | ||
| 68 | + | ||
| 69 | + argparser.add_argument('-u', '--update', | ||
| 70 | + nargs='+', | ||
| 71 | + metavar='uid', | ||
| 72 | + default=[], | ||
| 73 | + help='users to update') | ||
| 53 | 74 | ||
| 54 | argparser.add_argument('--pw', | 75 | argparser.add_argument('--pw', |
| 55 | default='', | 76 | default='', |
| 56 | type=str, | 77 | type=str, |
| 57 | - help='default password') | 78 | + help='set password for new and updated users') |
| 58 | 79 | ||
| 59 | - argparser.add_argument('csvfile', | ||
| 60 | - nargs='?', | ||
| 61 | - type=str, | ||
| 62 | - default='', | ||
| 63 | - help='CSV filename') | 80 | + argparser.add_argument('-V', '--verbose', |
| 81 | + action='store_true', | ||
| 82 | + help='show all students in database') | ||
| 64 | 83 | ||
| 65 | return argparser.parse_args() | 84 | return argparser.parse_args() |
| 66 | 85 | ||
| 86 | + | ||
| 67 | # =========================================================================== | 87 | # =========================================================================== |
| 68 | def get_students_from_csv(filename): | 88 | def get_students_from_csv(filename): |
| 69 | try: | 89 | try: |
| 70 | csvreader = csv.DictReader(open(filename, encoding='iso-8859-1'), delimiter=';', quotechar='"', skipinitialspace=True) | 90 | csvreader = csv.DictReader(open(filename, encoding='iso-8859-1'), delimiter=';', quotechar='"', skipinitialspace=True) |
| 71 | except EnvironmentError: | 91 | except EnvironmentError: |
| 72 | - print(f'Error: CSV file "{filename}" not found.') | ||
| 73 | - exit(1) | ||
| 74 | - students = [{ | ||
| 75 | - 'uid': s['N.º'], | ||
| 76 | - 'name': fix(s['Nome']) | ||
| 77 | - } for s in csvreader] | 92 | + print(f'!!! Error. File "{filename}" not found !!!') |
| 93 | + students = [] | ||
| 94 | + else: | ||
| 95 | + students = [{ | ||
| 96 | + 'uid': s['N.º'], | ||
| 97 | + 'name': fix(s['Nome']) | ||
| 98 | + } for s in csvreader] | ||
| 78 | 99 | ||
| 79 | return students | 100 | return students |
| 80 | 101 | ||
| 102 | + | ||
| 81 | # =========================================================================== | 103 | # =========================================================================== |
| 82 | -args = parse_commandline_arguments() | ||
| 83 | - | ||
| 84 | -if args.csvfile: | ||
| 85 | - students = get_students_from_csv(args.csvfile) | ||
| 86 | -elif args.demo: | ||
| 87 | - students = [ | ||
| 88 | - {'uid': '1915', 'name': 'Alan Turing'}, | ||
| 89 | - {'uid': '1938', 'name': 'Donald Knuth'}, | ||
| 90 | - {'uid': '1815', 'name': 'Ada Lovelace'}, | ||
| 91 | - {'uid': '1969', 'name': 'Linus Torvalds'}, | ||
| 92 | - {'uid': '1955', 'name': 'Tim Burners-Lee'}, | ||
| 93 | - {'uid': '1916', 'name': 'Claude Shannon'}, | ||
| 94 | - {'uid': '1903', 'name': 'John von Neumann'}] | ||
| 95 | -students.append({'uid': '0', 'name': 'Admin'}) | ||
| 96 | - | ||
| 97 | -if args.pw: | ||
| 98 | - for s in students: | ||
| 99 | - s['pw'] = args.pw | ||
| 100 | - | ||
| 101 | -print(f'Generating {len(students)} bcrypt password hashes.') | ||
| 102 | -executor = ThreadPoolExecutor() | ||
| 103 | -event_loop = asyncio.get_event_loop() | ||
| 104 | -event_loop.run_until_complete(hash_all_passwords(executor, students)) | ||
| 105 | -event_loop.close() | ||
| 106 | - | ||
| 107 | -print(f'Creating database: {args.db}') | ||
| 108 | -engine = sa.create_engine(f'sqlite:///{args.db}', echo=False) | ||
| 109 | -Base.metadata.create_all(engine) # Criate schema if needed | ||
| 110 | -Session = sa.orm.sessionmaker(bind=engine) | ||
| 111 | - | ||
| 112 | -try: | ||
| 113 | - # --- start db session --- | 104 | +def insert_students_into_db(session, students): |
| 105 | + try: | ||
| 106 | + # --- start db session --- | ||
| 107 | + session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) | ||
| 108 | + for s in students]) | ||
| 109 | + | ||
| 110 | + session.commit() | ||
| 111 | + | ||
| 112 | + except sa.exc.IntegrityError: | ||
| 113 | + print('!!! Integrity error. User(s) already in database. None inserted !!!\n') | ||
| 114 | + session.rollback() | ||
| 115 | + | ||
| 116 | + | ||
| 117 | +# =========================================================================== | ||
| 118 | +def show_students_in_database(session, verbose=False): | ||
| 119 | + try: | ||
| 120 | + users = session.query(Student).order_by(Student.id).all() | ||
| 121 | + except: | ||
| 122 | + raise | ||
| 123 | + else: | ||
| 124 | + n = len(users) | ||
| 125 | + print(f'Users registered:') | ||
| 126 | + if n == 0: | ||
| 127 | + print(' -- none --') | ||
| 128 | + else: | ||
| 129 | + if verbose: | ||
| 130 | + for u in users: | ||
| 131 | + print(f'{u.id:>12} {u.name}') | ||
| 132 | + else: | ||
| 133 | + print(f'{users[0].id:>12} {users[0].name}') | ||
| 134 | + if n > 1: | ||
| 135 | + print(f'{users[1].id:>12} {users[1].name}') | ||
| 136 | + if n > 3: | ||
| 137 | + print(' | |') | ||
| 138 | + if n > 2: | ||
| 139 | + print(f'{users[-1].id:>12} {users[-1].name}') | ||
| 140 | + print(f'Total: {n}.') | ||
| 141 | + | ||
| 142 | + | ||
| 143 | +# =========================================================================== | ||
| 144 | +if __name__=='__main__': | ||
| 145 | + args = parse_commandline_arguments() | ||
| 146 | + | ||
| 147 | + # --- make list of students to insert/update | ||
| 148 | + students = [] | ||
| 149 | + | ||
| 150 | + for csvfile in args.csvfile: | ||
| 151 | + print('Adding users from:', csvfile) | ||
| 152 | + students.extend(get_students_from_csv(csvfile)) | ||
| 153 | + | ||
| 154 | + if args.admin: | ||
| 155 | + print('Adding user: 0, admin.') | ||
| 156 | + students.append({'uid': '0', 'name': 'Admin'}) | ||
| 157 | + | ||
| 158 | + if args.add: | ||
| 159 | + for uid, name in args.add: | ||
| 160 | + print(f'Adding user: {uid}, {name}.') | ||
| 161 | + students.append({'uid': uid, 'name': name}) | ||
| 162 | + | ||
| 163 | + # --- password hashing | ||
| 164 | + if students: | ||
| 165 | + print(f'Generating password hashes (bcrypt).') | ||
| 166 | + if args.pw: # maybe set default password | ||
| 167 | + for s in students: | ||
| 168 | + s['pw'] = args.pw | ||
| 169 | + | ||
| 170 | + executor = ThreadPoolExecutor() | ||
| 171 | + event_loop = asyncio.get_event_loop() | ||
| 172 | + event_loop.run_until_complete(hash_all_passwords(executor, students)) | ||
| 173 | + event_loop.close() | ||
| 174 | + | ||
| 175 | + # --- database stuff | ||
| 176 | + print(f'Using database: ', args.db) | ||
| 177 | + engine = sa.create_engine(f'sqlite:///{args.db}', echo=False) | ||
| 178 | + Base.metadata.create_all(engine) # Criate schema if needed | ||
| 179 | + Session = sa.orm.sessionmaker(bind=engine) | ||
| 114 | session = Session() | 180 | session = Session() |
| 115 | 181 | ||
| 116 | - session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) | ||
| 117 | - for s in students]) | ||
| 118 | - | ||
| 119 | - n = session.query(Student).count() | ||
| 120 | - print(f'{n} user(s):') | ||
| 121 | - | ||
| 122 | - users = session.query(Student).order_by(Student.id).all() | ||
| 123 | - print(f' {users[0].id:8} - {users[0].name} (administrator)') | ||
| 124 | - if n > 1: | ||
| 125 | - print(f' {users[1].id:8} - {users[1].name}') | ||
| 126 | - if n > 3: | ||
| 127 | - print(' ... ...') | ||
| 128 | - if n > 2: | ||
| 129 | - print(f' {users[-1].id:8} - {users[-1].name}') | ||
| 130 | - | ||
| 131 | -except sa.exc.IntegrityError: | ||
| 132 | - print('!!! Integrity error !!!') | ||
| 133 | - session.rollback() | ||
| 134 | - | ||
| 135 | -except Exception as e: | ||
| 136 | - print(f'Error: Database "{args.db}" already exists?') | ||
| 137 | - session.rollback() | ||
| 138 | - raise e | ||
| 139 | - # exit(1) | ||
| 140 | - | ||
| 141 | -else: | ||
| 142 | - # --- end session --- | ||
| 143 | - session.commit() | 182 | + if students: |
| 183 | + print(f'Inserting {len(students)}') | ||
| 184 | + insert_students_into_db(session, students) | ||
| 185 | + | ||
| 186 | + # print(args.update) | ||
| 187 | + for s in args.update: | ||
| 188 | + print(f'Updating password of: {s}') | ||
| 189 | + u = session.query(Student).get(s) | ||
| 190 | + pw =(args.pw or s).encode('utf-8') | ||
| 191 | + u.password = bcrypt.hashpw(pw, bcrypt.gensalt()) | ||
| 192 | + session.commit() | ||
| 193 | + | ||
| 194 | + show_students_in_database(session, args.verbose) | ||
| 195 | + | ||
| 196 | + session.close() |
tools.py
| @@ -154,6 +154,8 @@ def load_yaml(filename, default=None): | @@ -154,6 +154,8 @@ def load_yaml(filename, default=None): | ||
| 154 | 154 | ||
| 155 | # --------------------------------------------------------------------------- | 155 | # --------------------------------------------------------------------------- |
| 156 | # Runs a script and returns its stdout parsed as yaml, or None on error. | 156 | # Runs a script and returns its stdout parsed as yaml, or None on error. |
| 157 | +# The script is run in another process but this function blocks waiting | ||
| 158 | +# for its termination. | ||
| 157 | # --------------------------------------------------------------------------- | 159 | # --------------------------------------------------------------------------- |
| 158 | def run_script(script, stdin='', timeout=5): | 160 | def run_script(script, stdin='', timeout=5): |
| 159 | script = path.expanduser(script) | 161 | script = path.expanduser(script) |