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.') | ... | ... |