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 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
... ... @@ -4,6 +4,7 @@ from tornado.web import RedirectHandler, Application
4 4 from tornado.ioloop import IOLoop
5 5 import argparse
6 6  
  7 +
7 8 def main():
8 9 default_url = 'https://bit.xdi.uevora.pt/'
9 10 default_port = 8080
... ...
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=&#39;&#39;, 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)
... ...