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 | 33 | # project |
34 | 34 | from aprendizations.tools import run_script |
35 | 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 | 40 | # setup logger for this module |
40 | 41 | logger = logging.getLogger(__name__) |
... | ... | @@ -47,17 +48,17 @@ class QFactory(object): |
47 | 48 | # Depending on the type of question, a different question class will be |
48 | 49 | # instantiated. All these classes derive from the base class `Question`. |
49 | 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 | 55 | 'numeric-interval': QuestionNumericInterval, |
55 | - 'textarea' : QuestionTextArea, | |
56 | + 'textarea': QuestionTextArea, | |
56 | 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 | 64 | def __init__(self, question_dict={}): |
... | ... | @@ -82,17 +83,16 @@ class QFactory(object): |
82 | 83 | # output is then yaml parsed into a dictionary `q`. |
83 | 84 | if q['type'] == 'generator': |
84 | 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 | 87 | script = path.join(q['path'], q['script']) |
87 | 88 | out = run_script(script=script, stdin=q['arg']) |
88 | 89 | q.update(out) |
89 | 90 | |
90 | 91 | # Finally we create an instance of Question() |
91 | 92 | try: |
92 | - qinstance = self._types[q['type']](q) # instance with correct class | |
93 | + qinstance = self._types[q['type']](q) # instance matching class | |
93 | 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 | 96 | raise e |
96 | 97 | else: |
97 | 98 | return qinstance |
98 | - | ... | ... |
aprendizations/initdb.py
... | ... | @@ -26,40 +26,40 @@ def parse_commandline_arguments(): |
26 | 26 | ' is created.') |
27 | 27 | |
28 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 64 | return argparser.parse_args() |
65 | 65 | |
... | ... | @@ -104,7 +104,7 @@ def insert_students_into_db(session, students): |
104 | 104 | try: |
105 | 105 | # --- start db session --- |
106 | 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 | 109 | session.commit() |
110 | 110 | |
... | ... | @@ -117,7 +117,7 @@ def insert_students_into_db(session, students): |
117 | 117 | def show_students_in_database(session, verbose=False): |
118 | 118 | try: |
119 | 119 | users = session.query(Student).order_by(Student.id).all() |
120 | - except: | |
120 | + except Exception: | |
121 | 121 | raise |
122 | 122 | else: |
123 | 123 | n = len(users) |
... | ... | @@ -191,5 +191,5 @@ def main(): |
191 | 191 | |
192 | 192 | |
193 | 193 | # =========================================================================== |
194 | -if __name__=='__main__': | |
194 | +if __name__ == '__main__': | |
195 | 195 | main() | ... | ... |
aprendizations/knowledge.py
1 | 1 | |
2 | 2 | # python standard library |
3 | 3 | import random |
4 | -from datetime import datetime | |
4 | +from datetime import datetime | |
5 | 5 | import logging |
6 | 6 | |
7 | 7 | # libraries |
... | ... | @@ -13,6 +13,7 @@ import networkx as nx |
13 | 13 | # setup logger for this module |
14 | 14 | logger = logging.getLogger(__name__) |
15 | 15 | |
16 | + | |
16 | 17 | # ---------------------------------------------------------------------------- |
17 | 18 | # kowledge state of each student....?? |
18 | 19 | # Contains: |
... | ... | @@ -25,16 +26,15 @@ class StudentKnowledge(object): |
25 | 26 | # methods that update state |
26 | 27 | # ======================================================================= |
27 | 28 | def __init__(self, deps, factory, state={}): |
28 | - self.deps = deps # dependency graph shared among students | |
29 | + self.deps = deps # shared dependency graph | |
29 | 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 | 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 | 36 | self.current_topic = None |
36 | 37 | |
37 | - | |
38 | 38 | # ------------------------------------------------------------------------ |
39 | 39 | # Updates the proficiency levels of the topics, with forgetting factor |
40 | 40 | # FIXME no dependencies are considered yet... |
... | ... | @@ -45,7 +45,6 @@ class StudentKnowledge(object): |
45 | 45 | dt = now - s['date'] |
46 | 46 | s['level'] *= 0.95 ** dt.days # forgetting factor 0.95 FIXME |
47 | 47 | |
48 | - | |
49 | 48 | # ------------------------------------------------------------------------ |
50 | 49 | # Unlock topics whose dependencies are satisfied (> min_level) |
51 | 50 | # ------------------------------------------------------------------------ |
... | ... | @@ -57,15 +56,15 @@ class StudentKnowledge(object): |
57 | 56 | for topic in self.topic_sequence: |
58 | 57 | if topic not in self.state: # if locked |
59 | 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 | 62 | self.state[topic] = { |
63 | 63 | 'level': 0.0, # unlocked |
64 | 64 | 'date': datetime.now() |
65 | 65 | } |
66 | 66 | logger.debug(f'Unlocked "{topic}".') |
67 | 67 | |
68 | - | |
69 | 68 | # ------------------------------------------------------------------------ |
70 | 69 | # Start a new topic. |
71 | 70 | # questions: list of generated questions to do in the topic |
... | ... | @@ -74,6 +73,8 @@ class StudentKnowledge(object): |
74 | 73 | # FIXME async mas nao tem awaits... |
75 | 74 | async def start_topic(self, topic): |
76 | 75 | logger.debug(f'StudentKnowledge.start_topic({topic})') |
76 | + if self.current_topic == topic: | |
77 | + return False | |
77 | 78 | |
78 | 79 | # do not allow locked topics |
79 | 80 | if self.is_locked(topic): |
... | ... | @@ -94,8 +95,8 @@ class StudentKnowledge(object): |
94 | 95 | logger.debug(f'Questions: {", ".join(questions)}') |
95 | 96 | |
96 | 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 | 100 | # self.questions = [gen(qref) for qref in questions] |
100 | 101 | logger.debug(f'Total: {len(self.questions)} questions') |
101 | 102 | |
... | ... | @@ -103,7 +104,6 @@ class StudentKnowledge(object): |
103 | 104 | self.next_question() |
104 | 105 | return True |
105 | 106 | |
106 | - | |
107 | 107 | # ------------------------------------------------------------------------ |
108 | 108 | # The topic has finished and there are no more questions. |
109 | 109 | # The topic level is updated in state and unlocks are performed. |
... | ... | @@ -114,11 +114,11 @@ class StudentKnowledge(object): |
114 | 114 | |
115 | 115 | self.state[self.current_topic] = { |
116 | 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 | 120 | self.unlock_topics() |
120 | 121 | |
121 | - | |
122 | 122 | # ------------------------------------------------------------------------ |
123 | 123 | # corrects current question with provided answer. |
124 | 124 | # implements the logic: |
... | ... | @@ -143,21 +143,19 @@ class StudentKnowledge(object): |
143 | 143 | else: |
144 | 144 | self.wrong_answers += 1 |
145 | 145 | self.current_question['tries'] -= 1 |
146 | - logger.debug(f'Wrong answers = {self.wrong_answers}; Tries = {self.current_question["tries"]}') | |
147 | 146 | |
148 | 147 | if self.current_question['tries'] > 0: |
149 | 148 | action = 'try_again' |
150 | 149 | else: |
151 | 150 | action = 'wrong' |
152 | 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 | 153 | self.questions.append(self.factory[q['ref']].generate()) |
155 | 154 | self.next_question() |
156 | 155 | |
157 | 156 | # returns corrected question (not new one) which might include comments |
158 | 157 | return q, action |
159 | 158 | |
160 | - | |
161 | 159 | # ------------------------------------------------------------------------ |
162 | 160 | # Move to next question |
163 | 161 | # ------------------------------------------------------------------------ |
... | ... | @@ -170,7 +168,8 @@ class StudentKnowledge(object): |
170 | 168 | else: |
171 | 169 | self.current_question['start_time'] = datetime.now() |
172 | 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 | 173 | logger.debug(f'Next question is "{self.current_question["ref"]}"') |
175 | 174 | |
176 | 175 | return self.current_question # question or None |
... | ... | @@ -185,9 +184,21 @@ class StudentKnowledge(object): |
185 | 184 | # ------------------------------------------------------------------------ |
186 | 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 | 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 | 203 | def get_current_question(self): |
193 | 204 | return self.current_question |
... | ... | @@ -212,11 +223,12 @@ class StudentKnowledge(object): |
212 | 223 | 'type': self.deps.nodes[ref]['type'], |
213 | 224 | 'name': self.deps.nodes[ref]['name'], |
214 | 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 | 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 | 234 | def get_topic_level(self, topic): |
... | ... | @@ -231,4 +243,3 @@ class StudentKnowledge(object): |
231 | 243 | # ------------------------------------------------------------------------ |
232 | 244 | # def get_recommended_topic(self): # FIXME untested |
233 | 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 | 28 | async def _bcrypt_hash(a, b): |
29 | 29 | # loop = asyncio.get_running_loop() # FIXME python 3.7 only |
30 | 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 | 35 | async def check_password(try_pw, pw): |
... | ... | @@ -77,7 +78,9 @@ class LearnApp(object): |
77 | 78 | async def login(self, uid, try_pw): |
78 | 79 | with self.db_session() as s: |
79 | 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 | 84 | except Exception: |
82 | 85 | logger.info(f'User "{uid}" does not exist') |
83 | 86 | return False |
... | ... | @@ -103,7 +106,8 @@ class LearnApp(object): |
103 | 106 | self.online[uid] = { |
104 | 107 | 'number': uid, |
105 | 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 | 111 | 'counter': counter + 1, # counts simultaneous logins |
108 | 112 | } |
109 | 113 | |
... | ... | @@ -162,14 +166,17 @@ class LearnApp(object): |
162 | 166 | date = str(knowledge.get_topic_date(topic)) |
163 | 167 | |
164 | 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 | 172 | if a is None: |
167 | 173 | # insert new studenttopic into database |
168 | 174 | logger.debug('Database insert new studenttopic') |
169 | 175 | t = s.query(Topic).get(topic) |
170 | 176 | u = s.query(Student).get(uid) |
171 | 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 | 180 | u.topics.append(a) |
174 | 181 | else: |
175 | 182 | # update studenttopic in database |
... | ... | @@ -204,7 +211,7 @@ class LearnApp(object): |
204 | 211 | missing_topics = [Topic(id=t) for t in topics if t not in dbtopics] |
205 | 212 | if missing_topics: |
206 | 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 | 217 | # setup and check database |
... | ... | @@ -262,7 +269,8 @@ class LearnApp(object): |
262 | 269 | t['file'] = attr.get('file', default_file) # questions.yaml |
263 | 270 | t['shuffle'] = attr.get('shuffle', default_shuffle) |
264 | 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 | 274 | t['choose'] = attr.get('choose', default_choose) |
267 | 275 | t['append_wrong'] = attr.get('append_wrong', default_append_wrong) |
268 | 276 | t['questions'] = attr.get('questions', []) |
... | ... | @@ -347,10 +355,6 @@ class LearnApp(object): |
347 | 355 | def get_topic_name(self, ref): |
348 | 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 | 359 | def get_current_public_dir(self, uid): |
356 | 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 | 3 | from sqlalchemy.ext.declarative import declarative_base |
8 | 4 | from sqlalchemy.orm import relationship |
9 | 5 | |
... | ... | @@ -12,13 +8,14 @@ from sqlalchemy.orm import relationship |
12 | 8 | # Declare ORM |
13 | 9 | Base = declarative_base() |
14 | 10 | |
11 | + | |
15 | 12 | # --------------------------------------------------------------------------- |
16 | 13 | class StudentTopic(Base): |
17 | 14 | __tablename__ = 'studenttopic' |
18 | 15 | student_id = Column(String, ForeignKey('students.id'), primary_key=True) |
19 | 16 | topic_id = Column(String, ForeignKey('topics.id'), primary_key=True) |
20 | 17 | level = Column(Float) |
21 | - date = Column(String) | |
18 | + date = Column(String) | |
22 | 19 | |
23 | 20 | # --- |
24 | 21 | student = relationship('Student', back_populates='topics') |
... | ... | @@ -31,6 +28,7 @@ class StudentTopic(Base): |
31 | 28 | level: "{self.level}" |
32 | 29 | date: "{self.date}"''' |
33 | 30 | |
31 | + | |
34 | 32 | # --------------------------------------------------------------------------- |
35 | 33 | # Registered students |
36 | 34 | # --------------------------------------------------------------------------- |
... | ... | @@ -50,12 +48,13 @@ class Student(Base): |
50 | 48 | name: "{self.name}" |
51 | 49 | password: "{self.password}"''' |
52 | 50 | |
51 | + | |
53 | 52 | # --------------------------------------------------------------------------- |
54 | 53 | # Table with every answer given |
55 | 54 | # --------------------------------------------------------------------------- |
56 | 55 | class Answer(Base): |
57 | 56 | __tablename__ = 'answers' |
58 | - id = Column(Integer, primary_key=True) # auto_increment | |
57 | + id = Column(Integer, primary_key=True) # auto_increment | |
59 | 58 | ref = Column(String) |
60 | 59 | grade = Column(Float) |
61 | 60 | starttime = Column(String) |
... | ... | @@ -77,6 +76,7 @@ class Answer(Base): |
77 | 76 | student_id: "{self.student_id}" |
78 | 77 | topic_id: "{self.topic_id}"''' |
79 | 78 | |
79 | + | |
80 | 80 | # --------------------------------------------------------------------------- |
81 | 81 | # Table with student state |
82 | 82 | # --------------------------------------------------------------------------- | ... | ... |
aprendizations/questions.py
... | ... | @@ -43,12 +43,12 @@ class Question(dict): |
43 | 43 | 'files': {}, |
44 | 44 | }) |
45 | 45 | |
46 | - def correct(self): | |
46 | + def correct(self) -> float: | |
47 | 47 | self['comments'] = '' |
48 | 48 | self['grade'] = 0.0 |
49 | 49 | return 0.0 |
50 | 50 | |
51 | - async def correct_async(self): | |
51 | + async def correct_async(self) -> float: | |
52 | 52 | # loop = asyncio.get_running_loop() # FIXME python 3.7 only |
53 | 53 | loop = asyncio.get_event_loop() |
54 | 54 | grade = await loop.run_in_executor(None, self.correct) |
... | ... | @@ -56,7 +56,7 @@ class Question(dict): |
56 | 56 | |
57 | 57 | def set_defaults(self, d): |
58 | 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 | 60 | self.setdefault(k, v) |
61 | 61 | |
62 | 62 | |
... | ... | @@ -73,7 +73,7 @@ class QuestionRadio(Question): |
73 | 73 | choose (int) # only used if shuffle=True |
74 | 74 | ''' |
75 | 75 | |
76 | - #------------------------------------------------------------------------ | |
76 | + # ------------------------------------------------------------------------ | |
77 | 77 | def __init__(self, q): |
78 | 78 | super().__init__(q) |
79 | 79 | |
... | ... | @@ -90,7 +90,8 @@ class QuestionRadio(Question): |
90 | 90 | # always convert to list, e.g. correct: 2 --> correct: [0,0,1,0,0] |
91 | 91 | # correctness levels from 0.0 to 1.0 (no discount here!) |
92 | 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 | 96 | if self['shuffle']: |
96 | 97 | # separate right from wrong options |
... | ... | @@ -101,8 +102,8 @@ class QuestionRadio(Question): |
101 | 102 | |
102 | 103 | # choose 1 correct option |
103 | 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 | 108 | # choose remaining wrong options |
108 | 109 | random.shuffle(wrong) |
... | ... | @@ -112,10 +113,10 @@ class QuestionRadio(Question): |
112 | 113 | |
113 | 114 | # final shuffle of the options |
114 | 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 | 120 | # can return negative values for wrong answers |
120 | 121 | def correct(self): |
121 | 122 | super().correct() |
... | ... | @@ -144,7 +145,7 @@ class QuestionCheckbox(Question): |
144 | 145 | answer (None or an actual answer) |
145 | 146 | ''' |
146 | 147 | |
147 | - #------------------------------------------------------------------------ | |
148 | + # ------------------------------------------------------------------------ | |
148 | 149 | def __init__(self, q): |
149 | 150 | super().__init__(q) |
150 | 151 | |
... | ... | @@ -153,24 +154,25 @@ class QuestionCheckbox(Question): |
153 | 154 | # set defaults if missing |
154 | 155 | self.set_defaults({ |
155 | 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 | 158 | 'shuffle': True, |
158 | 159 | 'discount': True, |
159 | 160 | 'choose': n, # number of options |
160 | 161 | }) |
161 | 162 | |
162 | 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 | 167 | # if an option is a list of (right, wrong), pick one |
166 | 168 | # FIXME it's possible that all options are chosen wrong |
167 | 169 | options = [] |
168 | 170 | correct = [] |
169 | - for o,c in zip(self['options'], self['correct']): | |
171 | + for o, c in zip(self['options'], self['correct']): | |
170 | 172 | if isinstance(o, list): |
171 | - r = random.randint(0,1) | |
173 | + r = random.randint(0, 1) | |
172 | 174 | o = o[r] |
173 | - c = c if r==0 else -c | |
175 | + c = c if r == 0 else -c | |
174 | 176 | options.append(str(o)) |
175 | 177 | correct.append(float(c)) |
176 | 178 | |
... | ... | @@ -181,7 +183,7 @@ class QuestionCheckbox(Question): |
181 | 183 | self['options'] = [options[i] for i in perm] |
182 | 184 | self['correct'] = [correct[i] for i in perm] |
183 | 185 | |
184 | - #------------------------------------------------------------------------ | |
186 | + # ------------------------------------------------------------------------ | |
185 | 187 | # can return negative values for wrong answers |
186 | 188 | def correct(self): |
187 | 189 | super().correct() |
... | ... | @@ -215,7 +217,7 @@ class QuestionText(Question): |
215 | 217 | answer (None or an actual answer) |
216 | 218 | ''' |
217 | 219 | |
218 | - #------------------------------------------------------------------------ | |
220 | + # ------------------------------------------------------------------------ | |
219 | 221 | def __init__(self, q): |
220 | 222 | super().__init__(q) |
221 | 223 | |
... | ... | @@ -231,7 +233,7 @@ class QuestionText(Question): |
231 | 233 | # make sure all elements of the list are strings |
232 | 234 | self['correct'] = [str(a) for a in self['correct']] |
233 | 235 | |
234 | - #------------------------------------------------------------------------ | |
236 | + # ------------------------------------------------------------------------ | |
235 | 237 | # can return negative values for wrong answers |
236 | 238 | def correct(self): |
237 | 239 | super().correct() |
... | ... | @@ -251,7 +253,7 @@ class QuestionTextRegex(Question): |
251 | 253 | answer (None or an actual answer) |
252 | 254 | ''' |
253 | 255 | |
254 | - #------------------------------------------------------------------------ | |
256 | + # ------------------------------------------------------------------------ | |
255 | 257 | def __init__(self, q): |
256 | 258 | super().__init__(q) |
257 | 259 | |
... | ... | @@ -260,15 +262,17 @@ class QuestionTextRegex(Question): |
260 | 262 | 'correct': '$.^', # will always return false |
261 | 263 | }) |
262 | 264 | |
263 | - #------------------------------------------------------------------------ | |
265 | + # ------------------------------------------------------------------------ | |
264 | 266 | # can return negative values for wrong answers |
265 | 267 | def correct(self): |
266 | 268 | super().correct() |
267 | 269 | if self['answer'] is not None: |
268 | 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 | 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 | 277 | return self['grade'] |
274 | 278 | |
... | ... | @@ -283,7 +287,7 @@ class QuestionNumericInterval(Question): |
283 | 287 | An answer is correct if it's in the closed interval. |
284 | 288 | ''' |
285 | 289 | |
286 | - #------------------------------------------------------------------------ | |
290 | + # ------------------------------------------------------------------------ | |
287 | 291 | def __init__(self, q): |
288 | 292 | super().__init__(q) |
289 | 293 | |
... | ... | @@ -292,7 +296,7 @@ class QuestionNumericInterval(Question): |
292 | 296 | 'correct': [1.0, -1.0], # will always return false |
293 | 297 | }) |
294 | 298 | |
295 | - #------------------------------------------------------------------------ | |
299 | + # ------------------------------------------------------------------------ | |
296 | 300 | # can return negative values for wrong answers |
297 | 301 | def correct(self): |
298 | 302 | super().correct() |
... | ... | @@ -302,7 +306,8 @@ class QuestionNumericInterval(Question): |
302 | 306 | try: # replace , by . and convert to float |
303 | 307 | answer = float(self['answer'].replace(',', '.', 1)) |
304 | 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 | 311 | self['grade'] = 0.0 |
307 | 312 | else: |
308 | 313 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 |
... | ... | @@ -320,7 +325,7 @@ class QuestionTextArea(Question): |
320 | 325 | lines (int) |
321 | 326 | ''' |
322 | 327 | |
323 | - #------------------------------------------------------------------------ | |
328 | + # ------------------------------------------------------------------------ | |
324 | 329 | def __init__(self, q): |
325 | 330 | super().__init__(q) |
326 | 331 | |
... | ... | @@ -331,10 +336,9 @@ class QuestionTextArea(Question): |
331 | 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 | 342 | # can return negative values for wrong answers |
339 | 343 | def correct(self): |
340 | 344 | super().correct() |
... | ... | @@ -347,31 +351,30 @@ class QuestionTextArea(Question): |
347 | 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 | 355 | self['comments'] = out.get('comments', '') |
355 | 356 | try: |
356 | 357 | self['grade'] = float(out['grade']) |
357 | 358 | except ValueError: |
358 | - logger.error(f'Correction script of "{self["ref"]}" returned nonfloat.') | |
359 | + logger.error(f'Output error in "{self["correct"]}".') | |
359 | 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 | 365 | return self['grade'] |
363 | 366 | |
364 | 367 | |
365 | 368 | # =========================================================================== |
366 | 369 | class QuestionInformation(Question): |
367 | - #------------------------------------------------------------------------ | |
370 | + # ------------------------------------------------------------------------ | |
368 | 371 | def __init__(self, q): |
369 | 372 | super().__init__(q) |
370 | 373 | self.set_defaults({ |
371 | 374 | 'text': '', |
372 | 375 | }) |
373 | 376 | |
374 | - #------------------------------------------------------------------------ | |
377 | + # ------------------------------------------------------------------------ | |
375 | 378 | # can return negative values for wrong answers |
376 | 379 | def correct(self): |
377 | 380 | super().correct() | ... | ... |
aprendizations/redirect.py
aprendizations/serve.py
... | ... | @@ -381,7 +381,8 @@ def parse_cmdline_arguments(): |
381 | 381 | |
382 | 382 | return argparser.parse_args() |
383 | 383 | |
384 | -# ------------------------------------------------------------------------- | |
384 | + | |
385 | +# ---------------------------------------------------------------------------- | |
385 | 386 | def get_logger_config(debug=False): |
386 | 387 | if debug: |
387 | 388 | filename = 'logger-debug.yaml' |
... | ... | @@ -397,7 +398,8 @@ def get_logger_config(debug=False): |
397 | 398 | 'version': 1, |
398 | 399 | 'formatters': { |
399 | 400 | 'standard': { |
400 | - 'format': '%(asctime)s %(name)-24s %(levelname)-8s %(message)s', | |
401 | + 'format': '%(asctime)s %(name)-24s %(levelname)-8s ' | |
402 | + '%(message)s', | |
401 | 403 | 'datefmt': '%H:%M:%S', |
402 | 404 | }, |
403 | 405 | }, | ... | ... |
aprendizations/tools.py
... | ... | @@ -23,11 +23,13 @@ logger = logging.getLogger(__name__) |
23 | 23 | # ------------------------------------------------------------------------- |
24 | 24 | class MathBlockGrammar(mistune.BlockGrammar): |
25 | 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 | 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 | 34 | def __init__(self, rules=None, **kwargs): |
33 | 35 | if rules is None: |
... | ... | @@ -82,27 +84,29 @@ class MarkdownWithMath(mistune.Markdown): |
82 | 84 | return self.renderer.block_math(self.token['text']) |
83 | 85 | |
84 | 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 | 91 | class HighlightRenderer(mistune.Renderer): |
90 | 92 | def block_code(self, code, lang='text'): |
91 | 93 | try: |
92 | 94 | lexer = get_lexer_by_name(lang, stripall=False) |
93 | - except: | |
95 | + except Exception: | |
94 | 96 | lexer = get_lexer_by_name('text', stripall=False) |
95 | 97 | |
96 | 98 | formatter = HtmlFormatter() |
97 | 99 | return highlight(code, lexer, formatter) |
98 | 100 | |
99 | 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 | 105 | def image(self, src, title, alt): |
103 | 106 | alt = mistune.escape(alt, quote=True) |
104 | 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 | 111 | # Pass math through unaltered - mathjax does the rendering in the browser |
108 | 112 | def block_math(self, text): |
... | ... | @@ -115,7 +119,9 @@ class HighlightRenderer(mistune.Renderer): |
115 | 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 | 126 | def md_to_html(text, strip_p_tag=False, q=None): |
121 | 127 | md = markdown(text) |
... | ... | @@ -124,6 +130,7 @@ def md_to_html(text, strip_p_tag=False, q=None): |
124 | 130 | else: |
125 | 131 | return md |
126 | 132 | |
133 | + | |
127 | 134 | # --------------------------------------------------------------------------- |
128 | 135 | # load data from yaml file |
129 | 136 | # --------------------------------------------------------------------------- |
... | ... | @@ -143,10 +150,12 @@ def load_yaml(filename, default=None): |
143 | 150 | default = yaml.load(f) |
144 | 151 | except yaml.YAMLError as e: |
145 | 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 | 155 | finally: |
148 | 156 | return default |
149 | 157 | |
158 | + | |
150 | 159 | # --------------------------------------------------------------------------- |
151 | 160 | # Runs a script and returns its stdout parsed as yaml, or None on error. |
152 | 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 | 165 | script = path.expanduser(script) |
157 | 166 | try: |
158 | 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 | 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 | 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 | 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 | 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 | 182 | else: |
174 | 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 | 185 | else: |
177 | 186 | try: |
178 | 187 | output = yaml.load(p.stdout) |
179 | - except: | |
188 | + except Exception: | |
180 | 189 | logger.error(f'Error parsing yaml output of "{script}"') |
181 | 190 | else: |
182 | 191 | return output | ... | ... |
demo/solar_system/correct-first_3_planets.py
... | ... | @@ -9,19 +9,19 @@ s = sys.stdin.read() |
9 | 9 | answer = set(re.findall(r'[\w]+', s.lower())) |
10 | 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 | 18 | grade = (len(correct) - len(wrong)) / len(planets) |
20 | 19 | |
21 | -out = f'grade: {grade}' | |
20 | +out = f'''--- | |
21 | +grade: {grade}''' | |
22 | 22 | |
23 | 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 | 26 | print(out) |
27 | -exit(0) | |
28 | 27 | \ No newline at end of file |
28 | +exit(0) | ... | ... |