Commit bbfa03c996c64fca5d26a41e50006ba0bd22a5ed

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

- fix README.md error

- refactor several functions in app.py and rename some as private
- add pylint commands
@@ -39,7 +39,7 @@ This file is usually in `~/.config/pip/` in Linux and FreeBSD. In MacOS it's in @@ -39,7 +39,7 @@ This file is usually in `~/.config/pip/` in Linux and FreeBSD. In MacOS it's in
39 Download and install: 39 Download and install:
40 40
41 ```sh 41 ```sh
42 -git clone https://git.xdi.uevora.pt/perguntations.git 42 +git clone https://git.xdi.uevora.pt/mjsb/perguntations.git
43 cd perguntations 43 cd perguntations
44 npm install 44 npm install
45 pip3 install . 45 pip3 install .
@@ -225,7 +225,7 @@ Python packages can be upgraded independently of the rest using pip: @@ -225,7 +225,7 @@ Python packages can be upgraded independently of the rest using pip:
225 225
226 ```sh 226 ```sh
227 pip list --outdated # lists upgradable packages 227 pip list --outdated # lists upgradable packages
228 -pip install -U something # upgrade something 228 +pip install -U something # upgrade something
229 ``` 229 ```
230 230
231 To upgrade perguntations and javascript libraries do: 231 To upgrade perguntations and javascript libraries do:
@@ -2,13 +2,13 @@ @@ -2,13 +2,13 @@
2 "description": "Javascript libraries required to run the server", 2 "description": "Javascript libraries required to run the server",
3 "email": "mjsb@uevora.pt", 3 "email": "mjsb@uevora.pt",
4 "dependencies": { 4 "dependencies": {
5 - "@fortawesome/fontawesome-free": "^5.13.0",  
6 - "bootstrap": "^4.4.1",  
7 - "codemirror": "^5.53.2", 5 + "@fortawesome/fontawesome-free": "^5.15.1",
  6 + "bootstrap": "^4.5.3",
  7 + "codemirror": "^5.58.1",
8 "datatables": "^1.10", 8 "datatables": "^1.10",
9 "jquery": "^3.5.1", 9 "jquery": "^3.5.1",
10 - "mathjax": "^3.0.5", 10 + "mathjax": "^3.1.2",
11 "popper.js": "^1.16.1", 11 "popper.js": "^1.16.1",
12 - "underscore": "^1.10" 12 + "underscore": "^1.11.0"
13 } 13 }
14 } 14 }
perguntations/app.py
@@ -89,23 +89,7 @@ class App(): @@ -89,23 +89,7 @@ class App():
89 self.unfocus = set() # set of students that have no browser focus 89 self.unfocus = set() # set of students that have no browser focus
90 self.area = dict() # {uid: percent_area} 90 self.area = dict() # {uid: percent_area}
91 91
92 - logger.info('Loading test configuration "%s".', conf["testfile"])  
93 - try:  
94 - testconf = load_yaml(conf['testfile'])  
95 - except Exception as exc:  
96 - logger.critical('Error loading test configuration YAML.')  
97 - raise AppException(exc)  
98 -  
99 - testconf.update(conf) # command line options override configuration  
100 -  
101 - # start test factory  
102 - try:  
103 - self.testfactory = TestFactory(testconf)  
104 - except TestFactoryException as exc:  
105 - logger.critical(exc)  
106 - raise AppException('Failed to create test factory!')  
107 - else:  
108 - logger.info('No errors found. Test factory ready.') 92 + self._make_test_factory(conf)
109 93
110 # connect to database and check registered students 94 # connect to database and check registered students
111 dbfile = self.testfactory['database'] 95 dbfile = self.testfactory['database']
@@ -117,26 +101,16 @@ class App(): @@ -117,26 +101,16 @@ class App():
117 num = sess.query(Student).filter(Student.id != '0').count() 101 num = sess.query(Student).filter(Student.id != '0').count()
118 except Exception: 102 except Exception:
119 raise AppException(f'Database unusable {dbfile}.') 103 raise AppException(f'Database unusable {dbfile}.')
120 - else:  
121 - logger.info('Database "%s" has %s students.', dbfile, num) 104 +
  105 + logger.info('Database "%s" has %s students.', dbfile, num)
