Commit aec7789965f7720768e481b53bc8e6333d8d252c

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

- fix bug in password reset.

- new information question in the demo suggesting use of yamllint.
- satisfy pylint recommendations against logging f-strings
- handle JSON exception
- show file and ref both in the test and review
- add button to get detailed grades of all questions in a test
- version bump to 2020.05.dev3
1 1
2 # BUGS 2 # BUGS
3 3
4 -- retornar None quando nao ha alteracoes relativamente à última vez.  
5 -ou usar push (websockets?)  
6 -- quando scale_max não é 20, as cores das barras continuam a reflectir a escala 0,20  
7 - CRITICAL se answer for `i<n` a revisão de provas mostra apenas i (interpreta `<` como tag?) 4 - CRITICAL se answer for `i<n` a revisão de provas mostra apenas i (interpreta `<` como tag?)
8 -- na pagina grade.html as barras estao normalizadas para os limites scale_min e max do teste actual e nao do realizado. 5 +
  6 +- em admin, quando scale_max não é 20, as cores das barras continuam a reflectir a escala 0,20. a tabela teste na DB não tem a escala desse teste.
  7 +- em grade.html as barras estao normalizadas para os limites scale_min e max do teste actual e nao dos testes realizados no passado (tabela test devia guardar a escala).
9 - codigo `hello world` nao esta a preservar o whitespace. O renderer de markdown gera a tag <code> que não preserva whitespace. Necessario adicionar <pre>. 8 - codigo `hello world` nao esta a preservar o whitespace. O renderer de markdown gera a tag <code> que não preserva whitespace. Necessario adicionar <pre>.
10 -- teste nao esta a mostrar imagens de vez em quando.  
11 - mensagems de erro do assembler aparecem na mesma linha na correcao e nao fazerm rendering do `$t`, ver se servidor faz parse do markdown dessas mensagens. 9 - mensagems de erro do assembler aparecem na mesma linha na correcao e nao fazerm rendering do `$t`, ver se servidor faz parse do markdown dessas mensagens.
12 - impedir os eventos copy/paste. alunos usam isso para trazer codigo ja feito nos computadores. Obrigar a fazer reset? fazer um copy automaticamente? 10 - impedir os eventos copy/paste. alunos usam isso para trazer codigo ja feito nos computadores. Obrigar a fazer reset? fazer um copy automaticamente?
13 - a revisao do teste não mostra as imagens. 11 - a revisao do teste não mostra as imagens.
14 - Test.reset_answers() unused. 12 - Test.reset_answers() unused.
15 -- incluir test_id na tabela questions (futuro semestre, pode quebrar compatibilidade).  
16 -- na pagina admin, mostrar com cor vermelha as horas de entrada dos alunos que ja tenham excedido o tempo 13 +- teste nao esta a mostrar imagens de vez em quando.???
17 14
18 # TODO 15 # TODO
19 16
  17 +- na pagina admin, mostrar com cor vermelha as horas de entrada dos alunos que ja tenham excedido o tempo
  18 +- retornar None quando nao ha alteracoes relativamente à última vez.
  19 +ou usar push (websockets?)
20 - mudar ref do test para test_id (ref já é usado nas perguntas) 20 - mudar ref do test para test_id (ref já é usado nas perguntas)
21 - servidor ntpd no x220 para configurar a data/hora dos portateis dell 21 - servidor ntpd no x220 para configurar a data/hora dos portateis dell
22 - autorização dada, mas teste não disponível até que seja dada ordem para começar. 22 - autorização dada, mas teste não disponível até que seja dada ordem para começar.
demo/demo.yaml
1 --- 1 ---
2 # ============================================================================ 2 # ============================================================================
3 # Unique identifier of the test. 3 # Unique identifier of the test.
  4 +# Valid names can only include letters, digits, dash and underscore,
  5 +# e.g. asc1-test3
