Commit 1323bb4d0f868538ee9229ec405321deefc3a12c

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

fix pylint warnings

perguntations/app.py
  1 +'''
  2 +Main application module
  3 +'''
  4 +
1 5  
2 6 # python standard libraries
3   -from os import path
4   -import logging
5   -from contextlib import contextmanager # `with` statement in db sessions
6 7 import asyncio
  8 +from contextlib import contextmanager # `with` statement in db sessions
  9 +import json
  10 +import logging
  11 +from os import path
7 12  
8 13 # user installed packages
9 14 import bcrypt
10   -import json
11 15 from sqlalchemy import create_engine
12 16 from sqlalchemy.orm import sessionmaker
13 17  
... ... @@ -21,43 +25,50 @@ logger = logging.getLogger(__name__)
21 25  
22 26 # ============================================================================
23 27 class AppException(Exception):
24   - pass
  28 + '''Exception raised in this module'''
25 29  
26 30  
27 31 # ============================================================================
28 32 # helper functions
29 33 # ============================================================================
30 34 async def check_password(try_pw, password):
  35 + '''check password in executor'''
31 36 try_pw = try_pw.encode('utf-8')
32 37 loop = asyncio.get_running_loop()
33 38 hashed = await loop.run_in_executor(None, bcrypt.hashpw, try_pw, password)
34 39 return password == hashed
35 40  
36 41  
37   -async def hash_password(pw):
38   - pw = pw.encode('utf-8')
  42 +async def hash_password(password):
  43 + '''hash password in executor'''
39 44 loop = asyncio.get_running_loop()
40   - r = await loop.run_in_executor(None, bcrypt.hashpw, pw, bcrypt.gensalt())
41   - return r
  45 + return await loop.run_in_executor(None, bcrypt.hashpw,
  46 + password.encode('utf-8'),
  47 + bcrypt.gensalt())
42 48  
43 49  
44 50 # ============================================================================
45   -# Application
46   -# state:
47   -# self.Session
48   -# self.online - {uid:
49   -# {'student':{...}, 'test': {...}},
50   -# ...
51   -# }
52   -# self.allowd - {'123', '124', ...}
53   -# self.testfactory - TestFactory
54 51 # ============================================================================
55   -class App(object):
  52 +class App():
  53 + '''
  54 + This is the main application
  55 + state:
  56 + self.Session
  57 + self.online - {uid:
  58 + {'student':{...}, 'test': {...}},
  59 + ...
  60 + }
  61 + self.allowd - {'123', '124', ...}
  62 + self.testfactory - TestFactory
  63 + '''
  64 +
56 65 # ------------------------------------------------------------------------
57   - # helper to manage db sessions using the `with` statement, for example
58   - # with self.db_session() as s: s.query(...)
59 66 @contextmanager
60 67 def db_session(self):
  68 + '''
  69 + helper to manage db sessions using the `with` statement, for example:
  70 + with self.db_session() as s: s.query(...)
  71 + '''
61 72 session = self.Session()
62 73 try:
63 74 yield session
... ... @@ -73,15 +84,15 @@ class App(object):
73 84 self.online = dict() # {uid: {'student':{...}, 'test': {...}}, ...}
74 85 self.allowed = set([]) # '0' is hardcoded to allowed elsewhere
75 86  
76   - logger.info(f'Loading test configuration "{conf["testfile"]}".')
  87 + logger.info('Loading test configuration "%s".', conf["testfile"])
77 88 testconf = load_yaml(conf['testfile'])
78 89 testconf.update(conf) # command line options override configuration
79 90  
80 91 # start test factory
81 92 try:
82 93 self.testfactory = TestFactory(testconf)
83   - except TestFactoryException as e:
84   - logger.critical(e)
  94 + except TestFactoryException as exc:
  95 + logger.critical(exc)
85 96 raise AppException('Failed to create test factory!')
86 97 else:
87 98 logger.info('No errors found. Test factory ready.')
... ... @@ -92,12 +103,12 @@ class App(object):
92 103 engine = create_engine(database, echo=False)
93 104 self.Session = sessionmaker(bind=engine)
94 105 try:
95   - with self.db_session() as s:
96   - n = s.query(Student).filter(Student.id != '0').count()
  106 + with self.db_session() as sess:
  107 + num = sess.query(Student).filter(Student.id != '0').count()
