Commit a51bcefdf1c7771cdaec4595f2c398a443d25fbb

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

Many changes:

- drop support for python 3.6
- min_level in course configuration so that a topic is unlocked if
dependencies are above the minimum level.
- changed shuffle to shuffle_questions in course configuration to avoid
confusion between the shuffle options of some questions.
- correction no longer returns grade, just updates q['grade'].
- remove prefix 'l' from students login name.
- allow codemirror text boxes to grow.
- updated yaml.load to yaml.safe_load (untested in regex).
BUGS.md
1 1  
2 2 # BUGS
3 3  
  4 +- testar se perguntas regex funcionam com yaml.safe_load.
4 5 - safari as vezes envia dois gets no inicio do topico. nesses casos, a segunda pergunta não é actualizada no browser... o topico tem de ser gerado qd se escolhe o topico em main_topics. O get nao deve alterar o estado.
5 6 - click numa opcao checkbox fora da checkbox+label não está a funcionar.
6 7 - shift-enter não está a funcionar
... ...
aprendizations/factory.py
... ... @@ -48,17 +48,17 @@ class QFactory(object):
48 48 # Depending on the type of question, a different question class will be
49 49 # instantiated. All these classes derive from the base class `Question`.
50 50 _types = {
51   - 'radio': QuestionRadio,
52   - 'checkbox': QuestionCheckbox,
53   - 'text': QuestionText,
54   - 'text-regex': QuestionTextRegex,
  51 + 'radio': QuestionRadio,
  52 + 'checkbox': QuestionCheckbox,
  53 + 'text': QuestionText,
  54 + 'text-regex': QuestionTextRegex,
55 55 'numeric-interval': QuestionNumericInterval,
56   - 'textarea': QuestionTextArea,
  56 + 'textarea': QuestionTextArea,
57 57 # -- informative panels --
58   - 'information': QuestionInformation,
59   - 'warning': QuestionInformation,
60   - 'alert': QuestionInformation,
61   - 'success': QuestionInformation,
  58 + 'information': QuestionInformation,
  59 + 'warning': QuestionInformation,
  60 + 'alert': QuestionInformation,
  61 + 'success': QuestionInformation,
62 62 }
63 63  
64 64 def __init__(self, question_dict={}):
... ... @@ -69,7 +69,7 @@ class QFactory(object):
69 69 # i.e. a question object (radio, checkbox, ...).
70 70 # -----------------------------------------------------------------------
71 71 # async def generate_async(self):
72   - # loop = asyncio.get_event_loop()
  72 + # loop = asyncio.get_running_loop()
73 73 # return await loop.run_in_executor(None, self.generate)
74 74  
75 75 def generate(self):
... ...
aprendizations/knowledge.py
... ... @@ -43,34 +43,33 @@ class StudentKnowledge(object):
43 43 now = datetime.now()
44 44 for tref, s in self.state.items():
45 45 dt = now - s['date']
46   - s['level'] *= 0.95 ** dt.days # forgetting factor 0.95 FIXME
  46 + s['level'] *= 0.98 ** dt.days # forgetting factor 0.95 FIXME
47 47  
48 48 # ------------------------------------------------------------------------
49 49 # Unlock topics whose dependencies are satisfied (> min_level)
50 50 # ------------------------------------------------------------------------
51 51 def unlock_topics(self):
52   - # minimum level that the dependencies of a topic must have
53   - # for the topic to be unlocked.
54   - min_level = 0.01
55   -
56 52 for topic in self.deps.nodes():
57 53 if topic not in self.state: # if locked
58 54 pred = self.deps.predecessors(topic)
  55 + min_level = self.deps.node[topic]['min_level']
59 56 if all(d in self.state and self.state[d]['level'] > min_level
60   - for d in pred): # all dependencies are done
  57 + for d in pred): # all deps are greater than min_level
61 58  
62 59 self.state[topic] = {
63 60 'level': 0.0, # unlocked
64 61 'date': datetime.now()
65 62 }
66 63 logger.debug(f'Unlocked "{topic}".')
  64 + # else: # lock this topic if deps do not satisfy min_level
  65 + # del self.state[topic]
67 66  
68 67 # ------------------------------------------------------------------------
69 68 # Start a new topic.
70 69 # questions: list of generated questions to do in the topic
71 70 # current_question: the current question to be presented
72 71 # ------------------------------------------------------------------------
73   - # FIXME async mas nao tem awaits...
  72 + # FIXME async mas nao tem awaits... do not allow restart same topic
74 73 async def start_topic(self, topic):
75 74 logger.debug(f'StudentKnowledge.start_topic({topic})')
76 75 if self.current_topic == topic:
... ... @@ -88,7 +87,7 @@ class StudentKnowledge(object):
88 87  
89 88 t = self.deps.node[topic]
90 89 k = t['choose']
91   - if t['shuffle']:
  90 + if t['shuffle_questions']:
