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