Commit 8e6019534a61621920e1352ff4cc18d642a2527a
1 parent
26c0d61d
Exists in
master
and in
1 other branch
fix mostly flake8 errors
Showing
10 changed files
with
180 additions
and
150 deletions
Show diff stats
aprendizations/factory.py
@@ -33,8 +33,9 @@ import logging | @@ -33,8 +33,9 @@ import logging | ||
33 | # project | 33 | # project |
34 | from aprendizations.tools import run_script | 34 | from aprendizations.tools import run_script |
35 | from aprendizations.questions import (QuestionInformation, QuestionRadio, | 35 | from aprendizations.questions import (QuestionInformation, QuestionRadio, |
36 | - QuestionCheckbox, QuestionText, QuestionTextRegex, QuestionTextArea, | ||
37 | - QuestionNumericInterval) | 36 | + QuestionCheckbox, QuestionText, |
37 | + QuestionTextRegex, QuestionTextArea, | ||
38 | + QuestionNumericInterval) | ||
38 | 39 | ||
39 | # setup logger for this module | 40 | # setup logger for this module |
40 | logger = logging.getLogger(__name__) | 41 | logger = logging.getLogger(__name__) |
@@ -47,17 +48,17 @@ class QFactory(object): | @@ -47,17 +48,17 @@ class QFactory(object): | ||
47 | # Depending on the type of question, a different question class will be | 48 | # Depending on the type of question, a different question class will be |
48 | # instantiated. All these classes derive from the base class `Question`. | 49 | # instantiated. All these classes derive from the base class `Question`. |
49 | _types = { | 50 | _types = { |
50 | - 'radio' : QuestionRadio, | ||
51 | - 'checkbox' : QuestionCheckbox, | ||
52 | - 'text' : QuestionText, | ||
53 | - 'text-regex': QuestionTextRegex, | 51 | + 'radio': QuestionRadio, |
52 | + 'checkbox': QuestionCheckbox, | ||
53 | + 'text': QuestionText, | ||
54 | + 'text-regex': QuestionTextRegex, | ||
54 | 'numeric-interval': QuestionNumericInterval, | 55 | 'numeric-interval': QuestionNumericInterval, |
55 | - 'textarea' : QuestionTextArea, | 56 | + 'textarea': QuestionTextArea, |
56 | # -- informative panels -- | 57 | # -- informative panels -- |
57 | - 'information': QuestionInformation, | ||
58 | - 'warning' : QuestionInformation, | ||
59 | - 'alert' : QuestionInformation, | ||
60 | - 'success' : QuestionInformation, | 58 | + 'information': QuestionInformation, |
59 | + 'warning': QuestionInformation, | ||
60 | + 'alert': QuestionInformation, | ||
61 | + 'success': QuestionInformation, | ||
61 | } | 62 | } |
62 | 63 | ||
63 | def __init__(self, question_dict={}): | 64 | def __init__(self, question_dict={}): |
@@ -82,17 +83,16 @@ class QFactory(object): | @@ -82,17 +83,16 @@ class QFactory(object): | ||
82 | # output is then yaml parsed into a dictionary `q`. | 83 | # output is then yaml parsed into a dictionary `q`. |
83 | if q['type'] == 'generator': | 84 | if q['type'] == 'generator': |
84 | logger.debug(f' \\_ Running "{q["script"]}".') | 85 | logger.debug(f' \\_ Running "{q["script"]}".') |
85 | - q.setdefault('arg', '') # optional arguments will be sent to stdin | 86 | + q.setdefault('arg', '') # optional arguments will be sent to stdin |
86 | script = path.join(q['path'], q['script']) | 87 | script = path.join(q['path'], q['script']) |
87 | out = run_script(script=script, stdin=q['arg']) | 88 | out = run_script(script=script, stdin=q['arg']) |
88 | q.update(out) | 89 | q.update(out) |
89 | 90 | ||
90 | # Finally we create an instance of Question() | 91 | # Finally we create an instance of Question() |
91 | try: | 92 | try: |
92 | - qinstance = self._types[q['type']](q) # instance with correct class | 93 | + qinstance = self._types[q['type']](q) # instance matching class |
93 | except KeyError as e: | 94 | except KeyError as e: |
94 | - logger.error(f'Unknown type "{q["type"]}" in question "{q["ref"]}"') | 95 | + logger.error(f'Unknown type "{q["type"]}" in "{q["ref"]}"') |
95 | raise e | 96 | raise e |
96 | else: | 97 | else: |
97 | return qinstance | 98 | return qinstance |
98 | - |
aprendizations/initdb.py
@@ -26,40 +26,40 @@ def parse_commandline_arguments(): | @@ -26,40 +26,40 @@ def parse_commandline_arguments(): | ||
26 | ' is created.') | 26 | ' is created.') |
27 | 27 | ||
28 | argparser.add_argument('csvfile', | 28 | argparser.add_argument('csvfile', |
29 | - nargs='*', | ||
30 | - type=str, | ||
31 | - default='', | ||
32 | - help='CSV file to import (SIIUE)') | 29 | + nargs='*', |
30 | + type=str, | ||
31 | + default='', | ||
32 | + help='CSV file to import (SIIUE)') | ||
33 | 33 | ||
34 | argparser.add_argument('--db', | 34 | argparser.add_argument('--db', |
35 | - default='students.db', | ||
36 | - type=str, | ||
37 | - help='database file') | 35 | + default='students.db', |
36 | + type=str, | ||
37 | + help='database file') | ||
38 | 38 | ||
39 | argparser.add_argument('-A', '--admin', | 39 | argparser.add_argument('-A', '--admin', |
40 | - action='store_true', | ||
41 | - help='insert the admin user') | 40 | + action='store_true', |
41 | + help='insert the admin user') | ||
42 | 42 | ||
43 | argparser.add_argument('-a', '--add', | 43 | argparser.add_argument('-a', '--add', |
44 | - nargs=2, | ||
45 | - action='append', | ||
46 | - metavar=('uid', 'name'), | ||
47 | - help='add new user') | 44 | + nargs=2, |
45 | + action='append', | ||
46 | + metavar=('uid', 'name'), | ||
47 | + help='add new user') | ||
48 | 48 | ||
49 | argparser.add_argument('-u', '--update', | 49 | argparser.add_argument('-u', '--update', |
50 | - nargs='+', | ||
51 | - metavar='uid', | ||
52 | - default=[], | ||
53 | - help='users to update') | 50 | + nargs='+', |
51 | + metavar='uid', | ||
52 | + default=[], | ||
53 | + help='users to update') | ||
54 | 54 | ||
55 | argparser.add_argument('--pw', | 55 | argparser.add_argument('--pw', |
56 | - default=None, | ||
57 | - type=str, | ||
58 | - help='set password for new and updated users') | 56 | + default=None, |
57 | + type=str, | ||
58 | + help='set password for new and updated users') | ||
59 | 59 | ||
60 | argparser.add_argument('-V', '--verbose', | 60 | argparser.add_argument('-V', '--verbose', |
61 | - action='store_true', | ||
62 | - help='show all students in database') | 61 | + action='store_true', |
62 | + help='show all students in database') | ||
63 | 63 | ||
64 | return argparser.parse_args() | 64 | return argparser.parse_args() |
65 | 65 | ||
@@ -104,7 +104,7 @@ def insert_students_into_db(session, students): | @@ -104,7 +104,7 @@ def insert_students_into_db(session, students): | ||
104 | try: | 104 | try: |
105 | # --- start db session --- | 105 | # --- start db session --- |
106 | session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) | 106 | session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) |
107 | - for s in students]) | 107 | + for s in students]) |
108 | 108 | ||
109 | session.commit() | 109 | session.commit() |
110 | 110 | ||
@@ -117,7 +117,7 @@ def insert_students_into_db(session, students): | @@ -117,7 +117,7 @@ def insert_students_into_db(session, students): | ||
117 | def show_students_in_database(session, verbose=False): | 117 | def show_students_in_database(session, verbose=False): |
118 | try: | 118 | try: |
119 | users = session.query(Student).order_by(Student.id).all() | 119 | users = session.query(Student).order_by(Student.id).all() |
120 | - except: | 120 | + except Exception: |
121 | raise | 121 | raise |
122 | else: | 122 | else: |
123 | n = len(users) | 123 | n = len(users) |
@@ -191,5 +191,5 @@ def main(): | @@ -191,5 +191,5 @@ def main(): | ||
191 | 191 | ||
192 | 192 | ||
193 | # =========================================================================== | 193 | # =========================================================================== |
194 | -if __name__=='__main__': | 194 | +if __name__ == '__main__': |
195 | main() | 195 | main() |
aprendizations/knowledge.py
1 | 1 | ||
2 | # python standard library | 2 | # python standard library |
3 | import random | 3 | import random |
4 | -from datetime import datetime | 4 | +from datetime import datetime |
5 | import logging | 5 | import logging |
6 | 6 | ||
7 | # libraries | 7 | # libraries |
@@ -13,6 +13,7 @@ import networkx as nx | @@ -13,6 +13,7 @@ import networkx as nx | ||
13 | # setup logger for this module | 13 | # setup logger for this module |
14 | logger = logging.getLogger(__name__) | 14 | logger = logging.getLogger(__name__) |
15 | 15 | ||
16 | + | ||
16 | # ---------------------------------------------------------------------------- | 17 | # ---------------------------------------------------------------------------- |
17 | # kowledge state of each student....?? | 18 | # kowledge state of each student....?? |
18 | # Contains: | 19 | # Contains: |
@@ -25,16 +26,15 @@ class StudentKnowledge(object): | @@ -25,16 +26,15 @@ class StudentKnowledge(object): | ||
25 | # methods that update state | 26 | # methods that update state |
26 | # ======================================================================= | 27 | # ======================================================================= |
27 | def __init__(self, deps, factory, state={}): | 28 | def __init__(self, deps, factory, state={}): |
28 | - self.deps = deps # dependency graph shared among students | 29 | + self.deps = deps # shared dependency graph |
29 | self.factory = factory # question factory | 30 | self.factory = factory # question factory |
30 | - self.state = state # {'topic': {'level':0.5, 'date': datetime}, ...} | 31 | + self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...} |
31 | 32 | ||
32 | self.update_topic_levels() # applies forgetting factor | 33 | self.update_topic_levels() # applies forgetting factor |
33 | - self.topic_sequence = self.recommend_topic_sequence() # ['a', 'b', ...] | ||
34 | - self.unlock_topics() # whose dependencies have been completed | 34 | + self.topic_sequence = self.recommend_topic_sequence() # ['ref1', ...] |
35 | + self.unlock_topics() # whose dependencies have been completed | ||
35 | self.current_topic = None | 36 | self.current_topic = None |
36 | 37 | ||
37 | - | ||
38 | # ------------------------------------------------------------------------ | 38 | # ------------------------------------------------------------------------ |
39 | # Updates the proficiency levels of the topics, with forgetting factor | 39 | # Updates the proficiency levels of the topics, with forgetting factor |
40 | # FIXME no dependencies are considered yet... | 40 | # FIXME no dependencies are considered yet... |
@@ -45,7 +45,6 @@ class StudentKnowledge(object): | @@ -45,7 +45,6 @@ class StudentKnowledge(object): | ||
45 | dt = now - s['date'] | 45 | dt = now - s['date'] |
46 | s['level'] *= 0.95 ** dt.days # forgetting factor 0.95 FIXME | 46 | s['level'] *= 0.95 ** dt.days # forgetting factor 0.95 FIXME |
47 | 47 | ||
48 | - | ||
49 | # ------------------------------------------------------------------------ | 48 | # ------------------------------------------------------------------------ |
50 | # Unlock topics whose dependencies are satisfied (> min_level) | 49 | # Unlock topics whose dependencies are satisfied (> min_level) |
51 | # ------------------------------------------------------------------------ | 50 | # ------------------------------------------------------------------------ |
@@ -57,15 +56,15 @@ class StudentKnowledge(object): | @@ -57,15 +56,15 @@ class StudentKnowledge(object): | ||
57 | for topic in self.topic_sequence: | 56 | for topic in self.topic_sequence: |
58 | if topic not in self.state: # if locked | 57 | if topic not in self.state: # if locked |
59 | pred = self.deps.predecessors(topic) | 58 | pred = self.deps.predecessors(topic) |
60 | - if all(d in self.state and self.state[d]['level'] > min_level for d in pred): | ||
61 | - # all dependencies are done | 59 | + if all(d in self.state and self.state[d]['level'] > min_level |
60 | + for d in pred): # all dependencies are done | ||
61 | + | ||
62 | self.state[topic] = { | 62 | self.state[topic] = { |
63 | 'level': 0.0, # unlocked | 63 | 'level': 0.0, # unlocked |
64 | 'date': datetime.now() | 64 | 'date': datetime.now() |
65 | } | 65 | } |
66 | logger.debug(f'Unlocked "{topic}".') | 66 | logger.debug(f'Unlocked "{topic}".') |
67 | 67 | ||
68 | - | ||
69 | # ------------------------------------------------------------------------ | 68 | # ------------------------------------------------------------------------ |
70 | # Start a new topic. | 69 | # Start a new topic. |
71 | # questions: list of generated questions to do in the topic | 70 | # questions: list of generated questions to do in the topic |
@@ -74,6 +73,8 @@ class StudentKnowledge(object): | @@ -74,6 +73,8 @@ class StudentKnowledge(object): | ||
74 | # FIXME async mas nao tem awaits... | 73 | # FIXME async mas nao tem awaits... |
75 | async def start_topic(self, topic): | 74 | async def start_topic(self, topic): |
76 | logger.debug(f'StudentKnowledge.start_topic({topic})') | 75 | logger.debug(f'StudentKnowledge.start_topic({topic})') |
76 | + if self.current_topic == topic: | ||
77 | + return False | ||
77 | 78 | ||
78 | # do not allow locked topics | 79 | # do not allow locked topics |
79 | if self.is_locked(topic): | 80 | if self.is_locked(topic): |
@@ -94,8 +95,8 @@ class StudentKnowledge(object): | @@ -94,8 +95,8 @@ class StudentKnowledge(object): | ||
94 | logger.debug(f'Questions: {", ".join(questions)}') | 95 | logger.debug(f'Questions: {", ".join(questions)}') |
95 | 96 | ||
96 | # generate instances of questions | 97 | # generate instances of questions |
97 | - gen = lambda qref: self.factory[qref].generate() | ||
98 | - self.questions = [gen(qref) for qref in questions] | 98 | + # gen = lambda qref: self.factory[qref].generate() |
99 | + self.questions = [self.factory[qref].generate() for qref in questions] | ||
99 | # self.questions = [gen(qref) for qref in questions] | 100 | # self.questions = [gen(qref) for qref in questions] |
100 | logger.debug(f'Total: {len(self.questions)} questions') | 101 | logger.debug(f'Total: {len(self.questions)} questions') |
101 | 102 | ||
@@ -103,7 +104,6 @@ class StudentKnowledge(object): | @@ -103,7 +104,6 @@ class StudentKnowledge(object): | ||
103 | self.next_question() | 104 | self.next_question() |
104 | return True | 105 | return True |
105 | 106 | ||
106 | - | ||
107 | # ------------------------------------------------------------------------ | 107 | # ------------------------------------------------------------------------ |
108 | # The topic has finished and there are no more questions. | 108 | # The topic has finished and there are no more questions. |
109 | # The topic level is updated in state and unlocks are performed. | 109 | # The topic level is updated in state and unlocks are performed. |
@@ -114,11 +114,11 @@ class StudentKnowledge(object): | @@ -114,11 +114,11 @@ class StudentKnowledge(object): | ||
114 | 114 | ||
115 | self.state[self.current_topic] = { | 115 | self.state[self.current_topic] = { |
116 | 'date': datetime.now(), | 116 | 'date': datetime.now(), |
117 | - 'level': self.correct_answers / (self.correct_answers + self.wrong_answers) | 117 | + 'level': self.correct_answers / (self.correct_answers + |
118 | + self.wrong_answers) | ||
118 | } | 119 | } |
119 | self.unlock_topics() | 120 | self.unlock_topics() |
120 | 121 | ||
121 | - | ||
122 | # ------------------------------------------------------------------------ | 122 | # ------------------------------------------------------------------------ |
123 | # corrects current question with provided answer. | 123 | # corrects current question with provided answer. |
124 | # implements the logic: | 124 | # implements the logic: |
@@ -143,21 +143,19 @@ class StudentKnowledge(object): | @@ -143,21 +143,19 @@ class StudentKnowledge(object): | ||
143 | else: | 143 | else: |
144 | self.wrong_answers += 1 | 144 | self.wrong_answers += 1 |
145 | self.current_question['tries'] -= 1 | 145 | self.current_question['tries'] -= 1 |
146 | - logger.debug(f'Wrong answers = {self.wrong_answers}; Tries = {self.current_question["tries"]}') | ||
147 | 146 | ||
148 | if self.current_question['tries'] > 0: | 147 | if self.current_question['tries'] > 0: |
149 | action = 'try_again' | 148 | action = 'try_again' |
150 | else: | 149 | else: |
151 | action = 'wrong' | 150 | action = 'wrong' |
152 | if self.current_question['append_wrong']: | 151 | if self.current_question['append_wrong']: |
153 | - logger.debug("Appending new instance of this question to the end") | 152 | + logger.debug("Append new instance of question at the end") |
154 | self.questions.append(self.factory[q['ref']].generate()) | 153 | self.questions.append(self.factory[q['ref']].generate()) |
155 | self.next_question() | 154 | self.next_question() |
156 | 155 | ||
157 | # returns corrected question (not new one) which might include comments | 156 | # returns corrected question (not new one) which might include comments |
158 | return q, action | 157 | return q, action |
159 | 158 | ||
160 | - | ||
161 | # ------------------------------------------------------------------------ | 159 | # ------------------------------------------------------------------------ |
162 | # Move to next question | 160 | # Move to next question |
163 | # ------------------------------------------------------------------------ | 161 | # ------------------------------------------------------------------------ |
@@ -170,7 +168,8 @@ class StudentKnowledge(object): | @@ -170,7 +168,8 @@ class StudentKnowledge(object): | ||
170 | else: | 168 | else: |
171 | self.current_question['start_time'] = datetime.now() | 169 | self.current_question['start_time'] = datetime.now() |
172 | default_maxtries = self.deps.nodes[self.current_topic]['max_tries'] | 170 | default_maxtries = self.deps.nodes[self.current_topic]['max_tries'] |
173 | - self.current_question['tries'] = self.current_question.get('max_tries', default_maxtries) | 171 | + maxtries = self.current_question.get('max_tries', default_maxtries) |
172 | + self.current_question['tries'] = maxtries | ||
174 | logger.debug(f'Next question is "{self.current_question["ref"]}"') | 173 | logger.debug(f'Next question is "{self.current_question["ref"]}"') |
175 | 174 | ||
176 | return self.current_question # question or None | 175 | return self.current_question # question or None |
@@ -185,9 +184,21 @@ class StudentKnowledge(object): | @@ -185,9 +184,21 @@ class StudentKnowledge(object): | ||
185 | # ------------------------------------------------------------------------ | 184 | # ------------------------------------------------------------------------ |
186 | # compute recommended sequence of topics ['a', 'b', ...] | 185 | # compute recommended sequence of topics ['a', 'b', ...] |
187 | # ------------------------------------------------------------------------ | 186 | # ------------------------------------------------------------------------ |
188 | - def recommend_topic_sequence(self): | 187 | + def recommend_topic_sequence(self, target=None): |
189 | return list(nx.topological_sort(self.deps)) | 188 | return list(nx.topological_sort(self.deps)) |
190 | 189 | ||
190 | + # if target is None: | ||
191 | + # target = list(nx.topological_sort(self.deps))[-1] | ||
192 | + | ||
193 | + # weights = {} | ||
194 | + # tseq = [target] | ||
195 | + | ||
196 | + # def topic_weights(d, g, n): | ||
197 | + # pred = g.predecessors(n) | ||
198 | + # for p in pred: | ||
199 | + # topic_weights(d, g, p) | ||
200 | + # d[n] = 1 + sum(d[m]) | ||
201 | + | ||
191 | # ------------------------------------------------------------------------ | 202 | # ------------------------------------------------------------------------ |
192 | def get_current_question(self): | 203 | def get_current_question(self): |
193 | return self.current_question | 204 | return self.current_question |
@@ -212,11 +223,12 @@ class StudentKnowledge(object): | @@ -212,11 +223,12 @@ class StudentKnowledge(object): | ||
212 | 'type': self.deps.nodes[ref]['type'], | 223 | 'type': self.deps.nodes[ref]['type'], |
213 | 'name': self.deps.nodes[ref]['name'], | 224 | 'name': self.deps.nodes[ref]['name'], |
214 | 'level': self.state[ref]['level'] if ref in self.state else None | 225 | 'level': self.state[ref]['level'] if ref in self.state else None |
215 | - } for ref in self.topic_sequence ] | 226 | + } for ref in self.topic_sequence] |
216 | 227 | ||
217 | # ------------------------------------------------------------------------ | 228 | # ------------------------------------------------------------------------ |
218 | def get_topic_progress(self): | 229 | def get_topic_progress(self): |
219 | - return self.correct_answers / (1 + self.correct_answers + len(self.questions)) | 230 | + return self.correct_answers / (1 + self.correct_answers + |
231 | + len(self.questions)) | ||
220 | 232 | ||
221 | # ------------------------------------------------------------------------ | 233 | # ------------------------------------------------------------------------ |
222 | def get_topic_level(self, topic): | 234 | def get_topic_level(self, topic): |
@@ -231,4 +243,3 @@ class StudentKnowledge(object): | @@ -231,4 +243,3 @@ class StudentKnowledge(object): | ||
231 | # ------------------------------------------------------------------------ | 243 | # ------------------------------------------------------------------------ |
232 | # def get_recommended_topic(self): # FIXME untested | 244 | # def get_recommended_topic(self): # FIXME untested |
233 | # return min(self.state.items(), key=lambda x: x[1]['level'])[0] | 245 | # return min(self.state.items(), key=lambda x: x[1]['level'])[0] |
234 | - |
aprendizations/learnapp.py
@@ -28,7 +28,8 @@ logger = logging.getLogger(__name__) | @@ -28,7 +28,8 @@ logger = logging.getLogger(__name__) | ||
28 | async def _bcrypt_hash(a, b): | 28 | async def _bcrypt_hash(a, b): |
29 | # loop = asyncio.get_running_loop() # FIXME python 3.7 only | 29 | # loop = asyncio.get_running_loop() # FIXME python 3.7 only |
30 | loop = asyncio.get_event_loop() | 30 | loop = asyncio.get_event_loop() |
31 | - return await loop.run_in_executor(None, bcrypt.hashpw, a.encode('utf-8'), b) | 31 | + return await loop.run_in_executor(None, bcrypt.hashpw, |
32 | + a.encode('utf-8'), b) | ||
32 | 33 | ||
33 | 34 | ||
34 | async def check_password(try_pw, pw): | 35 | async def check_password(try_pw, pw): |
@@ -77,7 +78,9 @@ class LearnApp(object): | @@ -77,7 +78,9 @@ class LearnApp(object): | ||
77 | async def login(self, uid, try_pw): | 78 | async def login(self, uid, try_pw): |
78 | with self.db_session() as s: | 79 | with self.db_session() as s: |
79 | try: | 80 | try: |
80 | - name, password = s.query(Student.name, Student.password).filter_by(id=uid).one() | 81 | + name, password = s.query(Student.name, Student.password) \ |
82 | + .filter_by(id=uid) \ | ||
83 | + .one() | ||
81 | except Exception: | 84 | except Exception: |
82 | logger.info(f'User "{uid}" does not exist') | 85 | logger.info(f'User "{uid}" does not exist') |
83 | return False | 86 | return False |
@@ -103,7 +106,8 @@ class LearnApp(object): | @@ -103,7 +106,8 @@ class LearnApp(object): | ||
103 | self.online[uid] = { | 106 | self.online[uid] = { |
104 | 'number': uid, | 107 | 'number': uid, |
105 | 'name': name, | 108 | 'name': name, |
106 | - 'state': StudentKnowledge(deps=self.deps, factory=self.factory, state=state), | 109 | + 'state': StudentKnowledge(deps=self.deps, factory=self.factory, |
110 | + state=state), | ||
107 | 'counter': counter + 1, # counts simultaneous logins | 111 | 'counter': counter + 1, # counts simultaneous logins |
108 | } | 112 | } |
109 | 113 | ||
@@ -162,14 +166,17 @@ class LearnApp(object): | @@ -162,14 +166,17 @@ class LearnApp(object): | ||
162 | date = str(knowledge.get_topic_date(topic)) | 166 | date = str(knowledge.get_topic_date(topic)) |
163 | 167 | ||
164 | with self.db_session() as s: | 168 | with self.db_session() as s: |
165 | - a = s.query(StudentTopic).filter_by(student_id=uid, topic_id=topic).one_or_none() | 169 | + a = s.query(StudentTopic) \ |
170 | + .filter_by(student_id=uid, topic_id=topic) \ | ||
171 | + .one_or_none() | ||
166 | if a is None: | 172 | if a is None: |
167 | # insert new studenttopic into database | 173 | # insert new studenttopic into database |
168 | logger.debug('Database insert new studenttopic') | 174 | logger.debug('Database insert new studenttopic') |
169 | t = s.query(Topic).get(topic) | 175 | t = s.query(Topic).get(topic) |
170 | u = s.query(Student).get(uid) | 176 | u = s.query(Student).get(uid) |
171 | # association object | 177 | # association object |
172 | - a = StudentTopic(level=level, date=date, topic=t, student=u) | 178 | + a = StudentTopic(level=level, date=date, topic=t, |
179 | + student=u) | ||
173 | u.topics.append(a) | 180 | u.topics.append(a) |
174 | else: | 181 | else: |
175 | # update studenttopic in database | 182 | # update studenttopic in database |
@@ -204,7 +211,7 @@ class LearnApp(object): | @@ -204,7 +211,7 @@ class LearnApp(object): | ||
204 | missing_topics = [Topic(id=t) for t in topics if t not in dbtopics] | 211 | missing_topics = [Topic(id=t) for t in topics if t not in dbtopics] |
205 | if missing_topics: | 212 | if missing_topics: |
206 | s.add_all(missing_topics) | 213 | s.add_all(missing_topics) |
207 | - logger.info(f'Added {len(missing_topics)} new topics to the database') | 214 | + logger.info(f'Added {len(missing_topics)} new topics') |
208 | 215 | ||
209 | # ------------------------------------------------------------------------ | 216 | # ------------------------------------------------------------------------ |
210 | # setup and check database | 217 | # setup and check database |
@@ -262,7 +269,8 @@ class LearnApp(object): | @@ -262,7 +269,8 @@ class LearnApp(object): | ||
262 | t['file'] = attr.get('file', default_file) # questions.yaml | 269 | t['file'] = attr.get('file', default_file) # questions.yaml |
263 | t['shuffle'] = attr.get('shuffle', default_shuffle) | 270 | t['shuffle'] = attr.get('shuffle', default_shuffle) |
264 | t['max_tries'] = attr.get('max_tries', default_maxtries) | 271 | t['max_tries'] = attr.get('max_tries', default_maxtries) |
265 | - t['forgetting_factor'] = attr.get('forgetting_factor', default_forgetting_factor) | 272 | + t['forgetting_factor'] = attr.get('forgetting_factor', |
273 | + default_forgetting_factor) | ||
266 | t['choose'] = attr.get('choose', default_choose) | 274 | t['choose'] = attr.get('choose', default_choose) |
267 | t['append_wrong'] = attr.get('append_wrong', default_append_wrong) | 275 | t['append_wrong'] = attr.get('append_wrong', default_append_wrong) |
268 | t['questions'] = attr.get('questions', []) | 276 | t['questions'] = attr.get('questions', []) |
@@ -347,10 +355,6 @@ class LearnApp(object): | @@ -347,10 +355,6 @@ class LearnApp(object): | ||
347 | def get_topic_name(self, ref): | 355 | def get_topic_name(self, ref): |
348 | return self.deps.node[ref]['name'] | 356 | return self.deps.node[ref]['name'] |
349 | 357 | ||
350 | - # # ------------------------------------------------------------------------ | ||
351 | - # def get_topic_type(self, ref): | ||
352 | - # return self.deps.node[ref]['type'] | ||
353 | - | ||
354 | # ------------------------------------------------------------------------ | 358 | # ------------------------------------------------------------------------ |
355 | def get_current_public_dir(self, uid): | 359 | def get_current_public_dir(self, uid): |
356 | topic = self.online[uid]['state'].get_current_topic() | 360 | topic = self.online[uid]['state'].get_current_topic() |
aprendizations/models.py
1 | -# FIXME see https://stackoverflow.com/questions/38248415/many-to-many-with-association-object-and-all-relationships-defined-crashes-on-de | ||
2 | -# And fix the association StudentTopic etc | ||
3 | 1 | ||
4 | - | ||
5 | - | ||
6 | -from sqlalchemy import Table, Column, ForeignKey, Integer, Float, String, DateTime | 2 | +from sqlalchemy import Column, ForeignKey, Integer, Float, String |
7 | from sqlalchemy.ext.declarative import declarative_base | 3 | from sqlalchemy.ext.declarative import declarative_base |
8 | from sqlalchemy.orm import relationship | 4 | from sqlalchemy.orm import relationship |
9 | 5 | ||
@@ -12,13 +8,14 @@ from sqlalchemy.orm import relationship | @@ -12,13 +8,14 @@ from sqlalchemy.orm import relationship | ||
12 | # Declare ORM | 8 | # Declare ORM |
13 | Base = declarative_base() | 9 | Base = declarative_base() |
14 | 10 | ||
11 | + | ||
15 | # --------------------------------------------------------------------------- | 12 | # --------------------------------------------------------------------------- |
16 | class StudentTopic(Base): | 13 | class StudentTopic(Base): |
17 | __tablename__ = 'studenttopic' | 14 | __tablename__ = 'studenttopic' |
18 | student_id = Column(String, ForeignKey('students.id'), primary_key=True) | 15 | student_id = Column(String, ForeignKey('students.id'), primary_key=True) |
19 | topic_id = Column(String, ForeignKey('topics.id'), primary_key=True) | 16 | topic_id = Column(String, ForeignKey('topics.id'), primary_key=True) |
20 | level = Column(Float) | 17 | level = Column(Float) |
21 | - date = Column(String) | 18 | + date = Column(String) |
22 | 19 | ||
23 | # --- | 20 | # --- |
24 | student = relationship('Student', back_populates='topics') | 21 | student = relationship('Student', back_populates='topics') |
@@ -31,6 +28,7 @@ class StudentTopic(Base): | @@ -31,6 +28,7 @@ class StudentTopic(Base): | ||
31 | level: "{self.level}" | 28 | level: "{self.level}" |
32 | date: "{self.date}"''' | 29 | date: "{self.date}"''' |
33 | 30 | ||
31 | + | ||
34 | # --------------------------------------------------------------------------- | 32 | # --------------------------------------------------------------------------- |
35 | # Registered students | 33 | # Registered students |
36 | # --------------------------------------------------------------------------- | 34 | # --------------------------------------------------------------------------- |
@@ -50,12 +48,13 @@ class Student(Base): | @@ -50,12 +48,13 @@ class Student(Base): | ||
50 | name: "{self.name}" | 48 | name: "{self.name}" |
51 | password: "{self.password}"''' | 49 | password: "{self.password}"''' |
52 | 50 | ||
51 | + | ||
53 | # --------------------------------------------------------------------------- | 52 | # --------------------------------------------------------------------------- |
54 | # Table with every answer given | 53 | # Table with every answer given |
55 | # --------------------------------------------------------------------------- | 54 | # --------------------------------------------------------------------------- |
56 | class Answer(Base): | 55 | class Answer(Base): |
57 | __tablename__ = 'answers' | 56 | __tablename__ = 'answers' |
58 | - id = Column(Integer, primary_key=True) # auto_increment | 57 | + id = Column(Integer, primary_key=True) # auto_increment |
59 | ref = Column(String) | 58 | ref = Column(String) |
60 | grade = Column(Float) | 59 | grade = Column(Float) |
61 | starttime = Column(String) | 60 | starttime = Column(String) |
@@ -77,6 +76,7 @@ class Answer(Base): | @@ -77,6 +76,7 @@ class Answer(Base): | ||
77 | student_id: "{self.student_id}" | 76 | student_id: "{self.student_id}" |
78 | topic_id: "{self.topic_id}"''' | 77 | topic_id: "{self.topic_id}"''' |
79 | 78 | ||
79 | + | ||
80 | # --------------------------------------------------------------------------- | 80 | # --------------------------------------------------------------------------- |
81 | # Table with student state | 81 | # Table with student state |
82 | # --------------------------------------------------------------------------- | 82 | # --------------------------------------------------------------------------- |
aprendizations/questions.py
@@ -43,12 +43,12 @@ class Question(dict): | @@ -43,12 +43,12 @@ class Question(dict): | ||
43 | 'files': {}, | 43 | 'files': {}, |
44 | }) | 44 | }) |
45 | 45 | ||
46 | - def correct(self): | 46 | + def correct(self) -> float: |
47 | self['comments'] = '' | 47 | self['comments'] = '' |
48 | self['grade'] = 0.0 | 48 | self['grade'] = 0.0 |
49 | return 0.0 | 49 | return 0.0 |
50 | 50 | ||
51 | - async def correct_async(self): | 51 | + async def correct_async(self) -> float: |
52 | # loop = asyncio.get_running_loop() # FIXME python 3.7 only | 52 | # loop = asyncio.get_running_loop() # FIXME python 3.7 only |
53 | loop = asyncio.get_event_loop() | 53 | loop = asyncio.get_event_loop() |
54 | grade = await loop.run_in_executor(None, self.correct) | 54 | grade = await loop.run_in_executor(None, self.correct) |
@@ -56,7 +56,7 @@ class Question(dict): | @@ -56,7 +56,7 @@ class Question(dict): | ||
56 | 56 | ||
57 | def set_defaults(self, d): | 57 | def set_defaults(self, d): |
58 | 'Add k:v pairs from default dict d for nonexistent keys' | 58 | 'Add k:v pairs from default dict d for nonexistent keys' |
59 | - for k,v in d.items(): | 59 | + for k, v in d.items(): |
60 | self.setdefault(k, v) | 60 | self.setdefault(k, v) |
61 | 61 | ||
62 | 62 | ||
@@ -73,7 +73,7 @@ class QuestionRadio(Question): | @@ -73,7 +73,7 @@ class QuestionRadio(Question): | ||
73 | choose (int) # only used if shuffle=True | 73 | choose (int) # only used if shuffle=True |
74 | ''' | 74 | ''' |
75 | 75 | ||
76 | - #------------------------------------------------------------------------ | 76 | + # ------------------------------------------------------------------------ |
77 | def __init__(self, q): | 77 | def __init__(self, q): |
78 | super().__init__(q) | 78 | super().__init__(q) |
79 | 79 | ||
@@ -90,7 +90,8 @@ class QuestionRadio(Question): | @@ -90,7 +90,8 @@ class QuestionRadio(Question): | ||
90 | # always convert to list, e.g. correct: 2 --> correct: [0,0,1,0,0] | 90 | # always convert to list, e.g. correct: 2 --> correct: [0,0,1,0,0] |
91 | # correctness levels from 0.0 to 1.0 (no discount here!) | 91 | # correctness levels from 0.0 to 1.0 (no discount here!) |
92 | if isinstance(self['correct'], int): | 92 | if isinstance(self['correct'], int): |
93 | - self['correct'] = [1.0 if x==self['correct'] else 0.0 for x in range(n)] | 93 | + self['correct'] = [1.0 if x == self['correct'] else 0.0 |
94 | + for x in range(n)] | ||
94 | 95 | ||
95 | if self['shuffle']: | 96 | if self['shuffle']: |
96 | # separate right from wrong options | 97 | # separate right from wrong options |
@@ -101,8 +102,8 @@ class QuestionRadio(Question): | @@ -101,8 +102,8 @@ class QuestionRadio(Question): | ||
101 | 102 | ||
102 | # choose 1 correct option | 103 | # choose 1 correct option |
103 | r = random.choice(right) | 104 | r = random.choice(right) |
104 | - options = [ self['options'][r] ] | ||
105 | - correct = [ 1.0 ] | 105 | + options = [self['options'][r]] |
106 | + correct = [1.0] | ||
106 | 107 | ||
107 | # choose remaining wrong options | 108 | # choose remaining wrong options |
108 | random.shuffle(wrong) | 109 | random.shuffle(wrong) |
@@ -112,10 +113,10 @@ class QuestionRadio(Question): | @@ -112,10 +113,10 @@ class QuestionRadio(Question): | ||
112 | 113 | ||
113 | # final shuffle of the options | 114 | # final shuffle of the options |
114 | perm = random.sample(range(self['choose']), self['choose']) | 115 | perm = random.sample(range(self['choose']), self['choose']) |
115 | - self['options'] = [ str(options[i]) for i in perm ] | ||
116 | - self['correct'] = [ float(correct[i]) for i in perm ] | 116 | + self['options'] = [str(options[i]) for i in perm] |
117 | + self['correct'] = [float(correct[i]) for i in perm] | ||
117 | 118 | ||
118 | - #------------------------------------------------------------------------ | 119 | + # ------------------------------------------------------------------------ |
119 | # can return negative values for wrong answers | 120 | # can return negative values for wrong answers |
120 | def correct(self): | 121 | def correct(self): |
121 | super().correct() | 122 | super().correct() |
@@ -144,7 +145,7 @@ class QuestionCheckbox(Question): | @@ -144,7 +145,7 @@ class QuestionCheckbox(Question): | ||
144 | answer (None or an actual answer) | 145 | answer (None or an actual answer) |
145 | ''' | 146 | ''' |
146 | 147 | ||
147 | - #------------------------------------------------------------------------ | 148 | + # ------------------------------------------------------------------------ |
148 | def __init__(self, q): | 149 | def __init__(self, q): |
149 | super().__init__(q) | 150 | super().__init__(q) |
150 | 151 | ||
@@ -153,24 +154,25 @@ class QuestionCheckbox(Question): | @@ -153,24 +154,25 @@ class QuestionCheckbox(Question): | ||
153 | # set defaults if missing | 154 | # set defaults if missing |
154 | self.set_defaults({ | 155 | self.set_defaults({ |
155 | 'text': '', | 156 | 'text': '', |
156 | - 'correct': [1.0] * n, # Using 0.0 breaks the (right, wrong) options | 157 | + 'correct': [1.0] * n, # Using 0.0 breaks (right, wrong) options |
157 | 'shuffle': True, | 158 | 'shuffle': True, |
158 | 'discount': True, | 159 | 'discount': True, |
159 | 'choose': n, # number of options | 160 | 'choose': n, # number of options |
160 | }) | 161 | }) |
161 | 162 | ||
162 | if len(self['correct']) != n: | 163 | if len(self['correct']) != n: |
163 | - logger.error(f'Options and correct size mismatch in "{self["ref"]}", file "{self["filename"]}".') | 164 | + logger.error(f'Options and correct size mismatch in ' |
165 | + f'"{self["ref"]}", file "{self["filename"]}".') | ||
164 | 166 | ||
165 | # if an option is a list of (right, wrong), pick one | 167 | # if an option is a list of (right, wrong), pick one |
166 | # FIXME it's possible that all options are chosen wrong | 168 | # FIXME it's possible that all options are chosen wrong |
167 | options = [] | 169 | options = [] |
168 | correct = [] | 170 | correct = [] |
169 | - for o,c in zip(self['options'], self['correct']): | 171 | + for o, c in zip(self['options'], self['correct']): |
170 | if isinstance(o, list): | 172 | if isinstance(o, list): |
171 | - r = random.randint(0,1) | 173 | + r = random.randint(0, 1) |
172 | o = o[r] | 174 | o = o[r] |
173 | - c = c if r==0 else -c | 175 | + c = c if r == 0 else -c |
174 | options.append(str(o)) | 176 | options.append(str(o)) |
175 | correct.append(float(c)) | 177 | correct.append(float(c)) |
176 | 178 | ||
@@ -181,7 +183,7 @@ class QuestionCheckbox(Question): | @@ -181,7 +183,7 @@ class QuestionCheckbox(Question): | ||
181 | self['options'] = [options[i] for i in perm] | 183 | self['options'] = [options[i] for i in perm] |
182 | self['correct'] = [correct[i] for i in perm] | 184 | self['correct'] = [correct[i] for i in perm] |
183 | 185 | ||
184 | - #------------------------------------------------------------------------ | 186 | + # ------------------------------------------------------------------------ |
185 | # can return negative values for wrong answers | 187 | # can return negative values for wrong answers |
186 | def correct(self): | 188 | def correct(self): |
187 | super().correct() | 189 | super().correct() |
@@ -215,7 +217,7 @@ class QuestionText(Question): | @@ -215,7 +217,7 @@ class QuestionText(Question): | ||
215 | answer (None or an actual answer) | 217 | answer (None or an actual answer) |
216 | ''' | 218 | ''' |
217 | 219 | ||
218 | - #------------------------------------------------------------------------ | 220 | + # ------------------------------------------------------------------------ |
219 | def __init__(self, q): | 221 | def __init__(self, q): |
220 | super().__init__(q) | 222 | super().__init__(q) |
221 | 223 | ||
@@ -231,7 +233,7 @@ class QuestionText(Question): | @@ -231,7 +233,7 @@ class QuestionText(Question): | ||
231 | # make sure all elements of the list are strings | 233 | # make sure all elements of the list are strings |
232 | self['correct'] = [str(a) for a in self['correct']] | 234 | self['correct'] = [str(a) for a in self['correct']] |
233 | 235 | ||
234 | - #------------------------------------------------------------------------ | 236 | + # ------------------------------------------------------------------------ |
235 | # can return negative values for wrong answers | 237 | # can return negative values for wrong answers |
236 | def correct(self): | 238 | def correct(self): |
237 | super().correct() | 239 | super().correct() |
@@ -251,7 +253,7 @@ class QuestionTextRegex(Question): | @@ -251,7 +253,7 @@ class QuestionTextRegex(Question): | ||
251 | answer (None or an actual answer) | 253 | answer (None or an actual answer) |
252 | ''' | 254 | ''' |
253 | 255 | ||
254 | - #------------------------------------------------------------------------ | 256 | + # ------------------------------------------------------------------------ |
255 | def __init__(self, q): | 257 | def __init__(self, q): |
256 | super().__init__(q) | 258 | super().__init__(q) |
257 | 259 | ||
@@ -260,15 +262,17 @@ class QuestionTextRegex(Question): | @@ -260,15 +262,17 @@ class QuestionTextRegex(Question): | ||
260 | 'correct': '$.^', # will always return false | 262 | 'correct': '$.^', # will always return false |
261 | }) | 263 | }) |
262 | 264 | ||
263 | - #------------------------------------------------------------------------ | 265 | + # ------------------------------------------------------------------------ |
264 | # can return negative values for wrong answers | 266 | # can return negative values for wrong answers |
265 | def correct(self): | 267 | def correct(self): |
266 | super().correct() | 268 | super().correct() |
267 | if self['answer'] is not None: | 269 | if self['answer'] is not None: |
268 | try: | 270 | try: |
269 | - self['grade'] = 1.0 if re.match(self['correct'], self['answer']) else 0.0 | 271 | + ok = re.match(self['correct'], self['answer']) |
270 | except TypeError: | 272 | except TypeError: |
271 | - logger.error('While matching regex {self["correct"]} with answer {self["answer"]}.') | 273 | + logger.error(f'While matching regex {self["correct"]} with ' |
274 | + f'answer {self["answer"]}.') | ||
275 | + self['grade'] = 1.0 if ok else 0.0 | ||
272 | 276 | ||
273 | return self['grade'] | 277 | return self['grade'] |
274 | 278 | ||
@@ -283,7 +287,7 @@ class QuestionNumericInterval(Question): | @@ -283,7 +287,7 @@ class QuestionNumericInterval(Question): | ||
283 | An answer is correct if it's in the closed interval. | 287 | An answer is correct if it's in the closed interval. |
284 | ''' | 288 | ''' |
285 | 289 | ||
286 | - #------------------------------------------------------------------------ | 290 | + # ------------------------------------------------------------------------ |
287 | def __init__(self, q): | 291 | def __init__(self, q): |
288 | super().__init__(q) | 292 | super().__init__(q) |
289 | 293 | ||
@@ -292,7 +296,7 @@ class QuestionNumericInterval(Question): | @@ -292,7 +296,7 @@ class QuestionNumericInterval(Question): | ||
292 | 'correct': [1.0, -1.0], # will always return false | 296 | 'correct': [1.0, -1.0], # will always return false |
293 | }) | 297 | }) |
294 | 298 | ||
295 | - #------------------------------------------------------------------------ | 299 | + # ------------------------------------------------------------------------ |
296 | # can return negative values for wrong answers | 300 | # can return negative values for wrong answers |
297 | def correct(self): | 301 | def correct(self): |
298 | super().correct() | 302 | super().correct() |
@@ -302,7 +306,8 @@ class QuestionNumericInterval(Question): | @@ -302,7 +306,8 @@ class QuestionNumericInterval(Question): | ||
302 | try: # replace , by . and convert to float | 306 | try: # replace , by . and convert to float |
303 | answer = float(self['answer'].replace(',', '.', 1)) | 307 | answer = float(self['answer'].replace(',', '.', 1)) |
304 | except ValueError: | 308 | except ValueError: |
305 | - self['comments'] = 'A resposta tem de ser numérica, por exemplo 12.345.' | 309 | + self['comments'] = ('A resposta tem de ser numérica, ' |
310 | + 'por exemplo 12.345.') | ||
306 | self['grade'] = 0.0 | 311 | self['grade'] = 0.0 |
307 | else: | 312 | else: |
308 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 | 313 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 |
@@ -320,7 +325,7 @@ class QuestionTextArea(Question): | @@ -320,7 +325,7 @@ class QuestionTextArea(Question): | ||
320 | lines (int) | 325 | lines (int) |
321 | ''' | 326 | ''' |
322 | 327 | ||
323 | - #------------------------------------------------------------------------ | 328 | + # ------------------------------------------------------------------------ |
324 | def __init__(self, q): | 329 | def __init__(self, q): |
325 | super().__init__(q) | 330 | super().__init__(q) |
326 | 331 | ||
@@ -331,10 +336,9 @@ class QuestionTextArea(Question): | @@ -331,10 +336,9 @@ class QuestionTextArea(Question): | ||
331 | 'correct': '' # trying to execute this will fail => grade 0.0 | 336 | 'correct': '' # trying to execute this will fail => grade 0.0 |
332 | }) | 337 | }) |
333 | 338 | ||
334 | - # self['correct'] = path.join(self['path'], self['correct']) | ||
335 | - self['correct'] = path.abspath(path.normpath(path.join(self['path'], self['correct']))) | 339 | + self['correct'] = path.join(self['path'], self['correct']) # FIXME |
336 | 340 | ||
337 | - #------------------------------------------------------------------------ | 341 | + # ------------------------------------------------------------------------ |
338 | # can return negative values for wrong answers | 342 | # can return negative values for wrong answers |
339 | def correct(self): | 343 | def correct(self): |
340 | super().correct() | 344 | super().correct() |
@@ -347,31 +351,30 @@ class QuestionTextArea(Question): | @@ -347,31 +351,30 @@ class QuestionTextArea(Question): | ||
347 | timeout=self['timeout'] | 351 | timeout=self['timeout'] |
348 | ) | 352 | ) |
349 | 353 | ||
350 | - if type(out) in (int, float): | ||
351 | - self['grade'] = float(out) | ||
352 | - | ||
353 | - elif isinstance(out, dict): | 354 | + if isinstance(out, dict): |
354 | self['comments'] = out.get('comments', '') | 355 | self['comments'] = out.get('comments', '') |
355 | try: | 356 | try: |
356 | self['grade'] = float(out['grade']) | 357 | self['grade'] = float(out['grade']) |
357 | except ValueError: | 358 | except ValueError: |
358 | - logger.error(f'Correction script of "{self["ref"]}" returned nonfloat.') | 359 | + logger.error(f'Output error in "{self["correct"]}".') |
359 | except KeyError: | 360 | except KeyError: |
360 | - logger.error('Correction script of "{self["ref"]}" returned no "grade".') | 361 | + logger.error(f'No grade in "{self["correct"]}".') |
362 | + else: | ||
363 | + self['grade'] = float(out) | ||
361 | 364 | ||
362 | return self['grade'] | 365 | return self['grade'] |
363 | 366 | ||
364 | 367 | ||
365 | # =========================================================================== | 368 | # =========================================================================== |
366 | class QuestionInformation(Question): | 369 | class QuestionInformation(Question): |
367 | - #------------------------------------------------------------------------ | 370 | + # ------------------------------------------------------------------------ |
368 | def __init__(self, q): | 371 | def __init__(self, q): |
369 | super().__init__(q) | 372 | super().__init__(q) |
370 | self.set_defaults({ | 373 | self.set_defaults({ |
371 | 'text': '', | 374 | 'text': '', |
372 | }) | 375 | }) |
373 | 376 | ||
374 | - #------------------------------------------------------------------------ | 377 | + # ------------------------------------------------------------------------ |
375 | # can return negative values for wrong answers | 378 | # can return negative values for wrong answers |
376 | def correct(self): | 379 | def correct(self): |
377 | super().correct() | 380 | super().correct() |
aprendizations/redirect.py
@@ -4,6 +4,7 @@ from tornado.web import RedirectHandler, Application | @@ -4,6 +4,7 @@ from tornado.web import RedirectHandler, Application | ||
4 | from tornado.ioloop import IOLoop | 4 | from tornado.ioloop import IOLoop |
5 | import argparse | 5 | import argparse |
6 | 6 | ||
7 | + | ||
7 | def main(): | 8 | def main(): |
8 | default_url = 'https://bit.xdi.uevora.pt/' | 9 | default_url = 'https://bit.xdi.uevora.pt/' |
9 | default_port = 8080 | 10 | default_port = 8080 |
aprendizations/serve.py
@@ -381,7 +381,8 @@ def parse_cmdline_arguments(): | @@ -381,7 +381,8 @@ def parse_cmdline_arguments(): | ||
381 | 381 | ||
382 | return argparser.parse_args() | 382 | return argparser.parse_args() |
383 | 383 | ||
384 | -# ------------------------------------------------------------------------- | 384 | + |
385 | +# ---------------------------------------------------------------------------- | ||
385 | def get_logger_config(debug=False): | 386 | def get_logger_config(debug=False): |
386 | if debug: | 387 | if debug: |
387 | filename = 'logger-debug.yaml' | 388 | filename = 'logger-debug.yaml' |
@@ -397,7 +398,8 @@ def get_logger_config(debug=False): | @@ -397,7 +398,8 @@ def get_logger_config(debug=False): | ||
397 | 'version': 1, | 398 | 'version': 1, |
398 | 'formatters': { | 399 | 'formatters': { |
399 | 'standard': { | 400 | 'standard': { |
400 | - 'format': '%(asctime)s %(name)-24s %(levelname)-8s %(message)s', | 401 | + 'format': '%(asctime)s %(name)-24s %(levelname)-8s ' |
402 | + '%(message)s', | ||
401 | 'datefmt': '%H:%M:%S', | 403 | 'datefmt': '%H:%M:%S', |
402 | }, | 404 | }, |
403 | }, | 405 | }, |
aprendizations/tools.py
@@ -23,11 +23,13 @@ logger = logging.getLogger(__name__) | @@ -23,11 +23,13 @@ logger = logging.getLogger(__name__) | ||
23 | # ------------------------------------------------------------------------- | 23 | # ------------------------------------------------------------------------- |
24 | class MathBlockGrammar(mistune.BlockGrammar): | 24 | class MathBlockGrammar(mistune.BlockGrammar): |
25 | block_math = re.compile(r'^\$\$(.*?)\$\$', re.DOTALL) | 25 | block_math = re.compile(r'^\$\$(.*?)\$\$', re.DOTALL) |
26 | - latex_environment = re.compile(r'^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}', re.DOTALL) | 26 | + latex_environment = re.compile(r'^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}', |
27 | + re.DOTALL) | ||
27 | 28 | ||
28 | 29 | ||
29 | class MathBlockLexer(mistune.BlockLexer): | 30 | class MathBlockLexer(mistune.BlockLexer): |
30 | - default_rules = ['block_math', 'latex_environment'] + mistune.BlockLexer.default_rules | 31 | + default_rules = ['block_math', 'latex_environment'] \ |
32 | + + mistune.BlockLexer.default_rules | ||
31 | 33 | ||
32 | def __init__(self, rules=None, **kwargs): | 34 | def __init__(self, rules=None, **kwargs): |
33 | if rules is None: | 35 | if rules is None: |
@@ -82,27 +84,29 @@ class MarkdownWithMath(mistune.Markdown): | @@ -82,27 +84,29 @@ class MarkdownWithMath(mistune.Markdown): | ||
82 | return self.renderer.block_math(self.token['text']) | 84 | return self.renderer.block_math(self.token['text']) |
83 | 85 | ||
84 | def output_latex_environment(self): | 86 | def output_latex_environment(self): |
85 | - return self.renderer.latex_environment(self.token['name'], self.token['text']) | ||
86 | - | 87 | + return self.renderer.latex_environment(self.token['name'], |
88 | + self.token['text']) | ||
87 | 89 | ||
88 | 90 | ||
89 | class HighlightRenderer(mistune.Renderer): | 91 | class HighlightRenderer(mistune.Renderer): |
90 | def block_code(self, code, lang='text'): | 92 | def block_code(self, code, lang='text'): |
91 | try: | 93 | try: |
92 | lexer = get_lexer_by_name(lang, stripall=False) | 94 | lexer = get_lexer_by_name(lang, stripall=False) |
93 | - except: | 95 | + except Exception: |
94 | lexer = get_lexer_by_name('text', stripall=False) | 96 | lexer = get_lexer_by_name('text', stripall=False) |
95 | 97 | ||
96 | formatter = HtmlFormatter() | 98 | formatter = HtmlFormatter() |
97 | return highlight(code, lexer, formatter) | 99 | return highlight(code, lexer, formatter) |
98 | 100 | ||
99 | def table(self, header, body): | 101 | def table(self, header, body): |
100 | - return '<table class="table table-sm"><thead class="thead-light">' + header + '</thead><tbody>' + body + "</tbody></table>" | 102 | + return '<table class="table table-sm"><thead class="thead-light">' \ |
103 | + + header + '</thead><tbody>' + body + "</tbody></table>" | ||
101 | 104 | ||
102 | def image(self, src, title, alt): | 105 | def image(self, src, title, alt): |
103 | alt = mistune.escape(alt, quote=True) | 106 | alt = mistune.escape(alt, quote=True) |
104 | title = mistune.escape(title or '', quote=True) | 107 | title = mistune.escape(title or '', quote=True) |
105 | - return f'<img src="/file/{src}" class="img-fluid mx-auto d-block" alt="{alt}" title="{title}">' | 108 | + return f'<img src="/file/{src}" class="img-fluid mx-auto d-block" ' \ |
109 | + f'alt="{alt}" title="{title}">' | ||
106 | 110 | ||
107 | # Pass math through unaltered - mathjax does the rendering in the browser | 111 | # Pass math through unaltered - mathjax does the rendering in the browser |
108 | def block_math(self, text): | 112 | def block_math(self, text): |
@@ -115,7 +119,9 @@ class HighlightRenderer(mistune.Renderer): | @@ -115,7 +119,9 @@ class HighlightRenderer(mistune.Renderer): | ||
115 | return fr'$$$ {text} $$$' | 119 | return fr'$$$ {text} $$$' |
116 | 120 | ||
117 | 121 | ||
118 | -markdown = MarkdownWithMath(HighlightRenderer(escape=True)) # hard_wrap=True to insert <br> on newline | 122 | +# hard_wrap=True to insert <br> on newline |
123 | +markdown = MarkdownWithMath(HighlightRenderer(escape=True)) | ||
124 | + | ||
119 | 125 | ||
120 | def md_to_html(text, strip_p_tag=False, q=None): | 126 | def md_to_html(text, strip_p_tag=False, q=None): |
121 | md = markdown(text) | 127 | md = markdown(text) |
@@ -124,6 +130,7 @@ def md_to_html(text, strip_p_tag=False, q=None): | @@ -124,6 +130,7 @@ def md_to_html(text, strip_p_tag=False, q=None): | ||
124 | else: | 130 | else: |
125 | return md | 131 | return md |
126 | 132 | ||
133 | + | ||
127 | # --------------------------------------------------------------------------- | 134 | # --------------------------------------------------------------------------- |
128 | # load data from yaml file | 135 | # load data from yaml file |
129 | # --------------------------------------------------------------------------- | 136 | # --------------------------------------------------------------------------- |
@@ -143,10 +150,12 @@ def load_yaml(filename, default=None): | @@ -143,10 +150,12 @@ def load_yaml(filename, default=None): | ||
143 | default = yaml.load(f) | 150 | default = yaml.load(f) |
144 | except yaml.YAMLError as e: | 151 | except yaml.YAMLError as e: |
145 | mark = e.problem_mark | 152 | mark = e.problem_mark |
146 | - logger.error(f'In YAML file "{filename}" near line {mark.line}, column {mark.column+1}') | 153 | + logger.error(f'In file "{filename}" near line {mark.line}, ' |
154 | + f'column {mark.column+1}') | ||
147 | finally: | 155 | finally: |
148 | return default | 156 | return default |
149 | 157 | ||
158 | + | ||
150 | # --------------------------------------------------------------------------- | 159 | # --------------------------------------------------------------------------- |
151 | # Runs a script and returns its stdout parsed as yaml, or None on error. | 160 | # Runs a script and returns its stdout parsed as yaml, or None on error. |
152 | # The script is run in another process but this function blocks waiting | 161 | # The script is run in another process but this function blocks waiting |
@@ -156,27 +165,27 @@ def run_script(script, stdin='', timeout=5): | @@ -156,27 +165,27 @@ def run_script(script, stdin='', timeout=5): | ||
156 | script = path.expanduser(script) | 165 | script = path.expanduser(script) |
157 | try: | 166 | try: |
158 | p = subprocess.run([script], | 167 | p = subprocess.run([script], |
159 | - input=stdin, | ||
160 | - stdout=subprocess.PIPE, | ||
161 | - stderr=subprocess.STDOUT, | ||
162 | - universal_newlines=True, | ||
163 | - timeout=timeout, | ||
164 | - ) | 168 | + input=stdin, |
169 | + stdout=subprocess.PIPE, | ||
170 | + stderr=subprocess.STDOUT, | ||
171 | + universal_newlines=True, | ||
172 | + timeout=timeout, | ||
173 | + ) | ||
165 | except FileNotFoundError: | 174 | except FileNotFoundError: |
166 | - logger.error(f'Could not execute script "{script}": not found.') | 175 | + logger.error(f'Can not execute script "{script}": not found.') |
167 | except PermissionError: | 176 | except PermissionError: |
168 | - logger.error(f'Could not execute script "{script}": wrong permissions.') | 177 | + logger.error(f'Can not execute script "{script}": wrong permissions.') |
169 | except OSError: | 178 | except OSError: |
170 | - logger.error(f'Could not execute script "{script}": unknown reason.') | 179 | + logger.error(f'Can not execute script "{script}": unknown reason.') |
171 | except subprocess.TimeoutExpired: | 180 | except subprocess.TimeoutExpired: |
172 | - logger.error(f'Timeout exceeded ({timeout}s) while running "{script}".') | 181 | + logger.error(f'Timeout {timeout}s exceeded while running "{script}".') |
173 | else: | 182 | else: |
174 | if p.returncode != 0: | 183 | if p.returncode != 0: |
175 | - logger.error(f'Script "{script}" returned error code {p.returncode}.') | 184 | + logger.error(f'Return code {p.returncode} running "{script}".') |
176 | else: | 185 | else: |
177 | try: | 186 | try: |
178 | output = yaml.load(p.stdout) | 187 | output = yaml.load(p.stdout) |
179 | - except: | 188 | + except Exception: |
180 | logger.error(f'Error parsing yaml output of "{script}"') | 189 | logger.error(f'Error parsing yaml output of "{script}"') |
181 | else: | 190 | else: |
182 | return output | 191 | return output |
demo/solar_system/correct-first_3_planets.py
@@ -9,19 +9,19 @@ s = sys.stdin.read() | @@ -9,19 +9,19 @@ s = sys.stdin.read() | ||
9 | answer = set(re.findall(r'[\w]+', s.lower())) | 9 | answer = set(re.findall(r'[\w]+', s.lower())) |
10 | answer.difference_update({'e', 'a', 'planeta', 'planetas'}) # ignore these | 10 | answer.difference_update({'e', 'a', 'planeta', 'planetas'}) # ignore these |
11 | 11 | ||
12 | -# correct set of colors | ||
13 | -planets = set(['mercúrio', 'vénus', 'terra']) | 12 | +# correct set of planets |
13 | +planets = {'mercúrio', 'vénus', 'terra'} | ||
14 | 14 | ||
15 | -correct = set.intersection(answer, planets) # os que acertei | ||
16 | -wrong = set.difference(answer, planets) # os que errei | ||
17 | -# allnames = set.union(answer, planets) | 15 | +correct = set.intersection(answer, planets) # the ones I got right |
16 | +wrong = set.difference(answer, planets) # the ones I got wrong | ||
18 | 17 | ||
19 | grade = (len(correct) - len(wrong)) / len(planets) | 18 | grade = (len(correct) - len(wrong)) / len(planets) |
20 | 19 | ||
21 | -out = f'grade: {grade}' | 20 | +out = f'''--- |
21 | +grade: {grade}''' | ||
22 | 22 | ||
23 | if grade < 1.0: | 23 | if grade < 1.0: |
24 | - out += '\ncomments: A resposta correcta é `Mercúrio, Vénus e Terra`.' | 24 | + out += '\ncomments: Vou dar uma ajuda, as iniciais são M, V e T...' |
25 | 25 | ||
26 | print(out) | 26 | print(out) |
27 | -exit(0) | ||
28 | \ No newline at end of file | 27 | \ No newline at end of file |
28 | +exit(0) |