Commit 6eac010448253294d1d9ba404c8093cde8419e35

Authored by Miguel Barao
1 parent d7ad8a11
Exists in master and in 1 other branch dev

changed how files (images) are served. Not it is serving the whole file, still asynchronous,

BUGS.md
... ... @@ -11,9 +11,11 @@ ou usar push (websockets?)
11 11 - submissao faz um post ajax.
12 12 - eventos unfocus?
13 13 - servidor nao esta a lidar com eventos scroll/resize. ignorar?
  14 +- Test.reset_answers() unused.
14 15  
15 16 # TODO
16 17  
  18 +- fazer package para instalar perguntations com pip.
17 19 - adicionar opcao para eliminar um teste em curso.
18 20 - gerar teste qd o prof autoriza? melhor nao, pode apagar o teste em curso. gerar previamente e manter uma pool de testes gerados?
19 21 - enviar resposta de cada pergunta individualmente.
... ...
README.md
... ... @@ -8,7 +8,7 @@
8 8  
9 9 ### 1.1 Requirements
10 10  
11   -The webserver is a python application and requires `python3.6` and `pip` to be installed, plus the following additional packages:
  11 +The webserver is a python application and requires `python3.7` and `pip` to be installed, plus the following additional packages:
12 12  
13 13 - tornado
14 14 - mistune
... ... @@ -42,13 +42,13 @@ I personally prefer python packages to be installed for a single user, but if a
42 42 Linux:
43 43  
44 44 ```bash
45   -apt-get install py36-tornado py36-mistune? py36-yaml py36-pygments...
  45 +apt-get install py37-tornado py37-mistune? py37-yaml py37-pygments...
46 46 ```
47 47  
48 48 macOS macports:
49 49  
50 50 ```bash
51   -port install py36-py36-tornado py36-mistune? py36-yaml py36-pygments...
  51 +port install py37-py37-tornado py37-mistune? py37-yaml py37-pygments...
52 52 ```
53 53  
54 54  
... ...
app.py
... ... @@ -67,7 +67,7 @@ class App(object):
67 67 try:
68 68 self.testfactory = test.TestFactory(testconf)
69 69 except test.TestFactoryException:
70   - logger.critical('Can\'t create test factory.')
  70 + logger.critical('Cannot create test factory.')
71 71 raise AppException()
72 72  
73 73 # connect to database and check registered students
... ...
questions.py
... ... @@ -332,8 +332,7 @@ class QuestionTextArea(Question):
332 332 'correct': '' # trying to execute this will fail => grade 0.0
333 333 })
334 334  
335   - self['correct'] = path.join(self['path'], self['correct'])
336   - # self['correct'] = path.abspath(path.normpath(path.join(self['path'], self['correct']))) # abspath will prepend cwd, which is plain wrong...
  335 + self['correct'] = path.abspath(path.normpath(path.join(self['path'], self['correct']))) # abspath will prepend cwd, which is plain wrong...
337 336  
338 337 #------------------------------------------------------------------------
339 338 # can return negative values for wrong answers
... ...
serve.py
... ... @@ -131,7 +131,6 @@ class RootHandler(BaseHandler):
131 131 # ----------------------------------------------------------------------------
132 132 class FileHandler(BaseHandler):
133 133 SUPPORTED_METHODS = ['GET']
134   - chunk_size = 512 * 1024 # serve up to 512 KiB multiple times
135 134  
136 135 @tornado.web.authenticated
137 136 async def get(self):
... ... @@ -139,40 +138,79 @@ class FileHandler(BaseHandler):
139 138 ref = self.get_query_argument('ref', None)
140 139 image = self.get_query_argument('image', None)
141 140  
142   - # FIXME does not work when user 0 is reviewing a test
  141 + if uid != '0':
  142 + t = self.testapp.get_student_test(uid)
  143 + else:
  144 + logging.error('FIXME Cannot serve images for review.')
  145 + raise tornado.web.HTTPError(404) # FIXME admin
  146 +
  147 + if t is None:
  148 + raise tornado.web.HTTPError(404) # Not Found
  149 +
  150 + for q in t['questions']:
  151 + if q['ref'] == ref:
  152 + filepath = path.join(q['path'], 'public', image)
  153 +
  154 + try:
  155 + f = open(filepath, 'rb')
  156 + except FileNotFoundError:
  157 + logging.error(f'File not found: {filepath}')
  158 + except PermissionError:
  159 + logging.error(f'No permission: {filepath}')
  160 + else:
  161 + content_type = mimetypes.guess_type(image)
  162 + self.set_header("Content-Type", content_type[0])
  163 + with f:
  164 + self.write(f.read())
  165 + await self.flush()
