Commit 6deb4a4fe31ff9b6b386f6524142926730265bd8

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

- file support added. Every file in public subdir of some topic is accessible th…

…rough /file/filename. In the question text images can be referred in markdown as `![alt](image.png "Title")`
- files are served asynchronously in chunks of 1MiB.
@@ -11,7 +11,6 @@ @@ -11,7 +11,6 @@
11 11
12 # TODO 12 # TODO
13 13
14 -- servir imagens/ficheiros.  
15 - session management. close after inactive time. 14 - session management. close after inactive time.
16 - each topic only loads a sample of K questions (max) in random order. 15 - each topic only loads a sample of K questions (max) in random order.
17 - radio e checkboxes, aceitar numeros como seleccao das opcoes. 16 - radio e checkboxes, aceitar numeros como seleccao das opcoes.
@@ -30,6 +29,7 @@ @@ -30,6 +29,7 @@
30 29
31 # FIXED 30 # FIXED
32 31
  32 +- servir imagens/ficheiros.
33 - radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa). 33 - radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa).
34 - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória. 34 - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória.
35 - async/threadpool no bcrypt do initdb. 35 - async/threadpool no bcrypt do initdb.
demo/solar_system/questions.yaml
1 --- 1 ---
2 2
3 -# # ---------------------------------------------------------------------------  
4 -# -  
5 -# ref: solar-system  
6 -# type: radio  
7 -# title: Sistema solar  
8 -# text: Qual é o maior planeta do Sistema Solar?  
9 -# options:  
10 -# - Mercúrio  
11 -# - Marte  
12 -# - Júpiter  
13 -# - Têm todos o mesmo tamanho  
14 -# # opcional  
15 -# correct: 2  
16 -# shuffle: False  
17 -# discount: True 3 +# ---------------------------------------------------------------------------
  4 +-
  5 + ref: solar-system
  6 + type: radio
  7 + title: Sistema solar
  8 + text: |
  9 + ![planetas](planetsa.png " Planetas do Sistema Solar")
  10 +
  11 + Qual é o maior planeta do Sistema Solar?
  12 + options:
  13 + - Mercúrio
  14 + - Marte
  15 + - Júpiter
  16 + - Têm todos o mesmo tamanho
  17 + # opcional
  18 + correct: 2
  19 + shuffle: False
  20 + discount: True
18 21
19 # # --------------------------------------------------------------------------- 22 # # ---------------------------------------------------------------------------
20 # - 23 # -
@@ -30,8 +30,8 @@ class WebApplication(tornado.web.Application): @@ -30,8 +30,8 @@ class WebApplication(tornado.web.Application):
30 (r'/change_password', ChangePasswordHandler), 30 (r'/change_password', ChangePasswordHandler),
31 (r'/question', QuestionHandler), # each question 31 (r'/question', QuestionHandler), # each question
32 (r'/topic/(.+)', TopicHandler), # page for doing a topic 32 (r'/topic/(.+)', TopicHandler), # page for doing a topic
33 - # (r'/file/(.+)', FileHandler), # FIXME  
34 - (r'/.*', RootHandler), # show list of topics 33 + (r'/file/(.+)', FileHandler), # FIXME
  34 + (r'/', RootHandler), # show list of topics
35 ] 35 ]
36 settings = { 36 settings = {
37 'template_path': path.join(path.dirname(__file__), 'templates'), 37 'template_path': path.join(path.dirname(__file__), 'templates'),
@@ -143,20 +143,36 @@ class TopicHandler(BaseHandler): @@ -143,20 +143,36 @@ class TopicHandler(BaseHandler):
143 self.redirect('/') 143 self.redirect('/')
144 144
145 # ---------------------------------------------------------------------------- 145 # ----------------------------------------------------------------------------
146 -# FIXME 146 +# Based on https://bhch.github.io/posts/2017/12/serving-large-files-with-tornado-safely-without-blocking/
147 class FileHandler(BaseHandler): 147 class FileHandler(BaseHandler):
148 @tornado.web.authenticated 148 @tornado.web.authenticated
149 - def get(self, filename): 149 + async def get(self, filename):
150 uid = self.current_user 150 uid = self.current_user
151 public_dir = self.learn.get_current_public_dir(uid) 151 public_dir = self.learn.get_current_public_dir(uid)
152 filepath = path.expanduser(path.join(public_dir, filename)) 152 filepath = path.expanduser(path.join(public_dir, filename))
  153 + chunk_size = 1024 * 1024 # serve 1MiB multiple times
153 try: 154 try:
154 f = open(filepath, 'rb') 155 f = open(filepath, 'rb')
155 except FileNotFoundError: 156 except FileNotFoundError:
156 - raise tornado.web.HTTPError(404) 157 + logging.error(f'File not found: {filepath}')
  158 + except PermissionError:
  159 + logging.error(f'No permission: {filepath}')
157 else: 160 else:
158 - self.write(f.read())  
159 - f.close() 161 + with f:
  162 + while True:
  163 + chunk = f.read(chunk_size)
  164 + if not chunk: break
  165 + try:
  166 + self.write(chunk) # write the cunk to response
  167 + await self.flush() # flush the current chunk to socket
  168 + except iostream.StreamClosedError:
  169 + # client closed the connection
  170 + break
  171 + finally:
  172 + del chunk
  173 + await gen.sleep(0.000000001) # 1 nanosecond (hack)
  174 + # in tornnado 5.0 use `await asyncio.sleep(0)` instead
  175 +
160 176
161 # ---------------------------------------------------------------------------- 177 # ----------------------------------------------------------------------------
162 # respond to AJAX to get a JSON question 178 # respond to AJAX to get a JSON question
@@ -99,19 +99,10 @@ class HighlightRenderer(mistune.Renderer): @@ -99,19 +99,10 @@ class HighlightRenderer(mistune.Renderer):
99 def table(self, header, body): 99 def table(self, header, body):
100 return '<table class="table table-sm"><thead class="thead-light">' + header + '</thead><tbody>' + body + "</tbody></table>" 100 return '<table class="table table-sm"><thead class="thead-light">' + header + '</thead><tbody>' + body + "</tbody></table>"
101 101
102 - # def image(self, src, title, text):  
103 - # if src.startswith('javascript:'):  
104 - # src = ''  
105 - # text = mistune.escape(text, quote=True)  
106 - # if title:  
107 - # title = mistune.escape(title, quote=True)  
108 - # html = '<img class="img-responsive center-block" src="%s" alt="%s" title="%s"' % (src, text, title)  
109 - # else:  
110 - # html = '<img class="img-responsive center-block" src="%s" alt="%s"' % (src, text)  
111 - # if self.options.get('use_xhtml'):  
112 - # return '%s />' % html  
113 - # return '%s>' % html  
114 - 102 + def image(self, src, title, alt):
  103 + alt = mistune.escape(alt, quote=True)
  104 + title = mistune.escape(title or '', quote=True)
  105 + return f'<img src="/file/{src}" class="img-fluid mx-auto d-block" alt="{alt}" title="{title}">'
115 106
116 # Pass math through unaltered - mathjax does the rendering in the browser 107 # Pass math through unaltered - mathjax does the rendering in the browser
117 def block_math(self, text): 108 def block_math(self, text):