92 91 questions = random.sample(t['questions'], k=k)
93 92 else:
94 93 questions = t['questions'][:k]
... ... @@ -131,11 +130,10 @@ class StudentKnowledge(object):
131 130 q = self.current_question
132 131 q['answer'] = answer
133 132 q['finish_time'] = datetime.now()
134   - grade = await q.correct_async()
135   -
136   - logger.debug(f'Grade {grade:.2} ({q["ref"]})')
  133 + await q.correct_async()
  134 + logger.debug(f'Grade {q["grade"]:.2} ({q["ref"]})')
137 135  
138   - if grade > 0.999:
  136 + if q['grade'] > 0.999:
139 137 self.correct_answers += 1
140 138 self.next_question()
141 139 action = 'right'
... ...
aprendizations/learnapp.py
... ... @@ -26,8 +26,7 @@ logger = logging.getLogger(__name__)
26 26 # helper functions
27 27 # ============================================================================
28 28 async def _bcrypt_hash(a, b):
29   - # loop = asyncio.get_running_loop() # FIXME python 3.7 only
30   - loop = asyncio.get_event_loop()
  29 + loop = asyncio.get_running_loop()
31 30 return await loop.run_in_executor(None, bcrypt.hashpw,
32 31 a.encode('utf-8'), b)
33 32  
... ... @@ -248,11 +247,12 @@ class LearnApp(object):
248 247  
249 248 # default attributes that apply to the topics
250 249 default_file = config.get('file', 'questions.yaml')
251   - default_shuffle = config.get('shuffle', True)
  250 + default_shuffle_questions = config.get('shuffle_questions', True)
252 251 default_choose = config.get('choose', 9999)
253 252 default_forgetting_factor = config.get('forgetting_factor', 1.0)
254 253 default_maxtries = config.get('max_tries', 3)
255 254 default_append_wrong = config.get('append_wrong', True)
  255 + default_min_level = config.get('min_level', 0.01) # to unlock topic
256 256  
257 257 # iterate over topics and populate graph
258 258 topics = config.get('topics', {})
... ... @@ -267,10 +267,11 @@ class LearnApp(object):
267 267 t['name'] = attr.get('name', tref)
268 268 t['path'] = path.join(g.graph['prefix'], tref) # prefix/topic
269 269 t['file'] = attr.get('file', default_file) # questions.yaml
270   - t['shuffle'] = attr.get('shuffle', default_shuffle)
  270 + t['shuffle_questions'] = attr.get('shuffle_questions', default_shuffle_questions)
271 271 t['max_tries'] = attr.get('max_tries', default_maxtries)
272 272 t['forgetting_factor'] = attr.get('forgetting_factor',
273 273 default_forgetting_factor)
  274 + t['min_level'] = attr.get('min_level', default_min_level)
274 275 t['choose'] = attr.get('choose', default_choose)
275 276 t['append_wrong'] = attr.get('append_wrong', default_append_wrong)
276 277 t['questions'] = attr.get('questions', [])
... ...
aprendizations/questions.py
... ... @@ -43,17 +43,15 @@ class Question(dict):
43 43 'files': {},
44 44 })
45 45  
46   - def correct(self) -> float:
  46 + def correct(self) -> None:
47 47 self['comments'] = ''
48 48 self['grade'] = 0.0
49   - return 0.0
50 49  
51   - async def correct_async(self) -> float:
52   - loop = asyncio.get_running_loop() # FIXME python 3.7 only
53   - # loop = asyncio.get_event_loop() # python 3.6
54   - return await loop.run_in_executor(None, self.correct)
  50 + async def correct_async(self) -> None:
  51 + loop = asyncio.get_running_loop()
  52 + await loop.run_in_executor(None, self.correct)
55 53  
56   - def set_defaults(self, d):
  54 + def set_defaults(self, d) -> None:
