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,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. |
README.md
@@ -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 |
app.py
@@ -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 |
questions.py
@@ -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 |
serve.py
@@ -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', |
test.py
@@ -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.') |