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.
BUGS.md
... ... @@ -11,7 +11,6 @@
11 11  
12 12 # TODO
13 13  
14   -- servir imagens/ficheiros.
15 14 - session management. close after inactive time.
16 15 - each topic only loads a sample of K questions (max) in random order.
17 16 - radio e checkboxes, aceitar numeros como seleccao das opcoes.
... ... @@ -30,6 +29,7 @@
30 29  
31 30 # FIXED
32 31  
  32 +- servir imagens/ficheiros.
33 33 - radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa).
34 34 - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória.
35 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 # -
... ...
serve.py
... ... @@ -30,8 +30,8 @@ class WebApplication(tornado.web.Application):
30 30 (r'/change_password', ChangePasswordHandler),
31 31 (r'/question', QuestionHandler), # each question
32 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 36 settings = {
37 37 'template_path': path.join(path.dirname(__file__), 'templates'),
... ... @@ -143,20 +143,36 @@ class TopicHandler(BaseHandler):
143 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 147 class FileHandler(BaseHandler):
148 148 @tornado.web.authenticated
149   - def get(self, filename):
  149 + async def get(self, filename):
150 150 uid = self.current_user
151 151 public_dir = self.learn.get_current_public_dir(uid)
152 152 filepath = path.expanduser(path.join(public_dir, filename))
  153 + chunk_size = 1024 * 1024 # serve 1MiB multiple times
153 154 try:
154 155 f = open(filepath, 'rb')
155 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 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 178 # respond to AJAX to get a JSON question
... ...
tools.py
... ... @@ -99,19 +99,10 @@ class HighlightRenderer(mistune.Renderer):
99 99 def table(self, header, body):
100 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 107 # Pass math through unaltered - mathjax does the rendering in the browser
117 108 def block_math(self, text):
... ...