Commit 5bad78cde6e7a158a3d6ccad34d472d008109c7b

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

highlights:

- mathjax update to version 3 improves performance.
- fix solution rendering in the review.
- minor text changes in the demo.
- minor version updates in javascript libraries.
BUGS.md
1 1  
2 2 # BUGS
3 3  
4   -- nao esta a usar points das perguntas
  4 +- 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>.
5 5 - teste nao esta a mostrar imagens.
6 6 - se houver erros a abrir ficheiros .yaml de perguntas, depois dos testes diz "No errors found".
7 7 - dizer quanto desconta em cada pergunta de escolha multipla
... ... @@ -19,6 +19,7 @@ ou usar push (websockets?)
19 19  
20 20 # TODO
21 21  
  22 +- nao esta a usar points das perguntas
22 23 - test: mostrar duração do teste com progressbar no navbar.
23 24 - submissao fazer um post ajax?
24 25 - adicionar opcao para eliminar um teste em curso.
... ...
demo/questions/questions-tutorial.yaml
... ... @@ -330,9 +330,10 @@
330 330 podem ser úteis por exemplo para introduzir código.
331 331  
332 332 A resposta é enviada para um programa externo para ser avaliada.
333   - O programa externo é um programa qualquer executável pelo sistema.
334   - Este recebe a resposta submetida pelo aluno via `stdin` e devolve a
335   - classificação via `stdout`.
  333 + O programa externo é um programa escrito numa linguagem qualquer, desde que
  334 + seja executável pelo sistema operativo (pode ser um script ou binário).
  335 + Este programa recebe a resposta submetida pelo aluno via `stdin` e devolve
  336 + a classificação via `stdout`.