122 106
123 # command line option --allow-all 107 # command line option --allow-all
124 if conf['allow_all']: 108 if conf['allow_all']:
125 - logger.info('Allowing all students:')  
126 - for student in self.get_all_students():  
127 - self.allow_student(student[0]) 109 + self.allow_all_students()
128 else: 110 else:
129 logger.info('Students not yet allowed to login.') 111 logger.info('Students not yet allowed to login.')
130 112
131 # ------------------------------------------------------------------------ 113 # ------------------------------------------------------------------------
132 - # FIXME unused???  
133 - # def exit(self):  
134 - # if len(self.online) > 1:  
135 - # online_students = ', '.join(self.online)  
136 - # logger.warning(f'Students still online: {online_students}')  
137 - # logger.critical('----------- !!! Server terminated !!! -----------')  
138 -  
139 - # ------------------------------------------------------------------------  
140 async def login(self, uid, try_pw): 114 async def login(self, uid, try_pw):
141 '''login authentication''' 115 '''login authentication'''
142 if uid not in self.allowed and uid != '0': # not allowed 116 if uid not in self.allowed and uid != '0': # not allowed
@@ -175,6 +149,32 @@ class App(): @@ -175,6 +149,32 @@ class App():
175 logger.info('"%s" logged out.', uid) 149 logger.info('"%s" logged out.', uid)
176 150
177 # ------------------------------------------------------------------------ 151 # ------------------------------------------------------------------------
  152 + def _make_test_factory(self, conf):
  153 + '''
  154 + Setup a factory for the test
  155 + '''
  156 +
  157 + # load configuration from yaml file
  158 + logger.info('Loading test configuration "%s".', conf["testfile"])
  159 + try:
  160 + testconf = load_yaml(conf['testfile'])
  161 + except Exception as exc:
  162 + logger.critical('Error loading test configuration YAML.')
  163 + raise AppException(exc)
  164 +
  165 + testconf.update(conf) # command line options override configuration
  166 +
  167 + # start test factory
  168 + logger.info('Making test factory...')
  169 + try:
  170 + self.testfactory = TestFactory(testconf)
  171 + except TestFactoryException as exc:
  172 + logger.critical(exc)
  173 + raise AppException('Failed to create test factory!')
  174 +
  175 + logger.info('Test factory ready. No errors found.')
  176 +
  177 + # ------------------------------------------------------------------------
178 async def generate_test(self, uid): 178 async def generate_test(self, uid):
179 '''generate a test for a given student''' 179 '''generate a test for a given student'''
180 if uid in self.online: 180 if uid in self.online:
@@ -271,11 +271,11 @@ class App(): @@ -271,11 +271,11 @@ class App():
271 '''handles browser events the occur during the test''' 271 '''handles browser events the occur during the test'''
272 if cmd == 'focus': 272 if cmd == 'focus':
273 if value: 273 if value:
274 - self.focus_student(uid) 274 + self._focus_student(uid)
275 else: 275 else:
276 - self.unfocus_student(uid) 276 + self._unfocus_student(uid)
277 elif cmd == 'size': 277 elif cmd == 'size':
278 - self.set_screen_area(uid, value) 278 + self._set_screen_area(uid, value)
279 279
280 # ------------------------------------------------------------------------ 280 # ------------------------------------------------------------------------
281 # --- GETTERS 281 # --- GETTERS
@@ -297,11 +297,11 @@ class App(): @@ -297,11 +297,11 @@ class App():
297 297
298 cols = ['Aluno', 'Início'] + \ 298 cols = ['Aluno', 'Início'] + \
299 [r for question in self.testfactory['questions'] 299 [r for question in self.testfactory['questions']
300 - for r in question['ref']] 300 + for r in question['ref']]
301 301
302 tests = {} 302 tests = {}
303 - for q in grades:  
304 - student, qref, qgrade = q[:2], q[2], q[3] 303 + for question in grades:
  304 + student, qref, qgrade = question[:2], *question[2:]