4 # Database queries can be done in the terminal with 6 # Database queries can be done in the terminal with
5 -# sqlite3 students.db "select * from tests where ref='tutorial'" 7 +# sqlite3 students.db "select * from tests where ref='asc1-test3'"
6 ref: tutorial 8 ref: tutorial
7 9
8 # Database file that includes student credentials, tests and questions grades. 10 # Database file that includes student credentials, tests and questions grades.
@@ -20,9 +22,9 @@ title: Teste de demonstração (tutorial) @@ -20,9 +22,9 @@ title: Teste de demonstração (tutorial)
20 22
21 # Duration in minutes. 23 # Duration in minutes.
22 # (0 or undefined means infinite time) 24 # (0 or undefined means infinite time)
23 -duration: 2 25 +duration: 0
24 26
25 -# Automatic test submission after the timeout 'duration'? 27 +# Automatic test submission after the given 'duration' timeout
26 # (default: false) 28 # (default: false)
27 autosubmit: true 29 autosubmit: true
28 30
@@ -72,7 +74,7 @@ questions: @@ -72,7 +74,7 @@ questions:
72 - tut-warning 74 - tut-warning
73 - [tut-alert1, tut-alert2] 75 - [tut-alert1, tut-alert2]
74 - tut-generator 76 - tut-generator
75 - 77 + - tut-yamllint
76 78
77 # test: 79 # test:
78 # - ref1 80 # - ref1
demo/questions/generators/generate-question.py
@@ -18,11 +18,10 @@ print(f&quot;&quot;&quot;--- @@ -18,11 +18,10 @@ print(f&quot;&quot;&quot;---
18 type: text 18 type: text
19 title: Geradores de perguntas 19 title: Geradores de perguntas
20 text: | 20 text: |
21 - Existe a possibilidade da pergunta ser gerada por um programa externo. Este  
22 - programa deve escrever no `stdout` uma pergunta em formato `yaml` como nos 21 + Existe a possibilidade da pergunta ser gerada por um programa externo. O
  22 + programa deve escrever no `stdout` uma pergunta em formato `yaml` tal como os
23 exemplos anteriores. Pode também receber argumentos para parametrizar a 23 exemplos anteriores. Pode também receber argumentos para parametrizar a
24 - geração da pergunta. Aqui está um exemplo de uma pergunta gerada por um  
25 - script python: 24 + pergunta. Aqui está um exemplo de uma pergunta gerada por um script python:
26 25
27 ```python 26 ```python
28 #!/usr/bin/env python3 27 #!/usr/bin/env python3
@@ -46,9 +45,7 @@ text: | @@ -46,9 +45,7 @@ text: |
46 A solução é {{r}}.''') 45 A solução é {{r}}.''')
47 ``` 46 ```
48 47
49 - Este script deve ter permissões para poder ser executado no terminal. Dá  
50 - jeito usar o comando `gen-somar.py 1 100 | yamllint -` para validar o `yaml`  
51 - gerado. 48 + Este script deve ter permissões para poder ser executado no terminal.
52 49
53 Para indicar que uma pergunta é gerada externamente, esta é declarada com 50 Para indicar que uma pergunta é gerada externamente, esta é declarada com
54 51
@@ -56,12 +53,12 @@ text: | @@ -56,12 +53,12 @@ text: |
56 - type: generator 53 - type: generator
57 ref: gen-somar 54 ref: gen-somar
58 script: gen-somar.py 55 script: gen-somar.py
59 - # opcional 56 + # argumentos opcionais
60 args: [1, 100] 57 args: [1, 100]
61 ``` 58 ```
62 59
63 - Os argumentos `args` são opcionais e são passados para o programa como  
64 - argumentos da linha de comando. 60 + Opcionalmente, o programa pode receber uma lista de argumentos declarados em
  61 + `args`.
65 62
66 --- 63 ---
67 64
demo/questions/questions-tutorial.yaml
@@ -581,3 +581,25 @@ @@ -581,3 +581,25 @@
581 ref: tut-generator 581 ref: tut-generator
582 script: generators/generate-question.py 582 script: generators/generate-question.py
583 args: [1, 100] 583 args: [1, 100]
  584 +
  585 +# ----------------------------------------------------------------------------
  586 +- type: information
  587 + ref: tut-yamllint
  588 + title: Sugestões para validar yaml
  589 + text: |
  590 + Como os testes e perguntas são ficheiros `yaml`, é conveniente validar se
  591 + estão correctamente definitos. Um *linter* recomendado é o `yamllint`. Pode
  592 + ser instalado com `pip install yamllint` e usado do seguinte modo:
  593 +
  594 + ```sh
  595 + yamllint test.yaml
  596 + yamllint questions.yaml
  597 + ```
  598 +
  599 + No caso de programas geradores de perguntas e programas de correcção de
  600 + respostas pode usar-se um *pipe*:
  601 +
  602 + ```sh
  603 + generate-question | yamllint -
  604 + correct-answer | yamllint -
  605 + ```
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.05.dev2' 35 +APP_VERSION = '2020.05.dev3'
36 APP_DESCRIPTION = __doc__ 36 APP_DESCRIPTION = __doc__
37 37
38 __author__ = 'Miguel Barão' 38 __author__ = 'Miguel Barão'
perguntations/app.py
@@ -276,11 +276,43 @@ class App(): @@ -276,11 +276,43 @@ class App():
276 uid, area, win_x, win_y, scr_x, scr_y) 276 uid, area, win_x, win_y, scr_x, scr_y)
277 277
278 # ------------------------------------------------------------------------ 278 # ------------------------------------------------------------------------
  279 + # --- GETTERS
  280 + # ------------------------------------------------------------------------
