Commit 8e6019534a61621920e1352ff4cc18d642a2527a

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

fix mostly flake8 errors

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=&#39;&#39;, timeout=5): @@ -156,27 +165,27 @@ def run_script(script, stdin=&#39;&#39;, 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)