305 tests.setdefault(student, {})[qref] = qgrade 305 tests.setdefault(student, {})[qref] = qgrade
306 306
307 rows = [{'Aluno': test[0], 'Início': test[1], **q} 307 rows = [{'Aluno': test[0], 'Início': test[1], **q}
@@ -351,13 +351,6 @@ class App(): @@ -351,13 +351,6 @@ class App():
351 .filter_by(id=test_id)\ 351 .filter_by(id=test_id)\
352 .scalar() 352 .scalar()
353 353
354 - def get_all_students(self):  
355 - '''get all students from database'''  
356 - with self.db_session() as sess:  
357 - return sess.query(Student.id, Student.name, Student.password)\  
358 - .filter(Student.id != '0')\  
359 - .order_by(Student.id)  
360 -  
361 def get_student_grades_from_test(self, uid, testid): 354 def get_student_grades_from_test(self, uid, testid):
362 '''get grades of student for a given testid''' 355 '''get grades of student for a given testid'''
363 with self.db_session() as sess: 356 with self.db_session() as sess:
@@ -380,7 +373,15 @@ class App(): @@ -380,7 +373,15 @@ class App():
380 'area': self.area.get(uid, None), 373 'area': self.area.get(uid, None),
381 'grades': self.get_student_grades_from_test( 374 'grades': self.get_student_grades_from_test(
382 uid, self.testfactory['ref']) 375 uid, self.testfactory['ref'])
383 - } for uid, name, pw in self.get_all_students()] 376 + } for uid, name, pw in self._get_all_students()]
  377 +
  378 + # --- private methods ----------------------------------------------------
  379 + def _get_all_students(self):
  380 + '''get all students from database'''
  381 + with self.db_session() as sess:
  382 + return sess.query(Student.id, Student.name, Student.password)\
  383 + .filter(Student.id != '0')\
  384 + .order_by(Student.id)
384 385
385 # def get_allowed_students(self): 386 # def get_allowed_students(self):
386 # # set of 'uid' allowed to login 387 # # set of 'uid' allowed to login
@@ -409,30 +410,30 @@ class App(): @@ -409,30 +410,30 @@ class App():
409 410
410 def allow_all_students(self): 411 def allow_all_students(self):
411 '''allow all students to login''' 412 '''allow all students to login'''
412 - logger.info('Allowing all students...')  
413 - self.allowed.update(s[0] for s in self.get_all_students()) 413 + self.allowed.update(s[0] for s in self._get_all_students())
  414 + logger.info('Allowed all students.')
414 415
415 def deny_all_students(self): 416 def deny_all_students(self):
416 '''deny all students to login''' 417 '''deny all students to login'''
417 logger.info('Denying all students...') 418 logger.info('Denying all students...')
418 self.allowed.clear() 419 self.allowed.clear()
419 420
420 - def focus_student(self, uid): 421 + def _focus_student(self, uid):
421 '''set student in focus state''' 422 '''set student in focus state'''
422 self.unfocus.discard(uid) 423 self.unfocus.discard(uid)
423 logger.info('"%s" focus', uid) 424 logger.info('"%s" focus', uid)
424 425
425 - def unfocus_student(self, uid): 426 + def _unfocus_student(self, uid):
426 '''set student in unfocus state''' 427 '''set student in unfocus state'''
427 self.unfocus.add(uid) 428 self.unfocus.add(uid)
428 logger.info('"%s" unfocus', uid) 429 logger.info('"%s" unfocus', uid)
429 430
430 - def set_screen_area(self, uid, sizes): 431 + def _set_screen_area(self, uid, sizes):
431 '''set current browser area as detected in resize event''' 432 '''set current browser area as detected in resize event'''
432 scr_y, scr_x, win_y, win_x = sizes 433 scr_y, scr_x, win_y, win_x = sizes
433 area = win_x * win_y / (scr_x * scr_y) * 100 434 area = win_x * win_y / (scr_x * scr_y) * 100
434 self.area[uid] = area 435 self.area[uid] = area
435 - logger.info('"%s": area=%g%%, window=%dx%d, screen=%dx%d', 436 + logger.info('"%s" area=%g%%, window=%dx%d, screen=%dx%d',
436 uid, area, win_x, win_y, scr_x, scr_y) 437 uid, area, win_x, win_y, scr_x, scr_y)
437 438
438 async def update_student_password(self, uid, password=''): 439 async def update_student_password(self, uid, password=''):
perguntations/initdb.py
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 2
3 ''' 3 '''
4 -Commandline utilizty to initialize and update student database 4 +Commandline utility to initialize and update student database
5 ''' 5 '''
6 6
7 # base 7 # base
perguntations/main.py
@@ -15,10 +15,10 @@ import sys @@ -15,10 +15,10 @@ import sys
15 # from typing import Any, Dict 15 # from typing import Any, Dict
16 16
17 # this project 17 # this project
18 -from .app import App, AppException  
19 -from .serve import run_webserver  
20 -from .tools import load_yaml  
21 -from . import APP_NAME, APP_VERSION 18 +from perguntations.app import App, AppException
  19 +from perguntations.serve import run_webserver
  20 +from perguntations.tools import load_yaml
  21 +from perguntations import APP_NAME, APP_VERSION
