Commit b724b70f2c9f19f82a8ea9113e6f41d008b2b008

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

initdb: add option -U, --update-all to update password of all students

other minor changes
1 1
2 # BUGS 2 # BUGS
3 3
4 -- cookies existe um perguntations_user e um user. De onde vem o user? 4 +- correct devia poder ser corrido mais que uma vez (por exemplo para alterar cotacoes, corrigir perguntas)
5 - nao esta a mostrar imagens?? internal server error? 5 - nao esta a mostrar imagens?? internal server error?
6 -- JOBE correct async  
7 -- esta a corrigir código JOBE mesmo que nao tenha respondido??? 6 +- guardar testes em JSON assim que sao atribuidos aos alunos (ou guardados inicialmente com um certo nome, e atribuidos posteriormente ao aluno).
  7 +- cookies existe um perguntations_user e um user. De onde vem o user?
8 - QuestionCode falta reportar nos comments os vários erros que podem ocorrer (timeout, etc) 8 - QuestionCode falta reportar nos comments os vários erros que podem ocorrer (timeout, etc)
9 -  
10 - algumas vezes a base de dados guarda o mesmo teste em duplicado. ver se dois submits dao origem a duas correcções. 9 - algumas vezes a base de dados guarda o mesmo teste em duplicado. ver se dois submits dao origem a duas correcções.
11 talvez a base de dados devesse ter como chave do teste um id que fosse único desse teste particular (não um auto counter, nem ref do teste) 10 talvez a base de dados devesse ter como chave do teste um id que fosse único desse teste particular (não um auto counter, nem ref do teste)
12 - em caso de timeout na submissão (e.g. JOBE ou script nao responde) a correcção não termina e o teste não é guardado. 11 - em caso de timeout na submissão (e.g. JOBE ou script nao responde) a correcção não termina e o teste não é guardado.
@@ -19,11 +18,12 @@ talvez a base de dados devesse ter como chave do teste um id que fosse único de @@ -19,11 +18,12 @@ talvez a base de dados devesse ter como chave do teste um id que fosse único de
19 - a revisao do teste não mostra as imagens. 18 - a revisao do teste não mostra as imagens.
20 - Test.reset_answers() unused. 19 - Test.reset_answers() unused.
21 - teste nao esta a mostrar imagens de vez em quando.??? 20 - teste nao esta a mostrar imagens de vez em quando.???
22 -- testar as perguntas todas no início do teste como o aprendizations.  
23 - show-ref nao esta a funcionar na correccao (pelo menos) 21 - show-ref nao esta a funcionar na correccao (pelo menos)
24 22
25 # TODO 23 # TODO
26 24
  25 +- JOBE correct async
  26 +- esta a corrigir código JOBE mesmo que nao tenha respondido???
27 - permitir remover alunos que estão online para poderem comecar de novo. 27 - permitir remover alunos que estão online para poderem comecar de novo.
28 - guardar nota final grade truncado em zero e sem ser truncado (quando é necessário fazer correcções à mão às perguntas, é necessário o valor não truncado) 28 - guardar nota final grade truncado em zero e sem ser truncado (quando é necessário fazer correcções à mão às perguntas, é necessário o valor não truncado)
29 - stress tests. use https://locust.io 29 - stress tests. use https://locust.io
@@ -74,6 +74,8 @@ ou usar push (websockets?) @@ -74,6 +74,8 @@ ou usar push (websockets?)
74 74
75 # FIXED 75 # FIXED
76 76
  77 +- testar as perguntas todas no início do teste como o aprendizations.
  78 +- adicionar identificacao do aluno no jumbotron inicial do teste, para que ao imprimir para pdf a identificacao do aluno venha escrita no documento.
