Commit ff655aef8a5cf33a7915d6178981565d55756c15

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

- large rewrite of initdb, with new options

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
@@ -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()
@@ -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)