22 22
23 23
24 # ---------------------------------------------------------------------------- 24 # ----------------------------------------------------------------------------
@@ -123,9 +123,8 @@ def main(): @@ -123,9 +123,8 @@ def main():
123 'review': args.review, 123 'review': args.review,
124 } 124 }
125 125
126 - # testapp = App(config)  
127 try: 126 try:
128 - testapp = App(config) 127 + app = App(config)
129 except AppException: 128 except AppException:
130 logging.critical('Failed to start application.') 129 logging.critical('Failed to start application.')
131 sys.exit(-1) 130 sys.exit(-1)
@@ -145,8 +144,7 @@ def main(): @@ -145,8 +144,7 @@ def main():
145 sys.exit(-1) 144 sys.exit(-1)
146 145
147 # --- run webserver ---------------------------------------------------- 146 # --- run webserver ----------------------------------------------------
148 - run_webserver(app=testapp, ssl_opt=ssl_opt, port=args.port,  
149 - debug=args.debug) 147 + run_webserver(app=app, ssl_opt=ssl_opt, port=args.port, debug=args.debug)
150 148
151 149
152 # ---------------------------------------------------------------------------- 150 # ----------------------------------------------------------------------------
perguntations/parser_markdown.py
  1 +
1 ''' 2 '''
2 Parse markdown and generate HTML 3 Parse markdown and generate HTML
3 Includes support for LaTeX formulas 4 Includes support for LaTeX formulas
@@ -25,12 +26,19 @@ logger = logging.getLogger(__name__) @@ -25,12 +26,19 @@ logger = logging.getLogger(__name__)
25 # Block math: $$x$$ or \begin{equation}x\end{equation} 26 # Block math: $$x$$ or \begin{equation}x\end{equation}
26 # ------------------------------------------------------------------------- 27 # -------------------------------------------------------------------------
27 class MathBlockGrammar(mistune.BlockGrammar): 28 class MathBlockGrammar(mistune.BlockGrammar):
  29 + '''
  30 + match block math $$x$$ and math environments begin{} end{}
  31 + '''
  32 + # pylint: disable=too-few-public-methods
28 block_math = re.compile(r"^\$\$(.*?)\$\$", re.DOTALL) 33 block_math = re.compile(r"^\$\$(.*?)\$\$", re.DOTALL)
29 latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}", 34 latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}",
30 re.DOTALL) 35 re.DOTALL)
31 36
32 37
33 class MathBlockLexer(mistune.BlockLexer): 38 class MathBlockLexer(mistune.BlockLexer):
  39 + '''
  40 + parser for block math and latex environment
  41 + '''
34 default_rules = ['block_math', 'latex_environment'] \ 42 default_rules = ['block_math', 'latex_environment'] \
35 + mistune.BlockLexer.default_rules 43 + mistune.BlockLexer.default_rules
36 44
@@ -56,12 +64,19 @@ class MathBlockLexer(mistune.BlockLexer): @@ -56,12 +64,19 @@ class MathBlockLexer(mistune.BlockLexer):
56 64
57 65
58 class MathInlineGrammar(mistune.InlineGrammar): 66 class MathInlineGrammar(mistune.InlineGrammar):
  67 + '''
  68 + match inline math $x$, block math $$x$$ and text
  69 + '''
  70 + # pylint: disable=too-few-public-methods
59 math = re.compile(r"^\$(.+?)\$", re.DOTALL) 71 math = re.compile(r"^\$(.+?)\$", re.DOTALL)
60 block_math = re.compile(r"^\$\$(.+?)\$\$", re.DOTALL) 72 block_math = re.compile(r"^\$\$(.+?)\$\$", re.DOTALL)
61 text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|https?://| {2,}\n|$)') 73 text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|https?://| {2,}\n|$)')
62 74
63 75
64 class MathInlineLexer(mistune.InlineLexer): 76 class MathInlineLexer(mistune.InlineLexer):
  77 + '''
  78 + render output math
  79 + '''