279 281
280 - # --- helpers (getters)  
281 # def get_student_name(self, uid): 282 # def get_student_name(self, uid):
282 # return self.online[uid]['student']['name'] 283 # return self.online[uid]['student']['name']
283 284
  285 + def get_questions_csv(self):
  286 + '''generates a CSV with the grades of the test'''
  287 + test_id = self.testfactory['ref']
  288 +
  289 + with self.db_session() as sess:
  290 + grades = sess.query(Question.student_id, Question.starttime,
  291 + Question.ref, Question.grade)\
  292 + .filter(Question.test_id == test_id)\
  293 + .order_by(Question.student_id)\
  294 + .all()
  295 +
  296 + cols = ['Aluno', 'Início'] + \
  297 + [r for question in self.testfactory['questions']
  298 + for r in question['ref']]
  299 +
  300 + tests = {}
  301 + for q in grades:
  302 + student, qref, qgrade = q[:2], q[2], q[3]
  303 + tests.setdefault(student, {})[qref] = qgrade
  304 +
  305 + rows = [{'Aluno': test[0], 'Início': test[1], **q}
  306 + for test, q in tests.items()]
  307 +
  308 + csvstr = io.StringIO()
  309 + writer = csv.DictWriter(csvstr, fieldnames=cols, restval=None,
  310 + delimiter=';', quoting=csv.QUOTE_ALL)
  311 + writer.writeheader()
  312 + writer.writerows(rows)
  313 + return test_id, csvstr.getvalue()
  314 +
  315 +
284 def get_test_csv(self): 316 def get_test_csv(self):
285 '''generates a CSV with the grades of the test''' 317 '''generates a CSV with the grades of the test'''
286 with self.db_session() as sess: 318 with self.db_session() as sess:
@@ -292,7 +324,7 @@ class App(): @@ -292,7 +324,7 @@ class App():
292 324
293 csvstr = io.StringIO() 325 csvstr = io.StringIO()
294 writer = csv.writer(csvstr, delimiter=';', quoting=csv.QUOTE_ALL) 326 writer = csv.writer(csvstr, delimiter=';', quoting=csv.QUOTE_ALL)
295 - writer.writerow(('Número', 'Nota', 'Início', 'Fim')) 327 + writer.writerow(('Aluno', 'Nota', 'Início', 'Fim'))
296 writer.writerows(grades) 328 writer.writerows(grades)
297 return self.testfactory['ref'], csvstr.getvalue() 329 return self.testfactory['ref'], csvstr.getvalue()
298 330
@@ -357,7 +389,10 @@ class App(): @@ -357,7 +389,10 @@ class App():
357 # if q['ref'] == ref and key in q['files']: 389 # if q['ref'] == ref and key in q['files']:
358 # return path.abspath(path.join(q['path'], q['files'][key])) 390 # return path.abspath(path.join(q['path'], q['files'][key]))
359 391
360 - # --- helpers (change state) 392 + # ------------------------------------------------------------------------
  393 + # --- SETTERS
  394 + # ------------------------------------------------------------------------
  395 +
