diff --git a/aprendizations/initdb.py b/aprendizations/initdb.py index 14c4fb9..e5e9883 100644 --- a/aprendizations/initdb.py +++ b/aprendizations/initdb.py @@ -84,25 +84,24 @@ def get_students_from_csv(filename): 'skipinitialspace': True, } + students = [] try: with open(filename, encoding='iso-8859-1') as file: csvreader = csv.DictReader(file, **csv_settings) students = [{ 'uid': s['N.ยบ'], 'name': capwords(re.sub(r'\(.*\)', '', s['Nome']).strip()) - } for s in csvreader] + } for s in csvreader] except OSError: print(f'!!! Error reading file "{filename}" !!!') - students = [] except csv.Error: print(f'!!! Error parsing CSV from "{filename}" !!!') - students = [] return students # =========================================================================== -def show_students_in_database(session, verbose=False): +def show_students_in_database(session, verbose=False) -> None: '''shows students in the database''' users = session.execute(select(Student)).scalars().all() total = len(users) @@ -136,7 +135,7 @@ def main(): Base.metadata.create_all(engine) # Creates schema if needed session = Session(engine, future=True) - # --- build list of students to insert/update + # --- make list of students to insert/update students = [] for csvfile in args.csvfile: @@ -145,24 +144,22 @@ def main(): if args.admin: students.append({'uid': '0', 'name': 'Admin'}) - if args.add: + if args.add is not None: for uid, name in args.add: students.append({'uid': uid, 'name': name}) # --- only insert students that are not yet in the database print('\nInserting new students:') + db_students = session.execute(select(Student.id)).scalars().all() + new_students = [s for s in students if s['uid'] not in set(db_students)] + for student in new_students: + print(f' {student["uid"]}, {student["name"]}') - db_students = set(session.execute(select(Student.id)).scalars().all()) - new_students = (s for s in students if s['uid'] not in db_students) - count = 0 - for s in new_students: - print(f' {s["uid"]}, {s["name"]}') - - pw = args.pw or s['uid'] - hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) - - session.add(Student(id=s['uid'], name=s['name'], password=hashed_pw)) - count += 1 + passwd = args.pw or student['uid'] + hashed_pw = bcrypt.hashpw(passwd.encode('utf-8'), bcrypt.gensalt()) + session.add(Student(id=student['uid'], + name=student['name'], + password=hashed_pw)) try: session.commit() @@ -170,23 +167,24 @@ def main(): print('!!! Integrity error. Aborted !!!\n') session.rollback() else: - print(f'Total {count} new student(s).') + print(f'Total {len(new_students)} new student(s).') - # --- update data for student in the database + # --- update data for students in the database if args.update: print('\nUpdating passwords of students:') count = 0 for sid in args.update: try: - s = session.execute(select(Student).filter_by(id=sid)).scalar_one() + query = select(Student).filter_by(id=sid) + student = session.execute(query).scalar_one() except NoResultFound: print(f' -> student {sid} does not exist!') continue - else: - print(f' {sid}, {s.name}') - count += 1 - pw = args.pw or sid - s.password = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) + count += 1 + print(f' {sid}, {student.name}') + passwd = (args.pw or sid).encode('utf-8') + student.password = bcrypt.hashpw(passwd, bcrypt.gensalt()) + session.commit() print(f'Total {count} password(s) updated.') diff --git a/aprendizations/learnapp.py b/aprendizations/learnapp.py index 5c3e410..89a1187 100644 --- a/aprendizations/learnapp.py +++ b/aprendizations/learnapp.py @@ -66,11 +66,11 @@ class LearnApp(): def __init__(self, courses: str, # filename with course configurations prefix: str, # path to topics - db: str, # database filename + dbase: str, # database filename check: bool = False) -> None: - self._db_setup(db) # setup database and check students - self.online: Dict[str, Dict] = dict() # online students + self._db_setup(dbase) # setup database and check students + self.online: Dict[str, Dict] = {} # online students try: config: Dict[str, Any] = load_yaml(courses) @@ -91,7 +91,6 @@ class LearnApp(): # load other course files with the topics the their deps for course_file in config.get('topics_from', []): course_conf = load_yaml(course_file) # course configuration - # FIXME set defaults?? logger.info('%6d topics from %s', len(course_conf["topics"]), course_file) self._populate_graph(course_conf) @@ -129,10 +128,10 @@ class LearnApp(): logger.info('Starting sanity checks (may take a while...)') errors: int = 0 - for qref in self.factory: + for qref, qfactory in self.factory.items(): logger.debug('checking %s...', qref) try: - question = self.factory[qref].generate() + question = qfactory.generate() except QuestionException as exc: logger.error(exc) errors += 1 @@ -334,9 +333,6 @@ class LearnApp(): '''Start new topic''' student = self.online[uid]['state'] - # if uid == '0': - # logger.warning('Reloading "%s"', topic) # FIXME should be an option - # self.factory.update(self._factory_for(topic)) try: await student.start_topic(topic) @@ -420,7 +416,7 @@ class LearnApp(): topic = self.deps.nodes[tref] # get current topic node topic['name'] = attr.get('name', tref) - topic['questions'] = attr.get('questions', []) # FIXME unused?? + topic['questions'] = attr.get('questions', []) for k, default in defaults.items(): topic[k] = attr.get(k, default) @@ -440,7 +436,7 @@ class LearnApp(): ''' logger.info('Building questions factory:') - factory = dict() + factory = {} for tref in self.deps.nodes: factory.update(self._factory_for(tref)) @@ -451,7 +447,7 @@ class LearnApp(): # makes factory for a single topic # ------------------------------------------------------------------------ def _factory_for(self, tref: str) -> Dict[str, QFactory]: - factory: Dict[str, QFactory] = dict() + factory: Dict[str, QFactory] = {} topic = self.deps.nodes[tref] # get node # load questions as list of dicts try: @@ -509,7 +505,7 @@ class LearnApp(): return factory def get_login_counter(self, uid: str) -> int: - '''login counter''' # FIXME + '''login counter''' return int(self.online[uid]['counter']) def get_student_name(self, uid: str) -> str: @@ -602,14 +598,16 @@ class LearnApp(): student_topics = session.execute(query_student_topics).all() # compute topic progress - now = datetime.now() - goals = self.courses[cid]['goals'] progress: DefaultDict[str, float] = defaultdict(int) + goals = self.courses[cid]['goals'] + num_goals = len(goals) + now = datetime.now() for student, topic, level, datestr in student_topics: if topic in goals: date = datetime.strptime(datestr, "%Y-%m-%d %H:%M:%S.%f") - progress[student] += level**(now - date).days / len(goals) + elapsed_days = (now - date).days + progress[student] += level**elapsed_days / num_goals return sorted(((u, name, progress[u]) for u, name in students diff --git a/aprendizations/main.py b/aprendizations/main.py index fe9c1e1..14e9514 100644 --- a/aprendizations/main.py +++ b/aprendizations/main.py @@ -182,7 +182,7 @@ def main(): try: learnapp = LearnApp(courses=arg.courses, prefix=arg.prefix, - db=arg.db, + dbase=arg.db, check=arg.check) except DatabaseUnusableError: logging.critical('Failed to start application.') @@ -197,10 +197,7 @@ def main(): sep='\n') sys.exit(1) except LearnException as exc: - logging.critical('Failed to start backend') - sys.exit(1) - except Exception: - logging.critical('Unknown error') + logging.critical('Failed to start backend: %s', str(exc)) sys.exit(1) else: logging.info('LearnApp started') diff --git a/aprendizations/serve.py b/aprendizations/serve.py index 5c878c1..4d32973 100644 --- a/aprendizations/serve.py +++ b/aprendizations/serve.py @@ -4,10 +4,9 @@ Webserver # python standard library -import asyncio import base64 import functools -import logging +from logging import getLogger import mimetypes from os.path import join, dirname, expanduser import signal @@ -28,7 +27,7 @@ from aprendizations import APP_NAME # setup logger for this module -logger = logging.getLogger(__name__) +logger = getLogger(__name__) # ---------------------------------------------------------------------------- @@ -37,7 +36,7 @@ def admin_only(func): Decorator used to restrict access to the administrator ''' @functools.wraps(func) - def wrapper(self, *args, **kwargs): + def wrapper(self, *args, **kwargs) -> None: if self.current_user != '0': raise tornado.web.HTTPError(403) # forbidden func(self, *args, **kwargs) @@ -49,7 +48,7 @@ class WebApplication(tornado.web.Application): ''' WebApplication - Tornado Web Server ''' - def __init__(self, learnapp, debug=False): + def __init__(self, learnapp, debug=False) -> None: handlers = [ (r'/login', LoginHandler), (r'/logout', LogoutHandler), @@ -94,7 +93,6 @@ class BaseHandler(tornado.web.RequestHandler): counter_cookie = self.get_secure_cookie('counter') if user_cookie is not None: uid = user_cookie.decode('utf-8') - if counter_cookie is not None: counter = counter_cookie.decode('utf-8') if counter == str(self.learn.get_login_counter(uid)): @@ -108,7 +106,7 @@ class RankingsHandler(BaseHandler): Handles rankings page ''' @tornado.web.authenticated - def get(self): + def get(self) -> None: ''' Renders list of students that have answers in this course. ''' @@ -122,18 +120,17 @@ class RankingsHandler(BaseHandler): name=self.learn.get_student_name(uid), rankings=rankings, course_id=course_id, - course_title=self.learn.get_student_course_title(uid), # FIXME get from course var + course_title=self.learn.get_student_course_title(uid), + # FIXME get from course var ) # ---------------------------------------------------------------------------- -# -# ---------------------------------------------------------------------------- class LoginHandler(BaseHandler): ''' Handles /login ''' - def get(self): + def get(self) -> None: ''' Renders login page ''' @@ -168,7 +165,7 @@ class LogoutHandler(BaseHandler): Handles /logout ''' @tornado.web.authenticated - def get(self): + def get(self) -> None: ''' clears cookies and removes user session ''' @@ -176,7 +173,7 @@ class LogoutHandler(BaseHandler): self.clear_cookie('counter') self.redirect('/') - def on_finish(self): + def on_finish(self) -> None: self.learn.logout(self.current_user) @@ -186,7 +183,7 @@ class ChangePasswordHandler(BaseHandler): Handles password change ''' @tornado.web.authenticated - async def post(self): + async def post(self) -> None: ''' Tries to perform password change and then replies success/fail status ''' @@ -216,7 +213,7 @@ class RootHandler(BaseHandler): Handles root / ''' @tornado.web.authenticated - def get(self): + def get(self) -> None: '''Simply redirects to the main entrypoint''' self.redirect('/courses') @@ -226,11 +223,11 @@ class CoursesHandler(BaseHandler): ''' Handles /courses ''' - def set_default_headers(self, *args, **kwargs): + def set_default_headers(self, *_) -> None: self.set_header('Cache-Control', 'no-cache') @tornado.web.authenticated - def get(self): + def get(self) -> None: '''Renders list of available courses''' uid = self.current_user self.render('courses.html', @@ -249,7 +246,7 @@ class CourseHandler(BaseHandler): ''' @tornado.web.authenticated - def get(self, course_id): + def get(self, course_id) -> None: ''' Handles get /course/... Starts a given course and show list of topics @@ -278,11 +275,11 @@ class TopicHandler(BaseHandler): ''' Handles a topic ''' - def set_default_headers(self, *args, **kwargs): + def set_default_headers(self, *_) -> None: self.set_header('Cache-Control', 'no-cache') @tornado.web.authenticated - async def get(self, topic): + async def get(self, topic) -> None: ''' Handles get /topic/... Starts a given topic @@ -308,7 +305,7 @@ class FileHandler(BaseHandler): Serves files from the /public subdir of the topics. ''' @tornado.web.authenticated - async def get(self, filename): + async def get(self, filename) -> None: ''' Serves files from /public subdirectories of a particular topic ''' @@ -351,7 +348,7 @@ class QuestionHandler(BaseHandler): # ------------------------------------------------------------------------ @tornado.web.authenticated - async def get(self): + async def get(self) -> None: ''' Gets question to render. Shows an animated trophy if there are no more questions in the topic. @@ -489,7 +486,7 @@ def signal_handler(*_) -> None: reply = input(' --> Stop webserver? (yes/no) ') if reply.lower() == 'yes': tornado.ioloop.IOLoop.current().stop() - logging.critical('Webserver stopped.') + logger.critical('Webserver stopped.') sys.exit(0) @@ -503,10 +500,9 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: try: webapp = WebApplication(app, debug=debug) except Exception: - logger.critical('Failed to start web application.') + logger.critical('Failed to start web application.', exc_info=True) sys.exit(1) - else: - logger.info('Web application started (tornado.web.Application)') + logger.info('Web application started (tornado.web.Application)') # --- create tornado webserver try: @@ -514,19 +510,17 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: except ValueError: logger.critical('Certificates cert.pem and privkey.pem not found') sys.exit(1) - else: - logger.debug('HTTP server started') + logger.debug('HTTP server started') try: httpserver.listen(port) except OSError: logger.critical('Cannot bind port %d. Already in use?', port) sys.exit(1) - - # --- run webserver logger.info('Webserver listening on %d... (Ctrl-C to stop)', port) signal.signal(signal.SIGINT, signal_handler) + # --- run webserver try: tornado.ioloop.IOLoop.current().start() # running... except Exception: diff --git a/setup.py b/setup.py index 2e0245d..17f2b63 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( url="https://git.xdi.uevora.pt/mjsb/aprendizations.git", packages=find_packages(), include_package_data=True, # install files from MANIFEST.in - python_requires='>=3.8.*', + python_requires='>=3.9.*', install_requires=[ 'tornado>=6.0', 'mistune<2', -- libgit2 0.21.2