65 default_rules = ['block_math', 'math'] + mistune.InlineLexer.default_rules 80 default_rules = ['block_math', 'math'] + mistune.InlineLexer.default_rules
66 81
67 def __init__(self, renderer, rules=None, **kwargs): 82 def __init__(self, renderer, rules=None, **kwargs):
@@ -70,13 +85,18 @@ class MathInlineLexer(mistune.InlineLexer): @@ -70,13 +85,18 @@ class MathInlineLexer(mistune.InlineLexer):
70 super().__init__(renderer, rules, **kwargs) 85 super().__init__(renderer, rules, **kwargs)
71 86
72 def output_math(self, math): 87 def output_math(self, math):
  88 + '''render inline math'''
73 return self.renderer.inline_math(math.group(1)) 89 return self.renderer.inline_math(math.group(1))
74 90
75 def output_block_math(self, math): 91 def output_block_math(self, math):
  92 + '''render block math'''
76 return self.renderer.block_math(math.group(1)) 93 return self.renderer.block_math(math.group(1))
77 94
78 95
79 class MarkdownWithMath(mistune.Markdown): 96 class MarkdownWithMath(mistune.Markdown):
  97 + '''
  98 + render ouput latex
  99 + '''
80 def __init__(self, renderer, **kwargs): 100 def __init__(self, renderer, **kwargs):
81 if 'inline' not in kwargs: 101 if 'inline' not in kwargs:
82 kwargs['inline'] = MathInlineLexer 102 kwargs['inline'] = MathInlineLexer
@@ -85,19 +105,25 @@ class MarkdownWithMath(mistune.Markdown): @@ -85,19 +105,25 @@ class MarkdownWithMath(mistune.Markdown):
85 super().__init__(renderer, **kwargs) 105 super().__init__(renderer, **kwargs)
86 106
87 def output_block_math(self): 107 def output_block_math(self):
  108 + '''render block math'''
88 return self.renderer.block_math(self.token['text']) 109 return self.renderer.block_math(self.token['text'])
89 110
90 def output_latex_environment(self): 111 def output_latex_environment(self):
  112 + '''render latex environment'''
91 return self.renderer.latex_environment(self.token['name'], 113 return self.renderer.latex_environment(self.token['name'],
92 self.token['text']) 114 self.token['text'])
93 115
94 116
95 class HighlightRenderer(mistune.Renderer): 117 class HighlightRenderer(mistune.Renderer):
  118 + '''
  119 + images, tables, block code
  120 + '''
96 def __init__(self, qref='.'): 121 def __init__(self, qref='.'):
97 super().__init__(escape=True) 122 super().__init__(escape=True)
98 self.qref = qref 123 self.qref = qref
99 124
100 def block_code(self, code, lang='text'): 125 def block_code(self, code, lang='text'):
  126 + '''render code block with syntax highlight'''
101 try: 127 try:
102 lexer = get_lexer_by_name(lang, stripall=False) 128 lexer = get_lexer_by_name(lang, stripall=False)
103 except Exception: 129 except Exception:
@@ -107,6 +133,7 @@ class HighlightRenderer(mistune.Renderer): @@ -107,6 +133,7 @@ class HighlightRenderer(mistune.Renderer):
107 return highlight(code, lexer, formatter) 133 return highlight(code, lexer, formatter)
108 134
109 def table(self, header, body): 135 def table(self, header, body):
  136 + '''render table'''
110 return '<table class="table table-sm"><thead class="thead-light">' \ 137 return '<table class="table table-sm"><thead class="thead-light">' \
111 + header + '</thead><tbody>' + body + '</tbody></table>' 138 + header + '</thead><tbody>' + body + '</tbody></table>'
112 139
@@ -141,14 +168,17 @@ class HighlightRenderer(mistune.Renderer): @@ -141,14 +168,17 @@ class HighlightRenderer(mistune.Renderer):
141 # Pass math through unaltered - mathjax does the rendering in the browser 168 # Pass math through unaltered - mathjax does the rendering in the browser
142 def block_math(self, text): 169 def block_math(self, text):
143 '''bypass block math''' 170 '''bypass block math'''
  171 + # pylint: disable=no-self-use
144 return fr'$$ {text} $$' 172 return fr'$$ {text} $$'
145 173
146 def latex_environment(self, name, text): 174 def latex_environment(self, name, text):
147 '''bypass latex environment''' 175 '''bypass latex environment'''
  176 + # pylint: disable=no-self-use