77 - internal server error quando em --review, download csv detalhado. 79 - internal server error quando em --review, download csv detalhado.
78 - perguntas repetidas (mesma ref) dao asneira, porque a referencia é usada como chave em varios sitios e as chaves nao podem ser dupplicadas. 80 - perguntas repetidas (mesma ref) dao asneira, porque a referencia é usada como chave em varios sitios e as chaves nao podem ser dupplicadas.
79 da asneira pelo menos na funcao get_questions_csv. na base de dados tem de estar registado tb o numero da pergunta, caso contrario é impossível saber a qual corresponde. 81 da asneira pelo menos na funcao get_questions_csv. na base de dados tem de estar registado tb o numero da pergunta, caso contrario é impossível saber a qual corresponde.
perguntations/__init__.py
@@ -32,7 +32,7 @@ proof of submission and for review. @@ -32,7 +32,7 @@ proof of submission and for review.
32 ''' 32 '''
33 33
34 APP_NAME = 'perguntations' 34 APP_NAME = 'perguntations'
35 -APP_VERSION = '2020.12.dev1' 35 +APP_VERSION = '2021.01.dev1'
36 APP_DESCRIPTION = __doc__ 36 APP_DESCRIPTION = __doc__
37 37
38 __author__ = 'Miguel Barão' 38 __author__ = 'Miguel Barão'
perguntations/initdb.py
@@ -42,24 +42,28 @@ def parse_commandline_arguments(): @@ -42,24 +42,28 @@ def parse_commandline_arguments():
42 42
43 parser.add_argument('-A', '--admin', 43 parser.add_argument('-A', '--admin',
44 action='store_true', 44 action='store_true',
45 - help='insert the admin user') 45 + help='insert admin user 0 "Admin"')
46 46
47 parser.add_argument('-a', '--add', 47 parser.add_argument('-a', '--add',
48 nargs=2, 48 nargs=2,
49 action='append', 49 action='append',
50 metavar=('uid', 'name'), 50 metavar=('uid', 'name'),
51 - help='add new user') 51 + help='add new user id and name')
52 52
53 parser.add_argument('-u', '--update', 53 parser.add_argument('-u', '--update',
54 nargs='+', 54 nargs='+',
55 metavar='uid', 55 metavar='uid',
56 default=[], 56 default=[],
57 - help='users to update') 57 + help='list of users whose password is to be updated')
  58 +
  59 + parser.add_argument('-U', '--update-all',
  60 + action='store_true',
  61 + help='all except admin will have the password updated')
