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,

@@ -11,9 +11,11 @@ ou usar push (websockets?) @@ -11,9 +11,11 @@ ou usar push (websockets?)
11 - submissao faz um post ajax. 11 - submissao faz um post ajax.
12 - eventos unfocus? 12 - eventos unfocus?
13 - servidor nao esta a lidar com eventos scroll/resize. ignorar? 13 - servidor nao esta a lidar com eventos scroll/resize. ignorar?
  14 +- Test.reset_answers() unused.
14 15
15 # TODO 16 # TODO
16 17
  18 +- fazer package para instalar perguntations com pip.
17 - adicionar opcao para eliminar um teste em curso. 19 - adicionar opcao para eliminar um teste em curso.
18 - gerar teste qd o prof autoriza? melhor nao, pode apagar o teste em curso. gerar previamente e manter uma pool de testes gerados? 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 - enviar resposta de cada pergunta individualmente. 21 - enviar resposta de cada pergunta individualmente.
@@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
8 8
9 ### 1.1 Requirements 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 - tornado 13 - tornado
14 - mistune 14 - mistune
@@ -42,13 +42,13 @@ I personally prefer python packages to be installed for a single user, but if a @@ -42,13 +42,13 @@ I personally prefer python packages to be installed for a single user, but if a
42 Linux: 42 Linux:
43 43
44 ```bash 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 macOS macports: 48 macOS macports:
49 49
50 ```bash 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
@@ -67,7 +67,7 @@ class App(object): @@ -67,7 +67,7 @@ class App(object):
67 try: 67 try:
68 self.testfactory = test.TestFactory(testconf) 68 self.testfactory = test.TestFactory(testconf)
69 except test.TestFactoryException: 69 except test.TestFactoryException:
70 - logger.critical('Can\'t create test factory.') 70 + logger.critical('Cannot create test factory.')
71 raise AppException() 71 raise AppException()
72 72
73 # connect to database and check registered students 73 # connect to database and check registered students
@@ -332,8 +332,7 @@ class QuestionTextArea(Question): @@ -332,8 +332,7 @@ class QuestionTextArea(Question):
332 'correct': '' # trying to execute this will fail => grade 0.0 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 # can return negative values for wrong answers 338 # can return negative values for wrong answers
@@ -131,7 +131,6 @@ class RootHandler(BaseHandler): @@ -131,7 +131,6 @@ class RootHandler(BaseHandler):
131 # ---------------------------------------------------------------------------- 131 # ----------------------------------------------------------------------------
132 class FileHandler(BaseHandler): 132 class FileHandler(BaseHandler):
133 SUPPORTED_METHODS = ['GET'] 133 SUPPORTED_METHODS = ['GET']
134 - chunk_size = 512 * 1024 # serve up to 512 KiB multiple times  
135 134
136 @tornado.web.authenticated 135 @tornado.web.authenticated
137 async def get(self): 136 async def get(self):
@@ -139,40 +138,79 @@ class FileHandler(BaseHandler): @@ -139,40 +138,79 @@ class FileHandler(BaseHandler):
139 ref = self.get_query_argument('ref', None) 138 ref = self.get_query_argument('ref', None)
140 image = self.get_query_argument('image', None) 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,6 +292,8 @@ class TestHandler(BaseHandler):
254 292
255 # --- REVIEW ------------------------------------------------------------- 293 # --- REVIEW -------------------------------------------------------------
256 class ReviewHandler(BaseHandler): 294 class ReviewHandler(BaseHandler):
  295 + SUPPORTED_METHODS = ['GET']
  296 +
257 _templates = { 297 _templates = {
258 'radio': 'review-question-radio.html', 298 'radio': 'review-question-radio.html',
259 'checkbox': 'review-question-checkbox.html', 299 'checkbox': 'review-question-checkbox.html',
@@ -50,8 +50,6 @@ class TestFactory(dict): @@ -50,8 +50,6 @@ class TestFactory(dict):
50 logger.info(f'Checking question "{r}".') 50 logger.info(f'Checking question "{r}".')
51 try: 51 try:
52 self.question_factory.generate(r) 52 self.question_factory.generate(r)
53 - # except questions.QuestionFactoryException:  
54 - # logger.critical(f'Can\'t generate question "{r}".')  
55 except: 53 except:
56 logger.critical(f'Can\'t generate question "{r}".') 54 logger.critical(f'Can\'t generate question "{r}".')
57 errors_found = True 55 errors_found = True
@@ -220,10 +218,10 @@ class Test(dict): @@ -220,10 +218,10 @@ class Test(dict):
220 218
221 # ----------------------------------------------------------------------- 219 # -----------------------------------------------------------------------
222 # Removes all answers from the test (clean) 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 # Given a dictionary ans={index: 'some answer'} updates the 227 # Given a dictionary ans={index: 'some answer'} updates the
@@ -238,14 +236,11 @@ class Test(dict): @@ -238,14 +236,11 @@ class Test(dict):
238 async def correct(self): 236 async def correct(self):
239 self['finish_time'] = datetime.now() 237 self['finish_time'] = datetime.now()
240 self['state'] = 'FINISHED' 238 self['state'] = 'FINISHED'
241 -  
242 grade = 0.0 239 grade = 0.0
243 for q in self['questions']: 240 for q in self['questions']:
244 grade += await q.correct_async() * q['points'] 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 return self['grade'] 244 return self['grade']
250 245
251 # ----------------------------------------------------------------------- 246 # -----------------------------------------------------------------------
@@ -259,5 +254,5 @@ class Test(dict): @@ -259,5 +254,5 @@ class Test(dict):
259 # ----------------------------------------------------------------------- 254 # -----------------------------------------------------------------------
260 def save_json(self, filepath): 255 def save_json(self, filepath):
261 with open(path.expanduser(filepath), 'w') as f: 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 logger.info(f'Student {self["student"]["number"]}: saved JSON file.') 258 logger.info(f'Student {self["student"]["number"]}: saved JSON file.')