97 108 except Exception:
98 109 raise AppException(f'Database unusable {dbfile}.')
99 110 else:
100   - logger.info(f'Database "{dbfile}" has {n} students.')
  111 + logger.info('Database "%s" has %s students.', dbfile, num)
101 112  
102 113 # command line option --allow-all
103 114 if conf['allow_all']:
... ... @@ -117,15 +128,16 @@ class App(object):
117 128  
118 129 # ------------------------------------------------------------------------
119 130 async def login(self, uid, try_pw):
  131 + '''login authentication'''
120 132 if uid not in self.allowed and uid != '0': # not allowed
121   - logger.warning(f'Student {uid}: not allowed to login.')
  133 + logger.warning('Student %s: not allowed to login.', uid)
122 134 return False
123 135  
124 136 # get name+password from db
125   - with self.db_session() as s:
126   - name, password = s.query(Student.name, Student.password)\
127   - .filter_by(id=uid)\
128   - .one()
  137 + with self.db_session() as sess:
  138 + name, password = sess.query(Student.name, Student.password)\
  139 + .filter_by(id=uid)\
  140 + .one()
129 141  
130 142 # first login updates the password
131 143 if password == '': # update password on first login
... ... @@ -137,104 +149,112 @@ class App(object):
137 149 if pw_ok: # success
138 150 self.allowed.discard(uid) # remove from set of allowed students
139 151 if uid in self.online:
140   - logger.warning(f'Student {uid}: already logged in.')
  152 + logger.warning('Student %s: already logged in.', uid)
141 153 else: # make student online
142 154 self.online[uid] = {'student': {'name': name, 'number': uid}}
143   - logger.info(f'Student {uid}: logged in.')
  155 + logger.info('Student %s: logged in.', uid)
144 156 return True
145   - else: # wrong password
146   - logger.info(f'Student {uid}: wrong password.')
147   - return False
  157 + # wrong password
  158 + logger.info('Student %s: wrong password.', uid)
  159 + return False
148 160  
149 161 # ------------------------------------------------------------------------
150 162 def logout(self, uid):
  163 + '''student logout'''
151 164 self.online.pop(uid, None) # remove from dict if exists
152   - logger.info(f'Student {uid}: logged out.')
  165 + logger.info('Student %s: logged out.', uid)
153 166  
154 167 # ------------------------------------------------------------------------
155 168 async def generate_test(self, uid):
  169 + '''generate a test for a given student'''
156 170 if uid in self.online:
157   - logger.info(f'Student {uid}: generating new test.')
  171 + logger.info('Student %s: generating new test.', uid)
158 172 student_id = self.online[uid]['student'] # {number, name}
159 173 test = await self.testfactory.generate(student_id)
160 174 self.online[uid]['test'] = test
161   - logger.info(f'Student {uid}: test is ready.')
  175 + logger.info('Student %s: test is ready.', uid)
162 176 return self.online[uid]['test']
163   - else:
164   - # this implies an error in the code. should never be here!
165   - logger.critical(f'Student {uid}: offline, can\'t generate test')
  177 +
  178 + # this implies an error in the code. should never be here!
  179 + logger.critical('Student %s: offline, can\'t generate test', uid)
166 180  
167 181 # ------------------------------------------------------------------------
168   - # ans is a dictionary {question_index: answer, ...}
169   - # for example: {0:'hello', 1:[1,2]}
170 182 async def correct_test(self, uid, ans):
171   - t = self.online[uid]['test']
  183 + '''
  184 + Corrects test
  185 +
  186 + ans is a dictionary {question_index: answer, ...}
  187 + for example: {0:'hello', 1:[1,2]}
  188 + '''
  189 + test = self.online[uid]['test']
172 190  
173 191 # --- submit answers and correct test
174   - t.update_answers(ans)
175   - logger.info(f'Student {uid}: {len(ans)} answers submitted.')
  192 + test.update_answers(ans)
  193 + logger.info('Student %s: %d answers submitted.', uid, len(ans))
176 194  
177   - grade = await t.correct()
178   - logger.info(f'Student {uid}: grade = {grade:5.3} points.')
  195 + grade = await test.correct()
  196 + logger.info('Student %s: grade = %.1g points.', uid, grade)