148 return fr'\begin{{{name}}} {text} \end{{{name}}}' 177 return fr'\begin{{{name}}} {text} \end{{{name}}}'
149 178
150 def inline_math(self, text): 179 def inline_math(self, text):
151 '''bypass inline math''' 180 '''bypass inline math'''
  181 + # pylint: disable=no-self-use
152 return fr'$$$ {text} $$$' 182 return fr'$$$ {text} $$$'
153 183
154 184
perguntations/questions.py
@@ -13,7 +13,7 @@ from typing import Any, Dict, NewType @@ -13,7 +13,7 @@ from typing import Any, Dict, NewType
13 import uuid 13 import uuid
14 14
15 # this project 15 # this project
16 -from .tools import run_script, run_script_async 16 +from perguntations.tools import run_script, run_script_async
17 17
18 # setup logger for this module 18 # setup logger for this module
19 logger = logging.getLogger(__name__) 19 logger = logging.getLogger(__name__)
perguntations/serve.py
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 2
3 ''' 3 '''
4 -Handles the web and html part of the application interface.  
5 -The tornadoweb framework is used. 4 +Handles the web, http & html part of the application interface.
  5 +Uses the tornadoweb framework.
6 ''' 6 '''
7 7
8 8
@@ -40,8 +40,8 @@ class WebApplication(tornado.web.Application): @@ -40,8 +40,8 @@ class WebApplication(tornado.web.Application):
40 (r'/review', ReviewHandler), 40 (r'/review', ReviewHandler),
41 (r'/admin', AdminHandler), 41 (r'/admin', AdminHandler),
42 (r'/file', FileHandler), 42 (r'/file', FileHandler),
43 - # (r'/root', MainHandler), # FIXME  
44 - # (r'/ws', AdminSocketHandler), 43 + # (r'/root', MainHandler),
  44 + # (r'/ws', AdminSocketHandler),
45 (r'/adminwebservice', AdminWebservice), 45 (r'/adminwebservice', AdminWebservice),
46 (r'/studentwebservice', StudentWebservice), 46 (r'/studentwebservice', StudentWebservice),
47 (r'/', RootHandler), 47 (r'/', RootHandler),
@@ -66,7 +66,7 @@ def admin_only(func): @@ -66,7 +66,7 @@ def admin_only(func):
66 Decorator used to restrict access to the administrator. 66 Decorator used to restrict access to the administrator.
67 Example: 67 Example:
68 68
69 - @admin_only() 69 + @admin_only
70 def get(self): ... 70 def get(self): ...
71 ''' 71 '''
72 @functools.wraps(func) 72 @functools.wraps(func)
@@ -78,6 +78,7 @@ def admin_only(func): @@ -78,6 +78,7 @@ def admin_only(func):
78 78
79 79
80 # ---------------------------------------------------------------------------- 80 # ----------------------------------------------------------------------------
  81 +# pylint: disable=abstract-method
81 class BaseHandler(tornado.web.RequestHandler): 82 class BaseHandler(tornado.web.RequestHandler):
82 ''' 83 '''
83 Handlers should inherit this one instead of tornado.web.RequestHandler. 84 Handlers should inherit this one instead of tornado.web.RequestHandler.
@@ -87,7 +88,7 @@ class BaseHandler(tornado.web.RequestHandler): @@ -87,7 +88,7 @@ class BaseHandler(tornado.web.RequestHandler):
87 88
88 @property 89 @property
89 def testapp(self): 90 def testapp(self):
90 - '''simplifies access to the application''' 91 + '''simplifies access to the application a little bit'''
91 return self.application.testapp 92 return self.application.testapp
92 93
93 def get_current_user(self): 94 def get_current_user(self):
@@ -158,6 +159,8 @@ class BaseHandler(tornado.web.RequestHandler): @@ -158,6 +159,8 @@ class BaseHandler(tornado.web.RequestHandler):
158 # AdminSocketHandler.update_cache(chat) # store msgs 159 # AdminSocketHandler.update_cache(chat) # store msgs
159 # AdminSocketHandler.send_updates(chat) # send to clients 160 # AdminSocketHandler.send_updates(chat) # send to clients
160 161
  162 +# ----------------------------------------------------------------------------
  163 +# pylint: disable=abstract-method