57 55 'Add k:v pairs from default dict d for nonexistent keys'
58 56 for k, v in d.items():
59 57 self.setdefault(k, v)
... ... @@ -204,8 +202,6 @@ class QuestionCheckbox(Question):
204 202  
205 203 self['grade'] = x / sum_abs
206 204  
207   - return self['grade']
208   -
209 205  
210 206 # ===========================================================================
211 207 class QuestionText(Question):
... ... @@ -239,8 +235,6 @@ class QuestionText(Question):
239 235 if self['answer'] is not None:
240 236 self['grade'] = 1.0 if self['answer'] in self['correct'] else 0.0
241 237  
242   - return self['grade']
243   -
244 238  
245 239 # ===========================================================================
246 240 class QuestionTextRegex(Question):
... ... @@ -271,8 +265,6 @@ class QuestionTextRegex(Question):
271 265 f'answer {self["answer"]}.')
272 266 self['grade'] = 1.0 if ok else 0.0
273 267  
274   - return self['grade']
275   -
276 268  
277 269 # ===========================================================================
278 270 class QuestionNumericInterval(Question):
... ... @@ -308,8 +300,6 @@ class QuestionNumericInterval(Question):
308 300 else:
309 301 self['grade'] = 1.0 if lower <= answer <= upper else 0.0
310 302  
311   - return self['grade']
312   -
313 303  
314 304 # ===========================================================================
315 305 class QuestionTextArea(Question):
... ... @@ -358,8 +348,6 @@ class QuestionTextArea(Question):
358 348 else:
359 349 self['grade'] = float(out)
360 350  
361   - return self['grade']
362   -
363 351  
364 352 # ===========================================================================
365 353 class QuestionInformation(Question):
... ... @@ -375,4 +363,3 @@ class QuestionInformation(Question):
375 363 def correct(self):
376 364 super().correct()
377 365 self['grade'] = 1.0 # always "correct" but points should be zero!
378   - return self['grade']
... ...
aprendizations/serve.py
... ... @@ -95,7 +95,7 @@ class LoginHandler(BaseHandler):
95 95 self.render('login.html', error='')
96 96  
97 97 async def post(self):
98   - uid = self.get_body_argument('uid')
  98 + uid = self.get_body_argument('uid').lstrip('l')
99 99 pw = self.get_body_argument('pw')
100 100  
101 101 login_ok = await self.learn.login(uid, pw)
... ...
aprendizations/static/css/topic.css
... ... @@ -23,6 +23,7 @@ html {
23 23 position: relative;
24 24 min-height: 100%;
25 25 }
26   -textarea {
27   - font-family: monospace;
  26 +.CodeMirror {
  27 + border: 1px solid #eee;
  28 + height: auto;
28 29 }
... ...
aprendizations/templates/question-textarea.html
... ... @@ -7,10 +7,9 @@
7 7  
8 8 <script>
9 9 var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
10   - lineNumbers: true,
11   - mode: "clike",
12   - // matchBrackets: true,
13   - // theme: "night",
  10 + lineNumbers: true,
  11 + viewportMargin: Infinity,
  12 + // styleActiveLine: true,
14 13 });
15 14 </script>
16 15  
... ...
aprendizations/templates/topic.html
... ... @@ -32,8 +32,8 @@
32 32 <script defer src="/static/mdbootstrap/js/bootstrap.min.js"></script>
33 33 <script defer src="/static/mdbootstrap/js/mdb.min.js"></script>
34 34 <script defer src="/static/fontawesome-free/js/all.min.js"></script>
35   - <script defer src="/static/codemirror/lib/codemirror.js"></script>
36   - <script defer src="/static/js/topic.js"></script>
  35 + <script defer src="/static/codemirror/lib/codemirror.js"></script>
  36 + <script defer src="/static/js/topic.js"></script>
37 37  
38 38 </head>
39 39 <!-- ===================================================================== -->
... ...
aprendizations/tools.py
... ... @@ -147,7 +147,7 @@ def load_yaml(filename, default=None):
147 147 else:
148 148 with f:
149 149 try:
150   - default = yaml.load(f)
  150 + default = yaml.safe_load(f) # FIXME check if supports all kinds of questions including regex
151 151 except yaml.YAMLError as e:
152 152 mark = e.problem_mark
153 153 logger.error(f'In file "{filename}" near line {mark.line}, '
... ... @@ -187,7 +187,7 @@ def run_script(script, args=[], stdin=&#39;&#39;, timeout=5):
187 187 logger.error(f'Return code {p.returncode} running "{script}".')
188 188 else:
189 189 try:
190   - output = yaml.load(p.stdout)
  190 + output = yaml.safe_load(p.stdout)
191 191 except Exception:
192 192 logger.error(f'Error parsing yaml output of "{script}"')
193 193 else:
... ...
demo/demo.yaml
... ... @@ -5,12 +5,13 @@ database: students.db
5 5  
6 6  
7 7 # values applie to each topic, if undefined there
8   -# default values are: file=question.yaml, shuffle=True, choose: all
9 8 file: questions.yaml
10   -shuffle: false
  9 +shuffle_questions: true
11 10 choose: 6
12 11 max_tries: 2
13   -forgetting_factor: 0.99
  12 +forgetting_factor: 0.97
  13 +min_level: 0.01
  14 +append_wrong: true
14 15  
15 16 # ----------------------------------------------------------------------------
16 17 topics:
... ...