179 197  
180 198 # --- save test in JSON format
181   - fields = (uid, t['ref'], str(t['finish_time']))
182   - fname = ' -- '.join(fields) + '.json'
183   - fpath = path.join(t['answers_dir'], fname)
184   - with open(path.expanduser(fpath), 'w') as f:
  199 + fields = (uid, test['ref'], str(test['finish_time']))
  200 + fname = '--'.join(fields) + '.json'
  201 + fpath = path.join(test['answers_dir'], fname)
  202 + with open(path.expanduser(fpath), 'w') as file:
185 203 # default=str required for datetime objects
186   - json.dump(t, f, indent=2, default=str)
187   - logger.info(f'Student {uid}: saved JSON.')
  204 + json.dump(test, file, indent=2, default=str)
  205 + logger.info('Student %s: saved JSON.', uid)
188 206  
189 207 # --- insert test and questions into database
190   - with self.db_session() as s:
191   - s.add(Test(
192   - ref=t['ref'],
193   - title=t['title'],
194   - grade=t['grade'],
195   - starttime=str(t['start_time']),
196   - finishtime=str(t['finish_time']),
  208 + with self.db_session() as sess:
  209 + sess.add(Test(
  210 + ref=test['ref'],
  211 + title=test['title'],
  212 + grade=test['grade'],
  213 + starttime=str(test['start_time']),
  214 + finishtime=str(test['finish_time']),
197 215 filename=fpath,
198 216 student_id=uid,
199   - state=t['state'],
  217 + state=test['state'],
200 218 comment=''))
201   - s.add_all([Question(
  219 + sess.add_all([Question(
202 220 ref=q['ref'],
203 221 grade=q['grade'],
204   - starttime=str(t['start_time']),
205   - finishtime=str(t['finish_time']),
  222 + starttime=str(test['start_time']),
  223 + finishtime=str(test['finish_time']),
206 224 student_id=uid,
207   - test_id=t['ref']) for q in t['questions'] if 'grade' in q])
  225 + test_id=test['ref'])
  226 + for q in test['questions'] if 'grade' in q])
208 227  
209   - logger.info(f'Student {uid}: database updated.')
  228 + logger.info('Student %s: database updated.', uid)
210 229 return grade
211 230  
212 231 # ------------------------------------------------------------------------
213 232 def giveup_test(self, uid):
214   - t = self.online[uid]['test']
215   - t.giveup()
  233 + '''giveup test - not used??'''
  234 + test = self.online[uid]['test']
  235 + test.giveup()
216 236  
217 237 # save JSON with the test
218   - fields = (t['student']['number'], t['ref'], str(t['finish_time']))
219   - fname = ' -- '.join(fields) + '.json'
220   - fpath = path.join(t['answers_dir'], fname)
221   - t.save_json(fpath)
  238 + fields = (test['student']['number'], test['ref'],
  239 + str(test['finish_time']))
  240 + fname = '--'.join(fields) + '.json'
  241 + fpath = path.join(test['answers_dir'], fname)
  242 + test.save_json(fpath)
222 243  
223 244 # insert test into database
224   - with self.db_session() as s:
225   - s.add(Test(
226   - ref=t['ref'],
227   - title=t['title'],
228   - grade=t['grade'],
229   - starttime=str(t['start_time']),
230   - finishtime=str(t['finish_time']),
231   - filename=fpath,
232   - student_id=t['student']['number'],
233   - state=t['state'],
234   - comment=''))
235   -
236   - logger.info(f'Student {uid}: gave up.')
237   - return t
  245 + with self.db_session() as sess:
  246 + sess.add(Test(ref=test['ref'],
  247 + title=test['title'],
  248 + grade=test['grade'],
  249 + starttime=str(test['start_time']),
  250 + finishtime=str(test['finish_time']),
  251 + filename=fpath,
  252 + student_id=test['student']['number'],
  253 + state=test['state'],
  254 + comment=''))
  255 +
  256 + logger.info('Student %s: gave up.', uid)
  257 + return test
238 258  
239 259 # ------------------------------------------------------------------------
240 260  
... ... @@ -243,52 +263,58 @@ class App(object):
243 263 # return self.online[uid]['student']['name']
244 264  
245 265 def get_student_test(self, uid, default=None):
  266 + '''get test from online student'''
246 267 return self.online[uid].get('test', default)
247 268  
248 269 # def get_questions_dir(self):
249 270 # return self.testfactory['questions_dir']
250 271  
251 272 def get_student_grades_from_all_tests(self, uid):
252   - with self.db_session() as s:
253   - return s.query(Test.title, Test.grade, Test.finishtime)\
254   - .filter_by(student_id=uid)\
255   - .order_by(Test.finishtime)
  273 + '''get grades of student from all tests'''
  274 + with self.db_session() as sess:
  275 + return sess.query(Test.title, Test.grade, Test.finishtime)\
  276 + .filter_by(student_id=uid)\
  277 + .order_by(Test.finishtime)
256 278  
257 279 def get_json_filename_of_test(self, test_id):
258   - with self.db_session() as s:
259   - return s.query(Test.filename)\
260   - .filter_by(id=test_id)\
261   - .scalar()
  280 + '''get JSON filename from database given the test_id'''
  281 + with self.db_session() as sess:
  282 + return sess.query(Test.filename)\
  283 + .filter_by(id=test_id)\
  284 + .scalar()
262 285  
263 286 def get_all_students(self):
264   - with self.db_session() as s:
265   - return s.query(Student.id, Student.name, Student.password)\
266   - .filter(Student.id != '0')\
267   - .order_by(Student.id)
  287 + '''get all students from database'''
  288 + with self.db_session() as sess:
  289 + return sess.query(Student.id, Student.name, Student.password)\
  290 + .filter(Student.id != '0')\
  291 + .order_by(Student.id)
268 292  
269 293 def get_student_grades_from_test(self, uid, testid):
270   - with self.db_session() as s:
271   - return s.query(Test.grade, Test.finishtime, Test.id)\
272   - .filter_by(student_id=uid)\
273   - .filter_by(ref=testid)\
274   - .all()
  294 + '''get grades of student for a given testid'''
  295 + with self.db_session() as sess:
  296 + return sess.query(Test.grade, Test.finishtime, Test.id)\
  297 + .filter_by(student_id=uid)\
  298 + .filter_by(ref=testid)\
  299 + .all()
275 300  
276 301 def get_students_state(self):
  302 + '''get list of states of every student'''
277 303 return [{
278   - 'uid': uid,
279   - 'name': name,
280   - 'allowed': uid in self.allowed,
281   - 'online': uid in self.online,
282   - 'start_time': self.online.get(uid, {}).get('test', {})
283   - .get('start_time', ''),
284   - 'password_defined': pw != '',
285   - 'grades': self.get_student_grades_from_test(
286   - uid,
287   - self.testfactory['ref']
288   - ),
289   -
290   - # 'focus': self.online.get(uid, {}).get('student', {}).get('focus', True), # FIXME
291   - } for uid, name, pw in self.get_all_students()]
  304 + 'uid': uid,
  305 + 'name': name,
  306 + 'allowed': uid in self.allowed,
  307 + 'online': uid in self.online,
  308 + 'start_time': self.online.get(uid, {}).get('test', {})
  309 + .get('start_time', ''),
  310 + 'password_defined': pw != '',
  311 + 'grades': self.get_student_grades_from_test(
  312 + uid,
  313 + self.testfactory['ref']
  314 + ),
  315 +
  316 + # 'focus': self.online.get(uid, {}).get('student', {}).get('focus', True), # FIXME
  317 + } for uid, name, pw in self.get_all_students()]