58 62
59 parser.add_argument('--pw', 63 parser.add_argument('--pw',
60 default=None, 64 default=None,
61 type=str, 65 type=str,
62 - help='set password for new and updated users') 66 + help='password for new or updated users')
63 67
64 parser.add_argument('-V', '--verbose', 68 parser.add_argument('-V', '--verbose',
65 action='store_true', 69 action='store_true',
@@ -152,45 +156,55 @@ def main(): @@ -152,45 +156,55 @@ def main():
152 156
153 args = parse_commandline_arguments() 157 args = parse_commandline_arguments()
154 158
155 - # --- make list of students to insert/update  
156 - students = [] 159 + # --- database
  160 + print(f'Using database: {args.db}')
  161 + engine = sa.create_engine(f'sqlite:///{args.db}', echo=False)
  162 + Base.metadata.create_all(engine) # Criates schema if needed
  163 + SessionMaker = sa.orm.sessionmaker(bind=engine)
  164 + session = SessionMaker()
157 165
158 - for csvfile in args.csvfile:  
159 - print('Adding users from:', csvfile)  
160 - students.extend(get_students_from_csv(csvfile)) 166 + # --- make list of students to insert
  167 + new_students = []
161 168
162 if args.admin: 169 if args.admin:
163 print('Adding user: 0, Admin.') 170 print('Adding user: 0, Admin.')
164 - students.append({'uid': '0', 'name': 'Admin'}) 171 + new_students.append({'uid': '0', 'name': 'Admin'})
  172 +
  173 + for csvfile in args.csvfile:
  174 + print('Adding users from:', csvfile)
  175 + new_students.extend(get_students_from_csv(csvfile))
165 176
166 if args.add: 177 if args.add:
167 for uid, name in args.add: 178 for uid, name in args.add:
168 print(f'Adding user: {uid}, {name}.') 179 print(f'Adding user: {uid}, {name}.')
169 - students.append({'uid': uid, 'name': name}) 180 + new_students.append({'uid': uid, 'name': name})
170 181
171 - # --- password hashing  
172 - if students: 182 + # --- insert new students
  183 + if new_students:
173 print('Generating password hashes', end='') 184 print('Generating password hashes', end='')
174 with ThreadPoolExecutor() as executor: # hashing in parallel 185 with ThreadPoolExecutor() as executor: # hashing in parallel
175 - executor.map(lambda s: hashpw(s, args.pw), students) 186 + executor.map(lambda s: hashpw(s, args.pw), new_students)
  187 + print(f'\nInserting {len(new_students)}')
  188 + insert_students_into_db(session, new_students)
  189 +
  190 + # --- update all students
  191 + if args.update_all:
  192 + all_students = session.query(Student).filter(Student.id != '0').all()
  193 + print(f'Updating password of {len(all_students)} users', end='')
  194 + for student in all_students:
  195 + password = (args.pw or student.id).encode('utf-8')
  196 + student.password = bcrypt.hashpw(password, bcrypt.gensalt())
  197 + print('.', end='', flush=True)
176 print() 198 print()
  199 + session.commit()
177 200
178 - # --- database stuff  
179 - print(f'Using database: {args.db}')  
180 - engine = sa.create_engine(f'sqlite:///{args.db}', echo=False)  
181 - Base.metadata.create_all(engine) # Criates schema if needed  
182 - SessionMaker = sa.orm.sessionmaker(bind=engine)  
183 - session = SessionMaker()  
184 -  
185 - if students:  
186 - print(f'Inserting {len(students)}')  
187 - insert_students_into_db(session, students)  
188 -  
189 - for student_id in args.update:  
190 - print(f'Updating password of: {student_id}')  
191 - student = session.query(Student).get(student_id)  
192 - password = (args.pw or student_id).encode('utf-8')  
193 - student.password = bcrypt.hashpw(password, bcrypt.gensalt()) 201 + # --- update some students
  202 + else:
  203 + for student_id in args.update:
  204 + print(f'Updating password of {student_id}')
  205 + student = session.query(Student).get(student_id)
  206 + password = (args.pw or student_id).encode('utf-8')
  207 + student.password = bcrypt.hashpw(password, bcrypt.gensalt())
194 session.commit() 208 session.commit()
195 209
196 show_students_in_database(session, args.verbose) 210 show_students_in_database(session, args.verbose)
perguntations/serve.py
@@ -50,7 +50,7 @@ class WebApplication(tornado.web.Application): @@ -50,7 +50,7 @@ class WebApplication(tornado.web.Application):
50 50
51 settings = { 51 settings = {
52 'template_path': path.join(path.dirname(__file__), 'templates'), 52 'template_path': path.join(path.dirname(__file__), 'templates'),
53 - 'static_path': path.join(path.dirname(__file__), 'static'), 53 + 'static_path': path.join(path.dirname(__file__), 'static'),
54 'static_url_prefix': '/static/', 54 'static_url_prefix': '/static/',
55 'xsrf_cookies': True, 55 'xsrf_cookies': True,
56 'cookie_secret': base64.b64encode(uuid.uuid4().bytes), 56 'cookie_secret': base64.b64encode(uuid.uuid4().bytes),
@@ -209,7 +209,7 @@ class LogoutHandler(BaseHandler): @@ -209,7 +209,7 @@ class LogoutHandler(BaseHandler):
209 209
210 210
211 # ---------------------------------------------------------------------------- 211 # ----------------------------------------------------------------------------
212 -# Test shown to students 212 +# Handles the TEST
213 # ---------------------------------------------------------------------------- 213 # ----------------------------------------------------------------------------
214 # pylint: disable=abstract-method 214 # pylint: disable=abstract-method
215 class RootHandler(BaseHandler): 215 class RootHandler(BaseHandler):
@@ -298,7 +298,7 @@ class RootHandler(BaseHandler): @@ -298,7 +298,7 @@ class RootHandler(BaseHandler):
298 298
299 # show final grade and grades of other tests in the database 299 # show final grade and grades of other tests in the database
300 # allgrades = self.testapp.get_student_grades_from_all_tests(uid) 300 # allgrades = self.testapp.get_student_grades_from_all_tests(uid)
301 - grade = self.testapp.get_student_grade(uid) 301 + # grade = self.testapp.get_student_grade(uid)
302 302
303 self.render('grade.html', t=test) 303 self.render('grade.html', t=test)
304 self.clear_cookie('perguntations_user') 304 self.clear_cookie('perguntations_user')
perguntations/templates/question-checkbox.html
@@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
8 <a class="list-group-item"> 8 <a class="list-group-item">
9 <div class="d-flex flex-row"> 9 <div class="d-flex flex-row">
10 <div class="p-2"> 10 <div class="p-2">
11 - <input type="checkbox" id="{{i}}:{{n}}" name="{{i}}" value="{{n}}"> 11 + <input type="checkbox" id="{{i}}:{{n}}" name="{{i}}" value="{{n}}" autocomplete="off">
12 </div> 12 </div>
13 <div class="p-2"> 13 <div class="p-2">
14 <label for="{{i}}:{{n}}">{{ md(opt) }}</label> 14 <label for="{{i}}:{{n}}">{{ md(opt) }}</label>
@@ -18,4 +18,4 @@ @@ -18,4 +18,4 @@
18 {% end %} 18 {% end %}
19 </div> 19 </div>
20 </fieldset> 20 </fieldset>
21 -{% end %}  
22 \ No newline at end of file 21 \ No newline at end of file
  22 +{% end %}
perguntations/templates/question-radio.html
@@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
8 <a class="list-group-item"> 8 <a class="list-group-item">
9 <div class="d-flex flex-row"> 9 <div class="d-flex flex-row">
10 <div class="p-2"> 10 <div class="p-2">
11 - <input type="radio" id="{{i}}:{{n}}" name="{{i}}" value="{{n}}"> 11 + <input type="radio" id="{{i}}:{{n}}" name="{{i}}" value="{{n}}" autocomplete="off">
12 </div> 12 </div>
13 <div class="p-2"> 13 <div class="p-2">
14 <label for="{{i}}:{{n}}">{{ md(opt) }}</label> 14 <label for="{{i}}:{{n}}">{{ md(opt) }}</label>
@@ -18,4 +18,4 @@ @@ -18,4 +18,4 @@
18 {% end %} 18 {% end %}
19 </div> 19 </div>
20 </fieldset> 20 </fieldset>
21 -{% end %}  
22 \ No newline at end of file 21 \ No newline at end of file
  22 +{% end %}
perguntations/templates/question-text.html
@@ -2,6 +2,6 @@ @@ -2,6 +2,6 @@
2 2
3 {% block answer %} 3 {% block answer %}
4 <fieldset data-role="controlgroup"> 4 <fieldset data-role="controlgroup">
5 - <input type="text" class="form-control" id="{{i}}" name="{{i}}" value=""> 5 + <input type="text" class="form-control" id="{{i}}" name="{{i}}" value="" autocomplete="off">
6 </fieldset> 6 </fieldset>
7 {% end %} 7 {% end %}
perguntations/templates/question-textarea.html
@@ -2,6 +2,6 @@ @@ -2,6 +2,6 @@
2 2
3 {% block answer %} 3 {% block answer %}
4 4
5 -<textarea class="form-control" name="{{i}}">{{q['answer'] or ''}}</textarea><br /> 5 +<textarea class="form-control" name="{{i}}" autocomplete="off">{{q['answer'] or ''}}</textarea><br />
6 6
7 {% end %} 7 {% end %}
perguntations/templates/test.html
@@ -88,11 +88,18 @@ @@ -88,11 +88,18 @@
88 <div class="container"> 88 <div class="container">
89 89
90 <div class="jumbotron"> 90 <div class="jumbotron">
91 - <h1 class="display-5">{{ t['title'] }}</h1>  
92 - 91 + <h3 class="display-5">{{ t['title'] }}</h3>
93 <hr> 92 <hr>
94 93
95 - <h5> 94 + <div class="row">
  95 + <label for="nome" class="col-sm-3">Nome:</label>
  96 + <div class="col-sm-9" id="nome">{{ escape(t['student']['name']) }}</div>
  97 + </div>
  98 + <div class="row">
  99 + <label for="numero" class="col-sm-3">Número:</label>
  100 + <div class="col-sm-9" id="numero">{{ escape(t['student']['number']) }}</div>
  101 + </div>
  102 +
96 <div class="row"> 103 <div class="row">
97 <label for="duracao" class="col-sm-3">Duração:</label> 104 <label for="duracao" class="col-sm-3">Duração:</label>
98 <div class="col-sm-9" id="duracao">{{ str(t['duration'])+' minutos' if t['duration'] > 0 else 'sem limite de tempo' }}</div> 105 <div class="col-sm-9" id="duracao">{{ str(t['duration'])+' minutos' if t['duration'] > 0 else 'sem limite de tempo' }}</div>
@@ -101,10 +108,9 @@ @@ -101,10 +108,9 @@
101 <label for="submissao" class="col-sm-3">Submissão:</label> 108 <label for="submissao" class="col-sm-3">Submissão:</label>
102 <div class="col-sm-9" id="submissao">{{ 'automática no fim do tempo' if t['autosubmit'] else 'manual' }}</div> 109 <div class="col-sm-9" id="submissao">{{ 'automática no fim do tempo' if t['autosubmit'] else 'manual' }}</div>
103 </div> 110 </div>
104 - </h5>  
105 </div> 111 </div>
106 112
107 - <form action="/" method="post" id="test" autocomplete="off"> 113 + <form action="/" method="post" id="test">
108 {% module xsrf_form_html() %} 114 {% module xsrf_form_html() %}
109 115
110 {% for i, q in enumerate(t['questions']) %} 116 {% for i, q in enumerate(t['questions']) %}
perguntations/testfactory.py
@@ -234,7 +234,7 @@ class TestFactory(dict): @@ -234,7 +234,7 @@ class TestFactory(dict):
234 234
235 if question['type'] in ('code', 'textarea'): 235 if question['type'] in ('code', 'textarea'):
236 if 'tests_right' in question: 236 if 'tests_right' in question:
237 - for i, right_answer in enumerate(question['tests_right']): 237 + for tnum, right_answer in enumerate(question['tests_right']):
238 try: 238 try:
239 question.set_answer(right_answer) 239 question.set_answer(right_answer)
240 question.correct() 240 question.correct()
@@ -243,11 +243,11 @@ class TestFactory(dict): @@ -243,11 +243,11 @@ class TestFactory(dict):
243 raise TestFactoryException(msg) from exc 243 raise TestFactoryException(msg) from exc
244 244
245 if question['grade'] == 1.0: 245 if question['grade'] == 1.0:
246 - logger.info(' test %i Ok', i) 246 + logger.info(' test %i Ok', tnum)
247 else: 247 else:
248 - logger.error(' TEST %i IS WRONG!!!', i) 248 + logger.error(' TEST %i IS WRONG!!!', tnum)
249 elif 'tests_wrong' in question: 249 elif 'tests_wrong' in question:
250 - for i, wrong_answer in enumerate(question['tests_wrong']): 250 + for tnum, wrong_answer in enumerate(question['tests_wrong']):
251 try: 251 try:
252 question.set_answer(wrong_answer) 252 question.set_answer(wrong_answer)
253 question.correct() 253 question.correct()
@@ -256,9 +256,9 @@ class TestFactory(dict): @@ -256,9 +256,9 @@ class TestFactory(dict):
256 raise TestFactoryException(msg) from exc 256 raise TestFactoryException(msg) from exc
257 257
258 if question['grade'] < 1.0: 258 if question['grade'] < 1.0:
259 - logger.info(' test %i Ok', i) 259 + logger.info(' test %i Ok', tnum)
260 else: 260 else:
261 - logger.error(' TEST %i IS WRONG!!!', i) 261 + logger.error(' TEST %i IS WRONG!!!', tnum)
262 else: 262 else:
263 try: 263 try:
264 question.set_answer('') 264 question.set_answer('')
update.sh 0 → 100755
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +#!/bin/sh
  2 +
  3 +git pull
  4 +npm update
  5 +pip install -U .
  6 +