Commit 1323bb4d0f868538ee9229ec405321deefc3a12c
1 parent
1db56dc6
Exists in
master
and in
1 other branch
fix pylint warnings
Showing
3 changed files
with
179 additions
and
153 deletions
Show diff stats
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 | ... | ... |