Commit bbfa03c996c64fca5d26a41e50006ba0bd22a5ed
1 parent
8ce81519
Exists in
master
and in
1 other branch
- fix README.md error
- refactor several functions in app.py and rename some as private - add pylint commands
Showing
10 changed files
with
123 additions
and
94 deletions
Show diff stats
README.md
@@ -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: |
package.json
@@ -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
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 |