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 | # BUGS | 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 | - se directorio logs não existir no directorio actual (não perguntations) rebenta. | 4 | - se directorio logs não existir no directorio actual (não perguntations) rebenta. |
8 | - usar thread.Lock para aceder a variaveis de estado? | 5 | - usar thread.Lock para aceder a variaveis de estado? |
9 | - servidor nao esta a lidar com eventos scroll/resize | 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,6 +24,7 @@ possivelmente as referencias das perguntas deveriam ser o "testeRef:numPergunta" | ||
27 | 24 | ||
28 | # FIXED | 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 | - usar http://fontawesome.io/examples/ em vez dos do bootstrap3 | 28 | - usar http://fontawesome.io/examples/ em vez dos do bootstrap3 |
31 | - se pergunta tiver 'type:' errado, rebenta. | 29 | - se pergunta tiver 'type:' errado, rebenta. |
32 | - se submeter um teste so com information, da divisao por zero. | 30 | - se submeter um teste so com information, da divisao por zero. |
app.py
@@ -124,6 +124,8 @@ class App(object): | @@ -124,6 +124,8 @@ class App(object): | ||
124 | return None | 124 | return None |
125 | 125 | ||
126 | # ----------------------------------------------------------------------- | 126 | # ----------------------------------------------------------------------- |
127 | + # ans is a dictionary {question_index: answer, ...} | ||
128 | + # for example: {0:'hello', 1:[1,2]} | ||
127 | def correct_test(self, uid, ans): | 129 | def correct_test(self, uid, ans): |
128 | t = self.online[uid]['test'] | 130 | t = self.online[uid]['test'] |
129 | t.update_answers(ans) | 131 | t.update_answers(ans) |
@@ -161,7 +163,7 @@ class App(object): | @@ -161,7 +163,7 @@ class App(object): | ||
161 | # ----------------------------------------------------------------------- | 163 | # ----------------------------------------------------------------------- |
162 | def giveup_test(self, uid): | 164 | def giveup_test(self, uid): |
163 | t = self.online[uid]['test'] | 165 | t = self.online[uid]['test'] |
164 | - grade = t.giveup() | 166 | + t.giveup() |
165 | 167 | ||
166 | # save JSON with the test | 168 | # save JSON with the test |
167 | fname = ' -- '.join((t['student']['number'], t['ref'], str(t['finish_time']))) + '.json' | 169 | fname = ' -- '.join((t['student']['number'], t['ref'], str(t['finish_time']))) + '.json' |
@@ -194,8 +196,6 @@ class App(object): | @@ -194,8 +196,6 @@ class App(object): | ||
194 | return self.online[uid].get('test', default) | 196 | return self.online[uid].get('test', default) |
195 | def get_questions_path(self): | 197 | def get_questions_path(self): |
196 | return self.testfactory['questions_dir'] | 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 | def get_student_grades_from_all_tests(self, uid): | 199 | def get_student_grades_from_all_tests(self, uid): |
200 | with self.db_session() as s: | 200 | with self.db_session() as s: |
201 | r = s.query(Test).filter_by(student_id=uid).order_by(Test.finishtime).all() | 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,30 +192,35 @@ class Root(object): | ||
192 | @require() | 192 | @require() |
193 | def correct(self, **kwargs): | 193 | def correct(self, **kwargs): |
194 | # receives dictionary with answers | 194 | # receives dictionary with answers |
195 | - # kwargs = {'answered-xpto': 'on', 'xpto': '13.45', ...} | 195 | + # kwargs = {'answered-0': 'on', '0': '13.45', ...} |
196 | # Format: | 196 | # Format: |
197 | # checkbox - all off -> no key, 1 on -> string '0', >1 on -> ['0', '1'] | 197 | # checkbox - all off -> no key, 1 on -> string '0', >1 on -> ['0', '1'] |
198 | # radio - all off -> no key, 1 on -> string '0' | 198 | # radio - all off -> no key, 1 on -> string '0' |
199 | # text - always returns string. no answer '', otherwise 'dskdjs' | 199 | # text - always returns string. no answer '', otherwise 'dskdjs' |
200 | uid = cherrypy.session.get(SESSION_KEY) | 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 | ans = {} | 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 | self.app.logout(uid) | 224 | self.app.logout(uid) |
220 | 225 | ||
221 | # --- Expire session | 226 | # --- Expire session |
templates/test.html
@@ -130,7 +130,7 @@ | @@ -130,7 +130,7 @@ | ||
130 | </h4> | 130 | </h4> |
131 | <div class="pull-right"> | 131 | <div class="pull-right"> |
132 | Classificar | 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 | </div> | 134 | </div> |
135 | </div> | 135 | </div> |
136 | <div class="panel-body" id="example${i}"> | 136 | <div class="panel-body" id="example${i}"> |
@@ -143,7 +143,7 @@ | @@ -143,7 +143,7 @@ | ||
143 | <div class="list-group"> | 143 | <div class="list-group"> |
144 | % for opt in q['options']: | 144 | % for opt in q['options']: |
145 | <a class="list-group-item"> | 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 | </a> | 147 | </a> |
148 | % endfor | 148 | % endfor |
149 | </div> | 149 | </div> |
@@ -152,25 +152,25 @@ | @@ -152,25 +152,25 @@ | ||
152 | <div class="list-group"> | 152 | <div class="list-group"> |
153 | % for opt in q['options']: | 153 | % for opt in q['options']: |
154 | <a class="list-group-item"> | 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 | </a> | 156 | </a> |
157 | % endfor | 157 | % endfor |
158 | </div> | 158 | </div> |
159 | % elif q['type'] in ('text', 'text_regex'): | 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 | % elif q['type'] == 'textarea': | 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 | % endif | 163 | % endif |
164 | </fieldset> | 164 | </fieldset> |
165 | 165 | ||
166 | % if t['show_hints']: | 166 | % if t['show_hints']: |
167 | % if 'hint' in q: | 167 | % if 'hint' in q: |
168 | <p> | 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 | Ajuda | 170 | Ajuda |
171 | </button> | 171 | </button> |
172 | </p> | 172 | </p> |
173 | - <div class="collapse" id="hint-${q['ref']}"> | 173 | + <div class="collapse" id="hint-${i}"> |
174 | <div class="well"> | 174 | <div class="well"> |
175 | ${md_to_html(q['hint'], q['ref'], q['files'])} | 175 | ${md_to_html(q['hint'], q['ref'], q['files'])} |
176 | </div> | 176 | </div> |
test.py
@@ -9,9 +9,7 @@ import logging | @@ -9,9 +9,7 @@ import logging | ||
9 | logger = logging.getLogger(__name__) | 9 | logger = logging.getLogger(__name__) |
10 | 10 | ||
11 | try: | 11 | try: |
12 | - # import yaml | ||
13 | import json | 12 | import json |
14 | - import markdown | ||
15 | except ImportError: | 13 | except ImportError: |
16 | logger.critical('Python package missing. See README.md for instructions.') | 14 | logger.critical('Python package missing. See README.md for instructions.') |
17 | sys.exit(1) | 15 | sys.exit(1) |
@@ -213,13 +211,12 @@ class Test(dict): | @@ -213,13 +211,12 @@ class Test(dict): | ||
213 | logger.info('Student {}: all answers cleared.'.format(self['student']['number'])) | 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 | # answers of the test. Only affects questions referred. | 215 | # answers of the test. Only affects questions referred. |
218 | def update_answers(self, ans): | 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 | # Corrects all the answers and computes the final grade | 222 | # Corrects all the answers and computes the final grade |