292 318  
293 319 # def get_allowed_students(self):
294 320 # # set of 'uid' allowed to login
... ... @@ -303,26 +329,30 @@ class App(object):
303 329  
304 330 # --- helpers (change state)
305 331 def allow_student(self, uid):
  332 + '''allow a single student to login'''
306 333 self.allowed.add(uid)
307   - logger.info(f'Student {uid}: allowed to login.')
  334 + logger.info('Student %s: allowed to login.', uid)
308 335  
309 336 def deny_student(self, uid):
  337 + '''deny a single student to login'''
310 338 self.allowed.discard(uid)
311   - logger.info(f'Student {uid}: denied to login')
  339 + logger.info('Student %s: denied to login', uid)
312 340  
313   - async def update_student_password(self, uid, pw=''):
314   - if pw:
315   - pw = await hash_password(pw)
316   - with self.db_session() as s:
317   - student = s.query(Student).filter_by(id=uid).one()
318   - student.password = pw
319   - logger.info(f'Student {uid}: password updated.')
  341 + async def update_student_password(self, uid, password=''):
  342 + '''change password on the database'''
  343 + if password:
  344 + password = await hash_password(password)
  345 + with self.db_session() as sess:
  346 + student = sess.query(Student).filter_by(id=uid).one()
  347 + student.password = password
  348 + logger.info('Student %s: password updated.', uid)