361 def allow_student(self, uid): 396 def allow_student(self, uid):
362 '''allow a single student to login''' 397 '''allow a single student to login'''
363 self.allowed.add(uid) 398 self.allowed.add(uid)
perguntations/models.py
1 ''' 1 '''
2 -Database tables 2 +SQLAlchemy ORM
  3 +
  4 +The classes below correspond to database tables
3 ''' 5 '''
4 6
5 7
@@ -26,10 +28,10 @@ class Student(Base): @@ -26,10 +28,10 @@ class Student(Base):
26 questions = relationship('Question', back_populates='student') 28 questions = relationship('Question', back_populates='student')
27 29
28 def __repr__(self): 30 def __repr__(self):
29 - return f'Student:\n\  
30 - id: "{self.id}"\n\  
31 - name: "{self.name}"\n\  
32 - password: "{self.password}"' 31 + return (f'Student:\n'
  32 + f' id: "{self.id}"\n'
  33 + f' name: "{self.name}"\n'
  34 + f' password: "{self.password}"\n')
33 35
34 36
35 # ---------------------------------------------------------------------------- 37 # ----------------------------------------------------------------------------
@@ -38,7 +40,7 @@ class Test(Base): @@ -38,7 +40,7 @@ class Test(Base):
38 __tablename__ = 'tests' 40 __tablename__ = 'tests'
39 id = Column(Integer, primary_key=True) # auto_increment 41 id = Column(Integer, primary_key=True) # auto_increment
40 ref = Column(String) 42 ref = Column(String)
41 - title = Column(String) # FIXME depends on ref and should come from another table... 43 + title = Column(String)
42 grade = Column(Float) 44 grade = Column(Float)
43 state = Column(String) # ACTIVE, FINISHED, QUIT, NULL 45 state = Column(String) # ACTIVE, FINISHED, QUIT, NULL
44 comment = Column(String) 46 comment = Column(String)
@@ -52,17 +54,17 @@ class Test(Base): @@ -52,17 +54,17 @@ class Test(Base):
52 questions = relationship('Question', back_populates='test') 54 questions = relationship('Question', back_populates='test')
53 55
54 def __repr__(self): 56 def __repr__(self):
55 - return f'Test:\n\  
56 - id: "{self.id}"\n\  
57 - ref: "{self.ref}"\n\  
58 - title: "{self.title}"\n\  
59 - grade: "{self.grade}"\n\  
60 - state: "{self.state}"\n\  
61 - comment: "{self.comment}"\n\  
62 - starttime: "{self.starttime}"\n\  
63 - finishtime: "{self.finishtime}"\n\  
64 - filename: "{self.filename}"\n\  
65 - student_id: "{self.student_id}"\n' 57 + return (f'Test:\n'
  58 + f' id: "{self.id}"\n'
  59 + f' ref: "{self.ref}"\n'
  60 + f' title: "{self.title}"\n'
  61 + f' grade: "{self.grade}"\n'
  62 + f' state: "{self.state}"\n'
  63 + f' comment: "{self.comment}"\n'
  64 + f' starttime: "{self.starttime}"\n'
  65 + f' finishtime: "{self.finishtime}"\n'
  66 + f' filename: "{self.filename}"\n'
  67 + f' student_id: "{self.student_id}"\n')