336 337 Exemplo:
337 338  
338 339 ```yaml
... ... @@ -348,12 +349,13 @@
348 349 Neste exemplo, o programa de avaliação é um script python que verifica se a
349 350 resposta contém as três palavras red, green e blue, e calcula uma nota no
350 351 intervalo 0.0 a 1.0.
351   - O programa externo é um programa executável no sistema, escrito em
352   - qualquer linguagem de programação. A interacção com o servidor faz-se
  352 + O programa externo é executado num processo separado do sistema operativo.
  353 + Pode escrito em qualquer linguagem de programação, desde que . A interacção com o servidor faz-se
353 354 sempre via stdin/stdout.
354 355  
355 356 Se o programa externo exceder o `timeout` indicado (em segundos),
356   - é automaticamente cancelado e é atribuída a classificação de 0.0 valores.
  357 + este é automaticamente terminado e é atribuída a classificação de 0.0
  358 + valores na pergunta.
357 359  
358 360 Após terminar a correcção, o programa externo deve enviar a classificação
359 361 para o stdout.
... ... @@ -377,8 +379,9 @@
377 379  
378 380 O comentário é mostrado na revisão de prova.
379 381 answer: |
  382 + Aqui o aluno escreve a resposta.
380 383 Esta caixa aumenta de tamanho automaticamente e
381   - pode estar previamente preenchida (use answer: texto).
  384 + pode estar previamente preenchida como neste caso (use `answer: texto`).
382 385 correct: correct/correct-question.py
383 386 timeout: 5
384 387  
... ... @@ -423,7 +426,7 @@
423 426 text: |
424 427 Também não conta para avaliação. É apenas o aspecto gráfico que muda.
425 428  
426   - Além das fórmulas LaTeX, também se pode escrever troços de código:
  429 + Além das fórmulas LaTeX, também se podem escrever troços de código:
427 430  
428 431 ```C
429 432 int main() {
... ...
package-lock.json
... ... @@ -3,9 +3,9 @@
3 3 "lockfileVersion": 1,
4 4 "dependencies": {
5 5 "@fortawesome/fontawesome-free": {
6   - "version": "5.8.1",
7   - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.8.1.tgz",
8   - "integrity": "sha512-GJtx6e55qLEOy2gPOsok2lohjpdWNGrYGtQx0FFT/++K4SYx+Z8LlPHdQBaFzKEwH5IbBB4fNgb//uyZjgYXoA=="
  6 + "version": "5.11.1",
  7 + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.11.1.tgz",
  8 + "integrity": "sha512-DtXLVYAkDU0ce1cFUgLvZaMd1R2J/LviBYih9xr4ZLhQMrgvYX7w2vOxlpKLRALfIj5GyC5zoVrcACOkLcFgvg=="
9 9 },
10 10 "bootstrap": {
11 11 "version": "4.3.1",
... ... @@ -13,9 +13,14 @@
13 13 "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag=="
14 14 },
15 15 "codemirror": {
16   - "version": "5.45.0",
17   - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.45.0.tgz",
18   - "integrity": "sha512-c19j644usCE8gQaXa0jqn2B/HN9MnB2u6qPIrrhrMkB+QAP42y8G4QnTwuwbVSoUS1jEl7JU9HZMGhCDL0nsAw=="
  16 + "version": "5.48.4",
  17 + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.48.4.tgz",
  18 + "integrity": "sha512-pUhZXDQ6qXSpWdwlgAwHEkd4imA0kf83hINmUEzJpmG80T/XLtDDEzZo8f6PQLuRCcUQhmzqqIo3ZPTRaWByRA=="
  19 + },
  20 + "commander": {
  21 + "version": "3.0.1",
  22 + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.1.tgz",
  23 + "integrity": "sha512-UNgvDd+csKdc9GD4zjtkHKQbT8Aspt2jCBqNSPp53vAS0L1tS9sXB2TCEOPHJ7kt9bN/niWkYj8T3RQSoMXdSQ=="
19 24 },
20 25 "datatables": {
21 26 "version": "1.10.18",
... ... @@ -25,20 +30,62 @@
25 30 "jquery": ">=1.7"
26 31 }
27 32 },
  33 + "esm": {
  34 + "version": "3.2.25",
  35 + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
  36 + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
  37 + },
28 38 "jquery": {
29   - "version": "3.3.1",
30   - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
31   - "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="
  39 + "version": "3.4.1",
  40 + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
  41 + "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
32 42 },
33 43 "mathjax": {
34   - "version": "2.7.5",
35   - "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-2.7.5.tgz",
36   - "integrity": "sha512-OzsJNitEHAJB3y4IIlPCAvS0yoXwYjlo2Y4kmm9KQzyIBZt2d8yKRalby3uTRNN4fZQiGL2iMXjpdP1u2Rq2DQ=="
  44 + "version": "3.0.0",
  45 + "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-3.0.0.tgz",
  46 + "integrity": "sha512-z4uLbDHNbs/aRuR6zCcnzwFQuMixkHCcWqgVaommfK/3cA1Ahq7OXemn+m8JwTYcBApSHgcrSbPr9sm3sZFL+A==",
  47 + "requires": {
  48 + "mathjax-full": "git://github.com/mathjax/MathJax-src.git"
  49 + }
  50 + },
  51 + "mathjax-full": {
  52 + "version": "git://github.com/mathjax/MathJax-src.git#0d74266e1820220d33cb6b29d4ca3575b352ac0d",
  53 + "from": "git://github.com/mathjax/MathJax-src.git",
  54 + "requires": {
  55 + "esm": "^3.2.25",
  56 + "mj-context-menu": "^0.2.0",
  57 + "speech-rule-engine": "^3.0.0-beta.6"
  58 + }
  59 + },
  60 + "mj-context-menu": {
  61 + "version": "0.2.0",
  62 + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.2.0.tgz",
  63 + "integrity": "sha512-yJxrWBHCjFZEHsZgfs7m5g9OSCNzsVYadW6f6lX3pgZL67vmodtSW/4zhsYmuDKweXfHs0M1kJge1uQIasWA+g=="
37 64 },
38 65 "popper.js": {
39 66 "version": "1.15.0",
40 67 "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.15.0.tgz",
41 68 "integrity": "sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA=="
  69 + },
  70 + "speech-rule-engine": {
  71 + "version": "3.0.0-beta.6",
  72 + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.0.0-beta.6.tgz",
  73 + "integrity": "sha512-B7gcT53jAsKpx7WvFYQcyUlFmgS3Wa9KlDy0FY8SOTa+Wz5EqmI0MpCD5/fYm8/2qiCPp8HwZg+H3cBgM+sNVw==",
  74 + "requires": {
  75 + "commander": "*",
  76 + "wicked-good-xpath": "*",
  77 + "xmldom-sre": "^0.1.31"
  78 + }
  79 + },
  80 + "wicked-good-xpath": {
  81 + "version": "1.3.0",
  82 + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz",
  83 + "integrity": "sha1-gbDpXoZQ5JyUsiKY//hoa1VTz2w="
  84 + },
  85 + "xmldom-sre": {
  86 + "version": "0.1.31",
  87 + "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz",
  88 + "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw=="
42 89 }
43 90 }
44 91 }
... ...
package.json
... ... @@ -2,12 +2,12 @@
2 2 "description": "Javascript libraries required to run the server",
3 3 "email": "mjsb@uevora.pt",
4 4 "dependencies": {
5   - "@fortawesome/fontawesome-free": "^5.8.1",
6   - "bootstrap": "^4.3.1",
7   - "codemirror": "^5.45.0",
8   - "datatables": "^1.10.18",
9   - "jquery": "^3.3.1",
10   - "mathjax": "^2.7.5",
11   - "popper.js": "^1.15.0"
  5 + "@fortawesome/fontawesome-free": "^5.11.1",
  6 + "bootstrap": "^4.3",
  7 + "codemirror": "^5.48",
  8 + "datatables": "^1.10",
  9 + "jquery": "^3.4.1",
  10 + "mathjax": "^3",
  11 + "popper.js": "^1.15"
12 12 }
13 13 }
... ...
perguntations/__init__.py
... ... @@ -32,7 +32,7 @@ proof of submission and for review.
32 32 '''
33 33  
34 34 APP_NAME = 'perguntations'
35   -APP_VERSION = '2019.06.dev1'
  35 +APP_VERSION = '2019.09.dev1'
36 36 APP_DESCRIPTION = __doc__
37 37  
38 38 __author__ = 'Miguel Barão'
... ...
perguntations/serve.py
... ... @@ -17,6 +17,7 @@ import ssl
17 17 # user installed libraries
18 18 import tornado.ioloop
19 19 import tornado.web
  20 +# import tornado.websocket
20 21 import tornado.httpserver
21 22  
22 23 # this project
... ... @@ -27,19 +28,6 @@ from perguntations.parser_markdown import md_to_html
27 28  
28 29  
29 30 # ----------------------------------------------------------------------------
30   -# Decorator used to restrict access to the administrator
31   -# ----------------------------------------------------------------------------
32   -def admin_only(func):
33   - @functools.wraps(func)
34   - async def wrapper(self, *args, **kwargs):
35   - if self.current_user != '0':
36   - raise tornado.web.HTTPError(403) # forbidden
37   - else:
38   - await func(self, *args, **kwargs)
39   - return wrapper
40   -
41   -
42   -# ----------------------------------------------------------------------------
43 31 # Web Application. Routes to handler classes.
44 32 # ----------------------------------------------------------------------------
45 33 class WebApplication(tornado.web.Application):
... ... @@ -51,7 +39,8 @@ class WebApplication(tornado.web.Application):
51 39 (r'/review', ReviewHandler),
52 40 (r'/admin', AdminHandler),
53 41 (r'/file', FileHandler),
54   - # (r'/ws', AdminWebSocketHandler),
  42 + # (r'/root', MainHandler), # FIXME
  43 + # (r'/ws', AdminSocketHandler),
55 44 (r'/', RootHandler), # TODO multiple tests
56 45 ]
57 46  
... ... @@ -69,6 +58,19 @@ class WebApplication(tornado.web.Application):
69 58  
70 59  
71 60 # ----------------------------------------------------------------------------
  61 +# Decorator used to restrict access to the administrator
  62 +# ----------------------------------------------------------------------------
  63 +def admin_only(func):
  64 + @functools.wraps(func)
  65 + async def wrapper(self, *args, **kwargs):
  66 + if self.current_user != '0':
  67 + raise tornado.web.HTTPError(403) # forbidden
  68 + else:
  69 + await func(self, *args, **kwargs)
  70 + return wrapper
  71 +
  72 +
  73 +# ----------------------------------------------------------------------------
72 74 # Base handler. Other handlers will inherit this one.
73 75 # ----------------------------------------------------------------------------
74 76 class BaseHandler(tornado.web.RequestHandler):
... ... @@ -83,6 +85,113 @@ class BaseHandler(tornado.web.RequestHandler):
83 85  
84 86  
85 87 # ----------------------------------------------------------------------------
  88 +# class MainHandler(BaseHandler):
  89 +
  90 +# @tornado.web.authenticated
  91 +# @admin_only
  92 +# def get(self):
  93 +# self.render("admin-ws.html", students=self.testapp.get_students_state())
  94 +
  95 +
  96 +# # ----------------------------------------------------------------------------
  97 +# class AdminSocketHandler(tornado.websocket.WebSocketHandler):
  98 +# waiters = set()
  99 +# # cache = []
  100 +
  101 +# # def get_compression_options(self):
  102 +# # return {} # Non-None enables compression with default options.
  103 +
  104 +# # called when opening connection
  105 +# def open(self):
  106 +# logging.debug('[AdminSocketHandler.open]')
  107 +# AdminSocketHandler.waiters.add(self)
  108 +
  109 +# # called when closing connection
  110 +# def on_close(self):
  111 +# logging.debug('[AdminSocketHandler.on_close]')
  112 +# AdminSocketHandler.waiters.remove(self)
  113 +
  114 +# # @classmethod
  115 +# # def update_cache(cls, chat):
  116 +# # logging.debug(f'[AdminSocketHandler.update_cache] "{chat}"')
  117 +# # cls.cache.append(chat)
  118 +
  119 +# # @classmethod
  120 +# # def send_updates(cls, chat):
  121 +# # logging.info("sending message to %d waiters", len(cls.waiters))
  122 +# # for waiter in cls.waiters:
  123 +# # try:
  124 +# # waiter.write_message(chat)
  125 +# # except Exception:
  126 +# # logging.error("Error sending message", exc_info=True)
  127 +
  128 +# # handle incomming messages
  129 +# def on_message(self, message):
  130 +# logging.info(f"[AdminSocketHandler.onmessage] got message {message}")
  131 +# parsed = tornado.escape.json_decode(message)
  132 +# print(parsed)
  133 +# chat = {"id": str(uuid.uuid4()), "body": parsed["body"]}
  134 +# print(chat)
  135 +# chat["html"] = tornado.escape.to_basestring(
  136 +# '<div>' + chat['body'] + '</div>'
  137 +# # self.render_string("message.html", message=chat)
  138 +# )
  139 +# print(chat)
  140 +
  141 +# AdminSocketHandler.update_cache(chat) # store msgs
  142 +# AdminSocketHandler.send_updates(chat) # send to clients
  143 +
  144 +
  145 +# --- ADMIN ------------------------------------------------------------------
  146 +class AdminHandler(BaseHandler):
  147 + SUPPORTED_METHODS = ['GET', 'POST']
  148 +
  149 + @tornado.web.authenticated
  150 + @admin_only
  151 + async def get(self):
  152 + cmd = self.get_query_argument('cmd', default=None)
  153 +
  154 + if cmd == 'students_table':
  155 + data = {'data': self.testapp.get_students_state()}
  156 + self.write(json.dumps(data, default=str))
  157 + elif cmd == 'test': # FIXME which test?
  158 + data = {
  159 + 'data': {
  160 + 'title': self.testapp.testfactory['title'],
  161 + 'ref': self.testapp.testfactory['ref'],
  162 + 'filename': self.testapp.testfactory['filename'],
  163 + 'database': self.testapp.testfactory['database'],
  164 + 'answers_dir': self.testapp.testfactory['answers_dir'],
  165 + }
  166 + }
  167 + self.write(json.dumps(data, default=str))
  168 + else:
  169 + self.render('admin.html')
  170 +
  171 + @tornado.web.authenticated
  172 + @admin_only
  173 + async def post(self):
  174 + cmd = self.get_body_argument('cmd', None)
  175 + value = self.get_body_argument('value', None)
  176 +
  177 + if cmd == 'allow':
  178 + self.testapp.allow_student(value)
  179 +
  180 + elif cmd == 'deny':
  181 + self.testapp.deny_student(value)
  182 +
  183 + elif cmd == 'reset_password':
  184 + await self.testapp.update_student_password(uid=value, pw='')
  185 +
  186 + elif cmd == 'insert_student':
  187 + s = json.loads(value)
  188 + self.testapp.insert_new_student(uid=s['number'], name=s['name'])
  189 +
  190 + else:
  191 + logging.error(f'Unknown command: "{cmd}"')
  192 +
  193 +
  194 +# ----------------------------------------------------------------------------
86 195 # /login
87 196 # ----------------------------------------------------------------------------
88 197 class LoginHandler(BaseHandler):
... ... @@ -281,55 +390,6 @@ class ReviewHandler(BaseHandler):
281 390 templ=self._templates)
282 391  
283 392  
284   -# --- ADMIN ------------------------------------------------------------------
285   -class AdminHandler(BaseHandler):
286   - SUPPORTED_METHODS = ['GET', 'POST']
287   -
288   - @tornado.web.authenticated
289   - @admin_only
290   - async def get(self):
291   - cmd = self.get_query_argument('cmd', default=None)
292   -
293   - if cmd == 'students_table':
294   - data = {'data': self.testapp.get_students_state()}
295   - self.write(json.dumps(data, default=str))
296   - elif cmd == 'test': # FIXME which test?
297   - data = {
298   - 'data': {
299   - 'title': self.testapp.testfactory['title'],
300   - 'ref': self.testapp.testfactory['ref'],
301   - 'filename': self.testapp.testfactory['filename'],
302   - 'database': self.testapp.testfactory['database'],
303   - 'answers_dir': self.testapp.testfactory['answers_dir'],
304   - }
305   - }
306   - self.write(json.dumps(data, default=str))
307   - else:
308   - self.render('admin.html')
309   -
310   - @tornado.web.authenticated
311   - @admin_only
312   - async def post(self):
313   - cmd = self.get_body_argument('cmd', None)
314   - value = self.get_body_argument('value', None)
315   -
316   - if cmd == 'allow':
317   - self.testapp.allow_student(value)
318   -
319   - elif cmd == 'deny':
320   - self.testapp.deny_student(value)
321   -
322   - elif cmd == 'reset_password':
323   - await self.testapp.update_student_password(uid=value, pw='')
324   -
325   - elif cmd == 'insert_student':
326   - s = json.loads(value)
327   - self.testapp.insert_new_student(uid=s['number'], name=s['name'])
328   -
329   - else:
330   - logging.error(f'Unknown command: "{cmd}"')
331   -
332   -
333 393 # ----------------------------------------------------------------------------
334 394 def signal_handler(signal, frame):
335 395 r = input(' --> Stop webserver? (yes/no) ')
... ...
perguntations/templates/review-question.html
... ... @@ -42,7 +42,7 @@
42 42 {{ q['comments'] }}
43 43 {% if 'solution' in q %}
44 44 <hr>
45   - {{ md('**Solução:** ' + q['solution']) }}
  45 + {{ md('**Solução:** \n\n' + q['solution']) }}
46 46 {% end %}
47 47 </p>
48 48 {% else %}
... ... @@ -53,7 +53,7 @@
53 53 {{ q['comments'] }}
54 54 {% if 'solution' in q %}
55 55 <hr>
56   - {{ md('**Solução:** ' + q['solution']) }}
  56 + {{ md('**Solução:** \n\n' + q['solution']) }}
57 57 {% end %}
58 58 </p>
59 59 {% end %}
... ... @@ -96,7 +96,7 @@
96 96 {{ q['comments'] }}
97 97 {% if 'solution' in q %}
98 98 <hr>
99   - {{ md('**Solução:** ' + q['solution']) }}
  99 + {{ md('**Solução:** \n\n' + q['solution']) }}
100 100 {% end %}
101 101 </p>
102 102  
... ...
perguntations/templates/review.html
... ... @@ -6,15 +6,20 @@
6 6 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7 7 <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
8 8  
9   -<!-- MathJax -->
10   - <script defer type="text/x-mathjax-config">
11   - MathJax.Hub.Config({
12   - tex2jax: {
13   - inlineMath: [["$$$","$$$"], ["\\(","\\)"]]
14   - }
15   - });
  9 +<!-- MathJax3 -->
  10 + <script>
  11 + MathJax = {
  12 + tex: {
  13 + inlineMath: [['$$$', '$$$'], ['\\(', '\\)']]
  14 + },
  15 + svg: {
  16 + fontCache: 'global'
  17 + }
  18 + };
  19 + </script>
  20 + <script type="text/javascript" id="MathJax-script" async
  21 + src="/static/mathjax/es5/tex-svg.js">
16 22 </script>
17   - <script defer type="text/javascript" src="/static/mathjax/MathJax.js?config=TeX-AMS_CHTML-full"></script>
18 23  
19 24 <!-- Styles -->
20 25 <link rel="stylesheet" type="text/css" href="/static/bootstrap/css/bootstrap.min.css">
... ... @@ -69,7 +74,7 @@
69 74 <h1 class="display-5">{{ t['title'] }}</h1>
70 75 <h5>
71 76 <div class="row">
72   - <label for="duracao" class="col-sm-2">Duração (min):</label>
  77 + <label for="duracao" class="col-sm-2">Duração (minutos):</label>
73 78 <div class="col-sm-10" id="duracao">{{ t.get('duration', chr(8734)) }}</div>
74 79 </div>
75 80 </h5>
... ...
perguntations/templates/test.html
... ... @@ -6,15 +6,20 @@
6 6 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7 7 <link rel="icon" href="/static/favicon.ico">
8 8  
9   -<!-- MathJax -->
10   - <script type="text/x-mathjax-config">
11   - MathJax.Hub.Config({
12   - tex2jax: {
13   - inlineMath: [["$$$","$$$"], ["\\(","\\)"]]
14   - }
15   - });
  9 +<!-- MathJax3 -->
  10 + <script>
  11 + MathJax = {
  12 + tex: {
  13 + inlineMath: [['$$$', '$$$'], ['\\(', '\\)']]
  14 + },
  15 + svg: {
  16 + fontCache: 'global'
  17 + }
  18 + };
  19 + </script>
  20 + <script type="text/javascript" id="MathJax-script" async
  21 + src="/static/mathjax/es5/tex-svg.js">
16 22 </script>
17   - <script type="text/javascript" src="/static/mathjax/MathJax.js?config=TeX-AMS_CHTML-full"></script>
18 23  
19 24 <!-- Scripts -->
20 25 <script src="/static/jquery/jquery.min.js"></script>
... ... @@ -88,7 +93,7 @@
88 93 </div>
89 94 <div class="row">
90 95 <label for="duracao" class="col-sm-3">Duração:</label>
91   - <div class="col-sm-9" id="duracao">{{ str(t['duration'])+' min.' if t['duration'] > 0 else chr(8734) }}</div>
  96 + <div class="col-sm-9" id="duracao">{{ str(t['duration'])+' minutos' if t['duration'] > 0 else chr(8734) }}</div>
92 97 </div>
93 98 </h5>
94 99 </div>
... ...
setup.py
... ... @@ -18,7 +18,7 @@ setup(
18 18 url="https:USERNAME//bitbucket.org/USERNAME/perguntations.git",
19 19 packages=find_packages(),
20 20 include_package_data=True, # install files from MANIFEST.in
21   - python_requires='>=3.6.*',
  21 + python_requires='>=3.7.*',
22 22 install_requires=[
23 23 'tornado', 'mistune', 'pyyaml', 'pygments', 'sqlalchemy', 'bcrypt'],
24 24 entry_points={
... ...