320 349  
321 350 def insert_new_student(self, uid, name):
  351 + '''insert new student into the database'''
322 352 try:
323   - with self.db_session() as s:
324   - s.add(Student(id=uid, name=name, password=''))
  353 + with self.db_session() as sess:
  354 + sess.add(Student(id=uid, name=name, password=''))
325 355 except Exception:
326   - logger.error(f'Insert failed: student {uid} already exists.')
  356 + logger.error('Insert failed: student %s already exists.', uid)
327 357 else:
328   - logger.info(f'New student inserted: {uid}, {name}')
  358 + logger.info('New student inserted: %s, %s', uid, name)
... ...
perguntations/main.py
1 1 #!/usr/bin/env python3
2 2  
  3 +'''
  4 +Main file that starts the application and the web server
  5 +'''
  6 +
  7 +
3 8 # python standard library
4 9 import argparse
5 10 import logging
... ... @@ -10,7 +15,7 @@ import sys
10 15 # from typing import Any, Dict
11 16  
12 17 # this project
13   -from .app import App
  18 +from .app import App, AppException
14 19 from .serve import run_webserver
15 20 from .tools import load_yaml
16 21 from . import APP_NAME, APP_VERSION
... ... @@ -125,7 +130,7 @@ def main():
125 130 # testapp = App(config)
126 131 try:
127 132 testapp = App(config)
128   - except Exception:
  133 + except AppException:
129 134 logging.critical('Failed to start application.')
130 135 sys.exit(-1)
131 136  
... ...
perguntations/test.py
... ... @@ -267,23 +267,16 @@ class TestFactory(dict):
267 267 if nerr > 0:
268 268 logger.error('%s errors found!', nerr)
269 269  
  270 + inherit = {'ref', 'title', 'database', 'answers_dir',
  271 + 'questions_dir', 'files',
  272 + 'duration', 'autosubmit',
  273 + 'scale_min', 'scale_max', 'show_points',
  274 + 'show_ref', 'debug', }
  275 + # NOT INCLUDED: scale_points, testfile, allow_all, review
  276 +
270 277 return Test({
271   - 'ref': self['ref'],
272   - 'title': self['title'], # title of the test
273   - 'student': student, # student id
274   - 'questions': test, # list of Question instances
275   - 'answers_dir': self['answers_dir'],
276   - 'duration': self['duration'],
277   - 'autosubmit': self['autosubmit'],
278   - 'scale_min': self['scale_min'],
279   - 'scale_max': self['scale_max'],
280   - 'show_points': self['show_points'],
281   - 'show_ref': self['show_ref'],
282   - 'debug': self['debug'], # required by template test.html
283   - 'database': self['database'],
284   - 'questions_dir': self['questions_dir'],
285   - 'files': self['files'],
286   - })
  278 + **{'student': student, 'questions': test},
  279 + **{k:self[k] for k in inherit}})
287 280  
288 281 # ------------------------------------------------------------------------
289 282 def __repr__(self):
... ... @@ -323,14 +316,12 @@ class Test(dict):
323 316 # ------------------------------------------------------------------------
324 317 async def correct(self):
325 318 '''Corrects all the answers of the test and computes the final grade'''
326   -
327 319 self['finish_time'] = datetime.now()
328 320 self['state'] = 'FINISHED'
329 321 grade = 0.0
330 322 for question in self['questions']:
331 323 await question.correct_async()
332 324 grade += question['grade'] * question['points']
333   - # logger.debug(f'Correcting {q["ref"]:>30}: {q["grade"]*100:4.0f}%')
334 325 logger.debug('Correcting %30s: %3g%%',
335 326 question["ref"], question["grade"]*100)
336 327  
... ...