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

BUGS.md
1 1  
2 2 # BUGS
3 3  
  4 +- generators e correct scripts que durem muito tempo bloqueiam o eventloop do tornado.
4 5 - change password modal nao aparece no ipad (safari e firefox)
5 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 7 - detect questions in questions.yaml without ref -> error ou generate default.
8 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 9 - session management. close after inactive time.
... ...
addstudent.py
... ... @@ -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 40 type: textarea
41 41 title: Sistema solar
42 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 45 # opcional
45 46 answer: Vulcano, Krypton, Plutão
46 47 lines: 3
47   - timeout: 5
  48 + timeout: 50
... ...
initdb.py
... ... @@ -31,113 +31,166 @@ async def hash_all_passwords(executor, students):
31 31 await asyncio.wait(tasks) # block until all tasks are done
32 32 print()
33 33  
  34 +# ===========================================================================
34 35 # SIIUE names have alien strings like "(TE)" and are sometimes capitalized
35 36 # We remove them so that students dont keep asking what it means
36 37 def fix(name):
37 38 return string.capwords(re.sub('\(.*\)', '', name).strip())
38 39  
  40 +
39 41 # ===========================================================================
40 42 # Parse command line options
41 43 def parse_commandline_arguments():
42 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 54 argparser.add_argument('--db',
46 55 default='students.db',
47 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 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 75 argparser.add_argument('--pw',
55 76 default='',
56 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 84 return argparser.parse_args()
66 85  
  86 +
67 87 # ===========================================================================
68 88 def get_students_from_csv(filename):
69 89 try:
70 90 csvreader = csv.DictReader(open(filename, encoding='iso-8859-1'), delimiter=';', quotechar='"', skipinitialspace=True)
71 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 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 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 154  
155 155 # ---------------------------------------------------------------------------
156 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 160 def run_script(script, stdin='', timeout=5):
159 161 script = path.expanduser(script)
... ...