66 68
67 69
68 # --------------------------------------------------------------------------- 70 # ---------------------------------------------------------------------------
@@ -82,11 +84,11 @@ class Question(Base): @@ -82,11 +84,11 @@ class Question(Base):
82 test = relationship('Test', back_populates='questions') 84 test = relationship('Test', back_populates='questions')
83 85
84 def __repr__(self): 86 def __repr__(self):
85 - return f'Question:\n\  
86 - id: "{self.id}"\n\  
87 - ref: "{self.ref}"\n\  
88 - grade: "{self.grade}"\n\  
89 - starttime: "{self.starttime}"\n\  
90 - finishtime: "{self.finishtime}"\n\  
91 - student_id: "{self.student_id}"\n\  
92 - test_id: "{self.test_id}"\n' 87 + return (f'Question:\n'
  88 + f' id: "{self.id}"\n'
  89 + f' ref: "{self.ref}"\n'
  90 + f' grade: "{self.grade}"\n'
  91 + f' starttime: "{self.starttime}"\n'
  92 + f' finishtime: "{self.finishtime}"\n'
  93 + f' student_id: "{self.student_id}"\n' # FIXME normal form
  94 + f' test_id: "{self.test_id}"\n')
perguntations/serve.py
@@ -63,7 +63,8 @@ class WebApplication(tornado.web.Application): @@ -63,7 +63,8 @@ class WebApplication(tornado.web.Application):
63 # ---------------------------------------------------------------------------- 63 # ----------------------------------------------------------------------------
64 def admin_only(func): 64 def admin_only(func):
65 ''' 65 '''
66 - Decorator used to restrict access to the administrator. For example: 66 + Decorator used to restrict access to the administrator.
  67 + Example:
67 68
68 @admin_only() 69 @admin_only()
69 def get(self): ... 70 def get(self): ...
@@ -91,7 +92,7 @@ class BaseHandler(tornado.web.RequestHandler): @@ -91,7 +92,7 @@ class BaseHandler(tornado.web.RequestHandler):
91 92
92 def get_current_user(self): 93 def get_current_user(self):
93 ''' 94 '''
94 - HTML is stateless, so a cookie is used to identify the user. 95 + Since HTTP is stateless, a cookie is used to identify the user.
95 This function returns the cookie for the current user. 96 This function returns the cookie for the current user.
96 ''' 97 '''
97 cookie = self.get_secure_cookie('user') 98 cookie = self.get_secure_cookie('user')
@@ -191,6 +192,15 @@ class AdminWebservice(BaseHandler): @@ -191,6 +192,15 @@ class AdminWebservice(BaseHandler):
191 self.write(data) 192 self.write(data)
192 await self.flush() 193 await self.flush()
193 194
  195 + if cmd == 'questionscsv':
  196 + test_ref, data = self.testapp.get_questions_csv()
  197 + self.set_header('Content-Type', 'text/csv')
  198 + self.set_header('content-Disposition',
  199 + f'attachment; filename={test_ref}-detailed.csv')
  200 + self.write(data)
  201 + await self.flush()
  202 +
  203 +
