Commit 6eac010448253294d1d9ba404c8093cde8419e35
1 parent
d7ad8a11
Exists in
master
and in
1 other branch
changed how files (images) are served. Not it is serving the whole file, still asynchronous,
Showing
6 changed files
with
88 additions
and
52 deletions
Show diff stats
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.') | ... | ... |