Commit 7e5841848b8ef20da03d2d383315327c8b25995d

Authored by Miguel Barão
1 parent 8005184e
Exists in master and in 1 other branch dev

- fixed bug where repeated questions with same reference would crash during correction.

- removed unused code.
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&nbsp;
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
... ...