Commit 7e5841848b8ef20da03d2d383315327c8b25995d
1 parent
8005184e
Exists in
master
and in
1 other branch
- fixed bug where repeated questions with same reference would crash during correction.
- removed unused code.
Showing
5 changed files
with
37 additions
and
37 deletions
Show diff stats
BUGS.md
1 | 1 | |
2 | 2 | # BUGS |
3 | 3 | |
4 | -- se um teste tiver a mesma pergunta (ref igual) varias vezes, rebenta na correcçao. As respostas são agregadas numa lista para cada ref. Ex: | |
5 | - {'ref1': 'resposta1', 'ref2': ['resposta2a', 'resposta2b']} | |
6 | -possivelmente as referencias das perguntas deveriam ser o "testeRef:numPergunta"... é preciso ver como depois estao associadas às correcções. | |
7 | 4 | - se directorio logs não existir no directorio actual (não perguntations) rebenta. |
8 | 5 | - usar thread.Lock para aceder a variaveis de estado? |
9 | 6 | - servidor nao esta a lidar com eventos scroll/resize |
... | ... | @@ -27,6 +24,7 @@ possivelmente as referencias das perguntas deveriam ser o "testeRef:numPergunta" |
27 | 24 | |
28 | 25 | # FIXED |
29 | 26 | |
27 | +- se um teste tiver a mesma pergunta repetida (ref igual), rebenta na correcçao. As respostas são agregadas numa lista para cada ref. Ex: {'ref1': 'resposta1', 'ref2': ['resposta2a', 'resposta2b']} | |
30 | 28 | - usar http://fontawesome.io/examples/ em vez dos do bootstrap3 |
31 | 29 | - se pergunta tiver 'type:' errado, rebenta. |
32 | 30 | - se submeter um teste so com information, da divisao por zero. | ... | ... |
app.py
... | ... | @@ -124,6 +124,8 @@ class App(object): |
124 | 124 | return None |
125 | 125 | |
126 | 126 | # ----------------------------------------------------------------------- |
127 | + # ans is a dictionary {question_index: answer, ...} | |
128 | + # for example: {0:'hello', 1:[1,2]} | |
127 | 129 | def correct_test(self, uid, ans): |
128 | 130 | t = self.online[uid]['test'] |
129 | 131 | t.update_answers(ans) |
... | ... | @@ -161,7 +163,7 @@ class App(object): |
161 | 163 | # ----------------------------------------------------------------------- |
162 | 164 | def giveup_test(self, uid): |
163 | 165 | t = self.online[uid]['test'] |
164 | - grade = t.giveup() | |
166 | + t.giveup() | |
165 | 167 | |
166 | 168 | # save JSON with the test |
167 | 169 | fname = ' -- '.join((t['student']['number'], t['ref'], str(t['finish_time']))) + '.json' |
... | ... | @@ -194,8 +196,6 @@ class App(object): |
194 | 196 | return self.online[uid].get('test', default) |
195 | 197 | def get_questions_path(self): |
196 | 198 | return self.testfactory['questions_dir'] |
197 | - def get_test_qtypes(self, uid): | |
198 | - return {q['ref']:q['type'] for q in self.online[uid]['test']['questions']} | |
199 | 199 | def get_student_grades_from_all_tests(self, uid): |
200 | 200 | with self.db_session() as s: |
201 | 201 | r = s.query(Test).filter_by(student_id=uid).order_by(Test.finishtime).all() | ... | ... |
serve.py
... | ... | @@ -192,30 +192,35 @@ class Root(object): |
192 | 192 | @require() |
193 | 193 | def correct(self, **kwargs): |
194 | 194 | # receives dictionary with answers |
195 | - # kwargs = {'answered-xpto': 'on', 'xpto': '13.45', ...} | |
195 | + # kwargs = {'answered-0': 'on', '0': '13.45', ...} | |
196 | 196 | # Format: |
197 | 197 | # checkbox - all off -> no key, 1 on -> string '0', >1 on -> ['0', '1'] |
198 | 198 | # radio - all off -> no key, 1 on -> string '0' |
199 | 199 | # text - always returns string. no answer '', otherwise 'dskdjs' |
200 | 200 | uid = cherrypy.session.get(SESSION_KEY) |
201 | - name = self.app.get_student_name(uid) | |
202 | - qq = self.app.get_test_qtypes(uid) # {'q1_ref': 'checkbox', ...} | |
201 | + t = self.app.get_test(uid) | |
203 | 202 | |
204 | - # each question that is marked to be classified must have an answer. | |
205 | - # `ans` contains the answers to be corrected. The missing ones were | |
206 | - # disabled by the student | |
203 | + # build dictionary ans={0: 'answer0', 1:, 'answer1', ...} | |
204 | + # questions not answer are not included. | |
207 | 205 | ans = {} |
208 | - for qref, qtype in qq.items(): | |
209 | - if 'answered-' + qref in kwargs: | |
210 | - # HTML HACK: checkboxes in html return None instead of an empty list if none is selected. Also, if only one is selected returns string instead of list of strings. | |
211 | - default_ans = [] if qtype == 'checkbox' else None | |
212 | - a = kwargs.get(qref, default_ans) | |
213 | - if qtype == 'checkbox' and isinstance(a, str): | |
214 | - a = [a] | |
215 | - ans[qref] = a | |
216 | - | |
217 | - grade = self.app.correct_test(uid, ans) | |
218 | - t = self.app.get_test(uid) | |
206 | + for i, q in enumerate(t['questions']): | |
207 | + if 'answered-' + str(i) in kwargs: | |
208 | + ans[i] = kwargs.get(str(i), None) | |
209 | + | |
210 | + # Begin HACK | |
211 | + # checkboxes in html do not have a stable type: | |
212 | + # returns None instead of [], when no checkboxes are selected | |
213 | + # returns '5' instead of ['5'], when one checkbox is selected | |
214 | + # returns correctly ['1', '3'], on multiple selections | |
215 | + # we fix it to always return a list | |
216 | + if q['type'] == 'checkbox': | |
217 | + if ans[i] is None: | |
218 | + ans[i] = [] | |
219 | + elif isinstance(ans[i], str): | |
220 | + ans[i] = [ans[i]] | |
221 | + # end HACK | |
222 | + | |
223 | + self.app.correct_test(uid, ans) | |
219 | 224 | self.app.logout(uid) |
220 | 225 | |
221 | 226 | # --- Expire session | ... | ... |
templates/test.html
... | ... | @@ -130,7 +130,7 @@ |
130 | 130 | </h4> |
131 | 131 | <div class="pull-right"> |
132 | 132 | Classificar |
133 | - <input type="checkbox" class="question_disabler" data-size="mini" name="answered-${q['ref']}" id="answered-${q['ref']}" checked=""> | |
133 | + <input type="checkbox" class="question_disabler" data-size="mini" name="answered-${i}" id="answered-${i}" checked=""> | |
134 | 134 | </div> |
135 | 135 | </div> |
136 | 136 | <div class="panel-body" id="example${i}"> |
... | ... | @@ -143,7 +143,7 @@ |
143 | 143 | <div class="list-group"> |
144 | 144 | % for opt in q['options']: |
145 | 145 | <a class="list-group-item"> |
146 | - ${md_to_html('<input type="radio" name="{0}" id="{0}{1}" value="{1}" {2}/> '.format(q['ref'], loop.index, 'checked' if q['answer'] is not None and str(loop.index) == q['answer'] else '') + opt, q['ref'], q['files'])} | |
146 | + ${md_to_html('<input type="radio" name="{0}" id="{0}:{1}" value="{1}" {2}/> '.format(i, loop.index, 'checked' if q['answer'] is not None and str(loop.index) == q['answer'] else '') + opt, q['ref'], q['files'])} | |
147 | 147 | </a> |
148 | 148 | % endfor |
149 | 149 | </div> |
... | ... | @@ -152,25 +152,25 @@ |
152 | 152 | <div class="list-group"> |
153 | 153 | % for opt in q['options']: |
154 | 154 | <a class="list-group-item"> |
155 | - ${md_to_html('<input type="checkbox" name="{0}" id="{0}{1}" value="{1}" {2}/> {3}'.format(q['ref'], loop.index, 'checked' if q['answer'] is not None and str(loop.index) in q['answer'] else '', opt), q['ref'], q['files'])} | |
155 | + ${md_to_html('<input type="checkbox" name="{0}" id="{0}:{1}" value="{1}" {2}/> '.format(i, loop.index, 'checked' if q['answer'] is not None and str(loop.index) in q['answer'] else '') + opt, q['ref'], q['files'])} | |
156 | 156 | </a> |
157 | 157 | % endfor |
158 | 158 | </div> |
159 | 159 | % elif q['type'] in ('text', 'text_regex'): |
160 | - <input type="text" name="${q['ref']}" class="form-control" value="${q['answer'] if q['answer'] is not None else ''}"> | |
160 | + <input type="text" name="${i}" class="form-control" value="${q['answer'] if q['answer'] is not None else ''}"> | |
161 | 161 | % elif q['type'] == 'textarea': |
162 | - <textarea class="form-control" rows="${q['lines']}" name="${q['ref']}">${q['answer'] if q['answer'] is not None else ''}</textarea><br /> | |
162 | + <textarea class="form-control" rows="${q['lines']}" name="${i}">${q['answer'] if q['answer'] is not None else ''}</textarea><br /> | |
163 | 163 | % endif |
164 | 164 | </fieldset> |
165 | 165 | |
166 | 166 | % if t['show_hints']: |
167 | 167 | % if 'hint' in q: |
168 | 168 | <p> |
169 | - <button class="btn btn-sm btn-warning" type="button" data-toggle="collapse" data-target="#hint-${q['ref']}" aria-expanded="false" aria-controls="hint-${q['ref']}"> | |
169 | + <button class="btn btn-sm btn-warning" type="button" data-toggle="collapse" data-target="#hint-${i}" aria-expanded="false" aria-controls="hint-${i}"> | |
170 | 170 | Ajuda |
171 | 171 | </button> |
172 | 172 | </p> |
173 | - <div class="collapse" id="hint-${q['ref']}"> | |
173 | + <div class="collapse" id="hint-${i}"> | |
174 | 174 | <div class="well"> |
175 | 175 | ${md_to_html(q['hint'], q['ref'], q['files'])} |
176 | 176 | </div> | ... | ... |
test.py
... | ... | @@ -9,9 +9,7 @@ import logging |
9 | 9 | logger = logging.getLogger(__name__) |
10 | 10 | |
11 | 11 | try: |
12 | - # import yaml | |
13 | 12 | import json |
14 | - import markdown | |
15 | 13 | except ImportError: |
16 | 14 | logger.critical('Python package missing. See README.md for instructions.') |
17 | 15 | sys.exit(1) |
... | ... | @@ -213,13 +211,12 @@ class Test(dict): |
213 | 211 | logger.info('Student {}: all answers cleared.'.format(self['student']['number'])) |
214 | 212 | |
215 | 213 | # ----------------------------------------------------------------------- |
216 | - # Given a dictionary ans={'someref': 'some answer'} updates the | |
214 | + # Given a dictionary ans={index: 'some answer'} updates the | |
217 | 215 | # answers of the test. Only affects questions referred. |
218 | 216 | def update_answers(self, ans): |
219 | - for q in self['questions']: | |
220 | - if q['ref'] in ans: | |
221 | - q['answer'] = ans[q['ref']] | |
222 | - logger.info('Student {}: answers updated.'.format(self['student']['number'])) | |
217 | + for i in ans: | |
218 | + self['questions'][i]['answer'] = ans[i] | |
219 | + logger.info('Student {}: {} answers updated.'.format(self['student']['number'], len(ans))) | |
223 | 220 | |
224 | 221 | # ----------------------------------------------------------------------- |
225 | 222 | # Corrects all the answers and computes the final grade | ... | ... |