194 # ---------------------------------------------------------------------------- 204 # ----------------------------------------------------------------------------
195 class AdminHandler(BaseHandler): 205 class AdminHandler(BaseHandler):
196 '''Handle /admin''' 206 '''Handle /admin'''
@@ -236,7 +246,7 @@ class AdminHandler(BaseHandler): @@ -236,7 +246,7 @@ class AdminHandler(BaseHandler):
236 self.testapp.deny_student(value) 246 self.testapp.deny_student(value)
237 247
238 elif cmd == 'reset_password': 248 elif cmd == 'reset_password':
239 - await self.testapp.update_student_password(uid=value, pw='') 249 + await self.testapp.update_student_password(uid=value, password='')
240 250
241 elif cmd == 'insert_student': 251 elif cmd == 'insert_student':
242 student = json.loads(value) 252 student = json.loads(value)
@@ -244,7 +254,7 @@ class AdminHandler(BaseHandler): @@ -244,7 +254,7 @@ class AdminHandler(BaseHandler):
244 name=student['name']) 254 name=student['name'])
245 255
246 else: 256 else:
247 - logging.error(f'Unknown command: "{cmd}"') 257 + logging.error('Unknown command: "%s"', cmd)
248 258
249 259
250 # ---------------------------------------------------------------------------- 260 # ----------------------------------------------------------------------------
@@ -337,11 +347,11 @@ class FileHandler(BaseHandler): @@ -337,11 +347,11 @@ class FileHandler(BaseHandler):
337 try: 347 try:
338 file = open(filepath, 'rb') 348 file = open(filepath, 'rb')
339 except FileNotFoundError: 349 except FileNotFoundError:
340 - logging.error(f'File not found: {filepath}') 350 + logging.error('File not found: %s', filepath)
341 except PermissionError: 351 except PermissionError:
342 - logging.error(f'No permission: {filepath}') 352 + logging.error('No permission: %s', filepath)
343 except OSError: 353 except OSError:
344 - logging.error(f'Error opening file: {filepath}') 354 + logging.error('Error opening file: %s', filepath)
345 else: 355 else:
346 data = file.read() 356 data = file.read()
347 file.close() 357 file.close()
@@ -466,21 +476,25 @@ class ReviewHandler(BaseHandler): @@ -466,21 +476,25 @@ class ReviewHandler(BaseHandler):
466 Opens JSON file with a given corrected test and renders it 476 Opens JSON file with a given corrected test and renders it
467 ''' 477 '''
468 test_id = self.get_query_argument('test_id', None) 478 test_id = self.get_query_argument('test_id', None)
469 - logging.info(f'Review test {test_id}.') 479 + logging.info('Review test %s.', test_id)
470 fname = self.testapp.get_json_filename_of_test(test_id) 480 fname = self.testapp.get_json_filename_of_test(test_id)
471 481
472 if fname is None: 482 if fname is None:
473 raise tornado.web.HTTPError(404) # Not Found 483 raise tornado.web.HTTPError(404) # Not Found
474 484
475 try: 485 try:
476 - jsonfile = open(path.expanduser(fname))  
477 - except OSError:  
478 - logging.error(f'Cannot open "{fname}" for review.')  
479 - else:  
480 - with jsonfile: 486 + with open(path.expanduser(fname)) as jsonfile:
481 test = json.load(jsonfile) 487 test = json.load(jsonfile)
482 - self.render('review.html', t=test, md=md_to_html,  
483 - templ=self._templates) 488 + except OSError:
  489 + logging.error('Cannot open "%s" for review.', fname)
  490 + raise tornado.web.HTTPError(404) # Not Found
  491 + except json.JSONDecodeError as exc:
  492 + logging.error('JSON error in "%s": %s', fname, exc)
  493 + raise tornado.web.HTTPError(404) # Not Found
  494 +
  495 + print(test['show_ref'])
  496 + self.render('review.html', t=test, md=md_to_html,
  497 + templ=self._templates)
484 498
485 499
486 # ---------------------------------------------------------------------------- 500 # ----------------------------------------------------------------------------
@@ -517,10 +531,10 @@ def run_webserver(app, ssl_opt, port, debug): @@ -517,10 +531,10 @@ def run_webserver(app, ssl_opt, port, debug):
517 try: 531 try:
518 httpserver.listen(port) 532 httpserver.listen(port)
519 except OSError: 533 except OSError:
520 - logging.critical(f'Cannot bind port {port}. Already in use?') 534 + logging.critical('Cannot bind port %d. Already in use?', port)
521 sys.exit(1) 535 sys.exit(1)
522 536
523 - logging.info(f'Webserver listening on {port}... (Ctrl-C to stop)') 537 + logging.info('Webserver listening on %d... (Ctrl-C to stop)', port)
524 signal.signal(signal.SIGINT, signal_handler) 538 signal.signal(signal.SIGINT, signal_handler)
525 539
526 # --- run webserver 540 # --- run webserver
perguntations/templates/admin.html
@@ -76,7 +76,8 @@ @@ -76,7 +76,8 @@
76 Base de dados: <code id="database">--</code><br> 76 Base de dados: <code id="database">--</code><br>
77 </p> 77 </p>
78 <p> 78 <p>
79 - <a href="/adminwebservice?cmd=testcsv" class="btn btn-primary">Obter CSV com as notas</a> 79 + <a href="/adminwebservice?cmd=testcsv" class="btn btn-primary">Obter CSV das notas</a>
  80 + <a href="/adminwebservice?cmd=questionscsv" class="btn btn-primary">Obter CSV detalhado</a>