143 166  
144   - t = self.testapp.get_student_test(uid)
145   - if t is not None:
146   - for q in t['questions']:
147   - if q['ref'] == ref:
148   - filepath = path.join(q['path'], 'public', image)
149   -
150   - try:
151   - f = open(filepath, 'rb')
152   - except FileNotFoundError:
153   - logging.error(f'File not found: {filepath}')
154   - except PermissionError:
155   - logging.error(f'No permission: {filepath}')
156   - else:
157   - content_type = mimetypes.guess_type(image)
158   - self.set_header("Content-Type", content_type[0])
159   -
160   - # divide the file into chunks and write one chunk at a time, so
161   - # that the write does not block the ioloop for very long.
162   - with f:
163   - chunk = f.read(self.chunk_size)
164   - while chunk:
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   - break # client closed the connection
170   - finally:
171   - del chunk
172   - await asyncio.sleep(0)
173   - chunk = f.read(self.chunk_size)
174   -
175   - raise tornado.web.HTTPError(status_code=404)
  167 +
  168 +
  169 +
  170 +# class FileHandler(BaseHandler):
  171 +# SUPPORTED_METHODS = ['GET']
  172 +# chunk_size = 512 * 1024 # serve up to 512 KiB multiple times
  173 +
  174 +# @tornado.web.authenticated
  175 +# async def get(self):
  176 +# uid = self.current_user
  177 +# ref = self.get_query_argument('ref', None)
  178 +# image = self.get_query_argument('image', None)
  179 +
  180 +# # FIXME does not work when user 0 is reviewing a test
  181 +
  182 +# t = self.testapp.get_student_test(uid)
  183 +# if t is not None:
  184 +# for q in t['questions']:
  185 +# if q['ref'] == ref:
  186 +# filepath = path.join(q['path'], 'public', image)
  187 +
  188 +# try:
  189 +# f = open(filepath, 'rb')
  190 +# except FileNotFoundError:
  191 +# logging.error(f'File not found: {filepath}')
  192 +# except PermissionError:
  193 +# logging.error(f'No permission: {filepath}')
  194 +# else:
  195 +# content_type = mimetypes.guess_type(image)
  196 +# self.set_header("Content-Type", content_type[0])
  197 +
  198 +# # divide the file into chunks and write one chunk at a time, so
  199 +# # that the write does not block the ioloop for very long.
  200 +# with f:
  201 +# chunk = f.read(self.chunk_size)
  202 +# while chunk:
  203 +# try:
  204 +# self.write(chunk) # write the cunk to response
  205 +# await self.flush() # flush the current chunk to socket
  206 +# except iostream.StreamClosedError:
  207 +# break # client closed the connection
  208 +# finally:
  209 +# del chunk
  210 +# await asyncio.sleep(0)
  211 +# chunk = f.read(self.chunk_size)
  212 +
  213 +# raise tornado.web.HTTPError(status_code=404) # Not Found
176 214  
177 215  
178 216 # -------------------------------------------------------------------------
... ... @@ -254,6 +292,8 @@ class TestHandler(BaseHandler):
254 292  
255 293 # --- REVIEW -------------------------------------------------------------
256 294 class ReviewHandler(BaseHandler):
  295 + SUPPORTED_METHODS = ['GET']
  296 +
257 297 _templates = {
258 298 'radio': 'review-question-radio.html',
259 299 'checkbox': 'review-question-checkbox.html',
... ...
test.py
... ... @@ -50,8 +50,6 @@ class TestFactory(dict):
50 50 logger.info(f'Checking question "{r}".')
51 51 try:
52 52 self.question_factory.generate(r)
53   - # except questions.QuestionFactoryException:
54   - # logger.critical(f'Can\'t generate question "{r}".')
55 53 except:
56 54 logger.critical(f'Can\'t generate question "{r}".')
57 55 errors_found = True
... ... @@ -220,10 +218,10 @@ class Test(dict):
220 218  
221 219 # -----------------------------------------------------------------------
222 220 # Removes all answers from the test (clean)
223   - def reset_answers(self):
224   - for q in self['questions']:
225   - q['answer'] = None
226   - logger.info(f'Student {self["student"]["number"]}: all answers cleared.')
  221 + # def reset_answers(self):
  222 + # for q in self['questions']:
  223 + # q['answer'] = None
  224 + # logger.info(f'Student {self["student"]["number"]}: all answers cleared.')
227 225  
228 226 # -----------------------------------------------------------------------
229 227 # Given a dictionary ans={index: 'some answer'} updates the
... ... @@ -238,14 +236,11 @@ class Test(dict):
238 236 async def correct(self):
239 237 self['finish_time'] = datetime.now()
240 238 self['state'] = 'FINISHED'
241   -
242 239 grade = 0.0
243 240 for q in self['questions']:
244 241 grade += await q.correct_async() * q['points']
245   -
246   - self['grade'] = max(0, round(grade, 1)) # avoid negative final grades
247   -
248   - logger.info(f'Student {self["student"]["number"]}: correction gave {self["grade"]} points.')
  242 + self['grade'] = max(0, round(grade, 1)) # avoid negative grade
  243 + logger.info(f'Student {self["student"]["number"]}: {self["grade"]} points.')
249 244 return self['grade']
250 245  
251 246 # -----------------------------------------------------------------------
... ... @@ -259,5 +254,5 @@ class Test(dict):
259 254 # -----------------------------------------------------------------------
260 255 def save_json(self, filepath):
261 256 with open(path.expanduser(filepath), 'w') as f:
262   - json.dump(self, f, indent=2, default=str) # HACK default=str required for datetime objects
  257 + json.dump(self, f, indent=2, default=str) # default=str required for datetime objects
263 258 logger.info(f'Student {self["student"]["number"]}: saved JSON file.')
... ...