161 class StudentWebservice(BaseHandler): 164 class StudentWebservice(BaseHandler):
162 ''' 165 '''
163 Receive ajax from students in the test in response from focus, unfocus and 166 Receive ajax from students in the test in response from focus, unfocus and
@@ -174,6 +177,7 @@ class StudentWebservice(BaseHandler): @@ -174,6 +177,7 @@ class StudentWebservice(BaseHandler):
174 177
175 178
176 # ---------------------------------------------------------------------------- 179 # ----------------------------------------------------------------------------
  180 +# pylint: disable=abstract-method
177 class AdminWebservice(BaseHandler): 181 class AdminWebservice(BaseHandler):
178 ''' 182 '''
179 Receive ajax requests from admin 183 Receive ajax requests from admin
@@ -202,6 +206,7 @@ class AdminWebservice(BaseHandler): @@ -202,6 +206,7 @@ class AdminWebservice(BaseHandler):
202 206
203 207
204 # ---------------------------------------------------------------------------- 208 # ----------------------------------------------------------------------------
  209 +# pylint: disable=abstract-method
205 class AdminHandler(BaseHandler): 210 class AdminHandler(BaseHandler):
206 '''Handle /admin''' 211 '''Handle /admin'''
207 212
@@ -260,6 +265,7 @@ class AdminHandler(BaseHandler): @@ -260,6 +265,7 @@ class AdminHandler(BaseHandler):
260 265
261 266
262 # ---------------------------------------------------------------------------- 267 # ----------------------------------------------------------------------------
  268 +# pylint: disable=abstract-method
263 class LoginHandler(BaseHandler): 269 class LoginHandler(BaseHandler):
264 '''Handle /login''' 270 '''Handle /login'''
265 271
@@ -282,6 +288,7 @@ class LoginHandler(BaseHandler): @@ -282,6 +288,7 @@ class LoginHandler(BaseHandler):
282 288
283 289
284 # ---------------------------------------------------------------------------- 290 # ----------------------------------------------------------------------------
  291 +# pylint: disable=abstract-method
285 class LogoutHandler(BaseHandler): 292 class LogoutHandler(BaseHandler):
286 '''Handle /logout''' 293 '''Handle /logout'''
287 294
@@ -296,6 +303,7 @@ class LogoutHandler(BaseHandler): @@ -296,6 +303,7 @@ class LogoutHandler(BaseHandler):
296 303
297 304
298 # ---------------------------------------------------------------------------- 305 # ----------------------------------------------------------------------------
  306 +# pylint: disable=abstract-method
299 class RootHandler(BaseHandler): 307 class RootHandler(BaseHandler):
300 ''' 308 '''
301 Handles / to redirect students and admin to /test and /admin, resp. 309 Handles / to redirect students and admin to /test and /admin, resp.
@@ -315,6 +323,7 @@ class RootHandler(BaseHandler): @@ -315,6 +323,7 @@ class RootHandler(BaseHandler):
315 # ---------------------------------------------------------------------------- 323 # ----------------------------------------------------------------------------
316 # Serves files from the /public subdir of the topics. 324 # Serves files from the /public subdir of the topics.
317 # ---------------------------------------------------------------------------- 325 # ----------------------------------------------------------------------------
  326 +# pylint: disable=abstract-method
318 class FileHandler(BaseHandler): 327 class FileHandler(BaseHandler):
319 ''' 328 '''
320 Handles static files from questions like images, etc. 329 Handles static files from questions like images, etc.
@@ -366,6 +375,7 @@ class FileHandler(BaseHandler): @@ -366,6 +375,7 @@ class FileHandler(BaseHandler):
366 # ---------------------------------------------------------------------------- 375 # ----------------------------------------------------------------------------
367 # Test shown to students 376 # Test shown to students
368 # ---------------------------------------------------------------------------- 377 # ----------------------------------------------------------------------------
  378 +# pylint: disable=abstract-method
369 class TestHandler(BaseHandler): 379 class TestHandler(BaseHandler):
370 ''' 380 '''
371 Generates test to student. 381 Generates test to student.
@@ -438,20 +448,8 @@ class TestHandler(BaseHandler): @@ -438,20 +448,8 @@ class TestHandler(BaseHandler):
438 self.render('grade.html', t=test, allgrades=allgrades) 448 self.render('grade.html', t=test, allgrades=allgrades)
439 449
440 450
441 -# ----------------------------------------------------------------------------  
442 -# FIXME should be a post in the test with command giveup instead of correct...  
443 -# class GiveupHandler(BaseHandler):  
444 -# @tornado.web.authenticated  
445 -# def get(self):  
446 -# uid = self.current_user  
447 -# t = self.testapp.giveup_test(uid)  
448 -# self.testapp.logout(uid)  
449 -  
450 -# # --- Show result to student  
451 -# self.render('grade.html', t=t, allgrades=self.testapp.get_student_grades_from_all_tests(uid))  
452 -  
453 -  
454 # --- REVIEW ----------------------------------------------------------------- 451 # --- REVIEW -----------------------------------------------------------------
  452 +# pylint: disable=abstract-method