80 </p> 81 </p>
81 </div> <!-- jumbotron --> 82 </div> <!-- jumbotron -->
82 83
perguntations/templates/question-information.html
@@ -19,8 +19,7 @@ @@ -19,8 +19,7 @@
19 19
20 {% if show_ref %} 20 {% if show_ref %}
21 <hr> 21 <hr>
22 - path: <code>{{ q['path'] }}</code><br>  
23 - file: <code>{{ q['filename'] }}</code><br> 22 + file: <code>{{ q['path'] }}/{{ q['filename'] }}</code><br>
24 ref: <code>{{ q['ref'] }}</code> 23 ref: <code>{{ q['ref'] }}</code>
25 {% end %} 24 {% end %}
26 </div> 25 </div>
27 \ No newline at end of file 26 \ No newline at end of file
perguntations/templates/question.html
@@ -31,8 +31,7 @@ @@ -31,8 +31,7 @@
31 31
32 {% if show_ref %} 32 {% if show_ref %}
33 <div class="card-footer"> 33 <div class="card-footer">
34 - path: <code>{{ q['path'] }}</code><br>  
35 - file: <code>{{ q['filename'] }}</code><br> 34 + file: <code>{{ q['path'] }}/{{ q['filename'] }}</code><br>
36 ref: <code>{{ q['ref'] }}</code> 35 ref: <code>{{ q['ref'] }}</code>
37 </div> 36 </div>
38 {% end %} 37 {% end %}
perguntations/templates/review-question-information.html
@@ -16,4 +16,9 @@ @@ -16,4 +16,9 @@
16 <div id="text"> 16 <div id="text">
17 {{ md(q['text']) }} 17 {{ md(q['text']) }}
18 </div> 18 </div>
  19 + {% if t['show_ref'] %}
  20 + <hr>
  21 + file: <code>{{ q['path'] }}/{{ q['filename'] }}</code><br>
  22 + ref: <code>{{ q['ref'] }}</code>
  23 + {% end %}
19 </div> 24 </div>
20 \ No newline at end of file 25 \ No newline at end of file
perguntations/templates/review-question.html
@@ -65,7 +65,9 @@ @@ -65,7 +65,9 @@
65 {% end %} 65 {% end %}
66 66
67 {% if t['show_ref'] %} 67 {% if t['show_ref'] %}
68 - <code>{{q['ref']}}</code> 68 + <hr>
  69 + file: <code>{{ q['path'] }}/{{ q['filename'] }}</code><br>
  70 + ref: <code>{{ q['ref'] }}</code>
69 {% end %} 71 {% end %}
70 72
71 </div> <!-- card-footer --> 73 </div> <!-- card-footer -->
@@ -107,7 +109,9 @@ @@ -107,7 +109,9 @@
107 </p> 109 </p>
108 110
109 {% if t['show_ref'] %} 111 {% if t['show_ref'] %}
110 - <code>{{ q['ref'] }}</code> 112 + <hr>
  113 + file: <code>{{ q['path'] }}/{{ q['filename'] }}</code><br>
  114 + ref: <code>{{ q['ref'] }}</code>
111 {% end %} 115 {% end %}
112 116
113 </div> <!-- card-footer --> 117 </div> <!-- card-footer -->