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.
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.
@@ -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()
@@ -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&nbsp; 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 </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>
@@ -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