455 class ReviewHandler(BaseHandler): 453 class ReviewHandler(BaseHandler):
456 ''' 454 '''
457 Show test for review 455 Show test for review
@@ -488,18 +486,20 @@ class ReviewHandler(BaseHandler): @@ -488,18 +486,20 @@ class ReviewHandler(BaseHandler):
488 with open(path.expanduser(fname)) as jsonfile: 486 with open(path.expanduser(fname)) as jsonfile:
489 test = json.load(jsonfile) 487 test = json.load(jsonfile)
490 except OSError: 488 except OSError:
491 - logging.error('Cannot open "%s" for review.', fname)  
492 - raise tornado.web.HTTPError(404) # Not Found 489 + msg = f'Cannot open "{fname}" for review.'
  490 + logging.error(msg)
  491 + raise tornado.web.HTTPError(status_code=404, reason=msg) from None
493 except json.JSONDecodeError as exc: 492 except json.JSONDecodeError as exc:
494 - logging.error('JSON error in "%s": %s', fname, exc)  
495 - raise tornado.web.HTTPError(404) # Not Found 493 + msg = f'JSON error in "{fname}": {exc}'
  494 + logging.error(msg)
  495 + raise tornado.web.HTTPError(status_code=404, reason=msg)
496 496
497 self.render('review.html', t=test, md=md_to_html, 497 self.render('review.html', t=test, md=md_to_html,
498 - templ=self._templates) 498 + templ=self._templates)
499 499
500 500
501 # ---------------------------------------------------------------------------- 501 # ----------------------------------------------------------------------------
502 -def signal_handler(sig, frame): 502 +def signal_handler(*_):
503 ''' 503 '''
504 Catches Ctrl-C and stops webserver 504 Catches Ctrl-C and stops webserver
505 ''' 505 '''
perguntations/static/js/admin.js
@@ -119,7 +119,7 @@ $(document).ready(function() { @@ -119,7 +119,7 @@ $(document).ready(function() {
119 var checked = d['allowed'] ? 'checked' : ''; 119 var checked = d['allowed'] ? 'checked' : '';
120 var password_defined = d['password_defined'] ? ' <span class="badge badge-secondary"><i class="fa fa-key" aria-hidden="true"></i></span>' : ''; 120 var password_defined = d['password_defined'] ? ' <span class="badge badge-secondary"><i class="fa fa-key" aria-hidden="true"></i></span>' : '';
121 var hora_inicio = d['start_time'] ? ' <span class="badge badge-success"><i class="fas fa-hourglass-start"></i> ' + d['start_time'].slice(11,16) + '</span>': ''; 121 var hora_inicio = d['start_time'] ? ' <span class="badge badge-success"><i class="fas fa-hourglass-start"></i> ' + d['start_time'].slice(11,16) + '</span>': '';
122 - var unfocus = d['unfocus']? ' <span class="badge badge-danger">unfocus</span>' : ''; 122 + var unfocus = d['unfocus'] ? ' <span class="badge badge-danger">unfocus</span>' : '';
123 var area = ''; 123 var area = '';
124 if (d['start_time'] ) { 124 if (d['start_time'] ) {
125 if (d['area'] > 75) 125 if (d['area'] > 75)
perguntations/templates/test.html
@@ -44,7 +44,7 @@ @@ -44,7 +44,7 @@
44 <!-- ===================================================================== --> 44 <!-- ===================================================================== -->
45 <body> 45 <body>
46 <!-- ===================================================================== --> 46 <!-- ===================================================================== -->
47 -<div class="progress fixed-top" style="height: 60px; border-radius: 0px;"> 47 +<div class="progress fixed-top" style="height: 61px; border-radius: 0px;">
48 <div class="progress-bar bg-secondary" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div> 48 <div class="progress-bar bg-secondary" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div>
49 </div> 49 </div>
50 50