Commit 3aa7e128221d42eafb5d60e19c2dd19a235c5bb1

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

- usable version!

- mistune for markdown rendering.
- change password is working.
- other fixes.
1 1
2 BUGS: 2 BUGS:
3 3
4 -- change password in maintopics.html, falta menu para lançar modal 4 +- indentação da primeira linha de código não funciona.
  5 +- submeter questoes radio, da erro se nao escolher nenhuma opção.
  6 +- gravar evolucao na bd no final de cada topico.
  7 +- servir imagens/ficheiros.
  8 +- topicos virtuais nao deveriam aparecer. na construção da árvore os sucessores seriam ligados directamente aos predecessores.
  9 +
5 10
6 - reportar comentarios após submeter. 11 - reportar comentarios após submeter.
7 - cada topico tem uma pagina begin e uma end 12 - cada topico tem uma pagina begin e uma end
@@ -26,6 +31,8 @@ TODO: @@ -26,6 +31,8 @@ TODO:
26 31
27 FIXED: 32 FIXED:
28 33
  34 +- markdown com o mistune.
  35 +- change password in maintopics.html, falta menu para lançar modal
29 - ver documentacao de migracao para networkx 2.0 https://networkx.github.io/documentation/stable/release/migration_guide_from_1.x_to_2.0.html 36 - ver documentacao de migracao para networkx 2.0 https://networkx.github.io/documentation/stable/release/migration_guide_from_1.x_to_2.0.html
30 - script para adicionar users/reset passwords. 37 - script para adicionar users/reset passwords.
31 - os topicos locked devem estar inactivos no sidebar. 38 - os topicos locked devem estar inactivos no sidebar.
demo/demo.yaml
@@ -19,6 +19,6 @@ topics: @@ -19,6 +19,6 @@ topics:
19 19
20 # topic with one dependency 20 # topic with one dependency
21 solar_system: 21 solar_system:
22 - name: Solar system 22 + name: Sistema solar
23 deps: 23 deps:
24 - math 24 - math
@@ -37,6 +37,7 @@ class StudentKnowledge(object): @@ -37,6 +37,7 @@ class StudentKnowledge(object):
37 # compute recommended sequence of topics ['a', 'b', ...] 37 # compute recommended sequence of topics ['a', 'b', ...]
38 self.topic_sequence = list(nx.topological_sort(self.deps)) 38 self.topic_sequence = list(nx.topological_sort(self.deps))
39 39
  40 + self.unlock_topics()
40 41
41 # ------------------------------------------------------------------------ 42 # ------------------------------------------------------------------------
42 # Unlock topics whose dependencies are satisfied (> min_level) 43 # Unlock topics whose dependencies are satisfied (> min_level)
@@ -90,7 +91,6 @@ class StudentKnowledge(object): @@ -90,7 +91,6 @@ class StudentKnowledge(object):
90 91
91 if not topic: 92 if not topic:
92 topic = self.recommended_topic() 93 topic = self.recommended_topic()
93 - print(f'recommended {topic}')  
94 94
95 self.current_topic = topic 95 self.current_topic = topic
96 logger.info(f'Topic set to "{topic}"') 96 logger.info(f'Topic set to "{topic}"')
@@ -112,10 +112,17 @@ class StudentKnowledge(object): @@ -112,10 +112,17 @@ class StudentKnowledge(object):
112 logger.debug('StudentKnowledge.check_answer()') 112 logger.debug('StudentKnowledge.check_answer()')
113 113
114 q = self.current_question 114 q = self.current_question
  115 +
  116 + # answers are returned from tornado in a list
  117 + if q['type'] in ('success', 'information', 'info'): # FIXME danger...
  118 + q['answer'] = None
  119 + elif q['type'] != 'checkbox':
  120 + q['answer'] = answer[0]
  121 + else:
  122 + q['answer'] = answer
  123 +
115 q['finish_time'] = datetime.now() 124 q['finish_time'] = datetime.now()
116 - q['answer'] = answer  
117 grade = q.correct() 125 grade = q.correct()
118 -  
119 logger.debug(f'Grade = {grade:.2} ({q["ref"]})') 126 logger.debug(f'Grade = {grade:.2} ({q["ref"]})')
120 127
121 # if answer is correct, get next question 128 # if answer is correct, get next question
@@ -124,17 +131,18 @@ class StudentKnowledge(object): @@ -124,17 +131,18 @@ class StudentKnowledge(object):
124 try: 131 try:
125 self.current_question = self.questions.pop(0) # FIXME empty? 132 self.current_question = self.questions.pop(0) # FIXME empty?
126 except IndexError: 133 except IndexError:
  134 + # finished topic, no more questions
127 self.current_question = None 135 self.current_question = None
128 self.state[self.current_topic] = { 136 self.state[self.current_topic] = {
129 'level': 1.0, 137 'level': 1.0,
130 'date': datetime.now() 138 'date': datetime.now()
131 } 139 }
  140 + self.unlock_topics()
132 else: 141 else:
133 self.current_question['start_time'] = datetime.now() 142 self.current_question['start_time'] = datetime.now()
134 143
135 # if answer is wrong, keep same question and add a similar one at the end 144 # if answer is wrong, keep same question and add a similar one at the end
136 else: 145 else:
137 - print('failed')  
138 factory = self.deps.node[self.current_topic]['factory'] 146 factory = self.deps.node[self.current_topic]['factory']
139 self.questions.append(factory[q['ref']].generate()) 147 self.questions.append(factory[q['ref']].generate())
140 148
@@ -19,7 +19,7 @@ from tornado import template, gen @@ -19,7 +19,7 @@ from tornado import template, gen
19 19
20 # this project 20 # this project
21 from learnapp import LearnApp 21 from learnapp import LearnApp
22 -from tools import load_yaml, md 22 +from tools import load_yaml, md_to_html
23 23
24 24
25 # ============================================================================ 25 # ============================================================================
@@ -30,11 +30,11 @@ class WebApplication(tornado.web.Application): @@ -30,11 +30,11 @@ class WebApplication(tornado.web.Application):
30 handlers = [ 30 handlers = [
31 (r'/login', LoginHandler), 31 (r'/login', LoginHandler),
32 (r'/logout', LogoutHandler), 32 (r'/logout', LogoutHandler),
33 - # (r'/change_password', ChangePasswordHandler), 33 + (r'/change_password', ChangePasswordHandler),
34 (r'/question', QuestionHandler), # each question 34 (r'/question', QuestionHandler), # each question
35 (r'/topic/(.+)', TopicHandler), # page for doing a topic 35 (r'/topic/(.+)', TopicHandler), # page for doing a topic
36 (r'/', RootHandler), # show list of topics 36 (r'/', RootHandler), # show list of topics
37 - # (r'/(.+)', FileHandler), 37 + # (r'/file/(.+)', FileHandler),
38 ] 38 ]
39 settings = { 39 settings = {
40 'template_path': path.join(path.dirname(__file__), 'templates'), 40 'template_path': path.join(path.dirname(__file__), 'templates'),
@@ -174,13 +174,9 @@ class QuestionHandler(BaseHandler): @@ -174,13 +174,9 @@ class QuestionHandler(BaseHandler):
174 } 174 }
175 175
176 def new_question(self, user): 176 def new_question(self, user):
177 - logger.debug(f'new_question({user})')  
178 - # state = self.learn.get_student_state(user) # [{ref, name, level},...]  
179 - # current_topic = self.learn.get_student_topic(user) # str  
180 -  
181 question = self.learn.get_student_question(user) # Question 177 question = self.learn.get_student_question(user) # Question
182 template = self.templates[question['type']] 178 template = self.templates[question['type']]
183 - question_html = self.render_string(template, question=question, md=md) 179 + question_html = self.render_string(template, question=question, md=md_to_html)
184 180
185 return { 181 return {
186 'method': 'new_question', 182 'method': 'new_question',
@@ -191,7 +187,6 @@ class QuestionHandler(BaseHandler): @@ -191,7 +187,6 @@ class QuestionHandler(BaseHandler):
191 } 187 }
192 188
193 def wrong_answer(self, user): 189 def wrong_answer(self, user):
194 - logger.debug(f'wrong_answer({user})')  
195 progress = self.learn.get_student_progress(user) # in the current topic 190 progress = self.learn.get_student_progress(user) # in the current topic
196 return { 191 return {
197 'method': 'shake', 192 'method': 'shake',
@@ -200,17 +195,12 @@ class QuestionHandler(BaseHandler): @@ -200,17 +195,12 @@ class QuestionHandler(BaseHandler):
200 } 195 }
201 } 196 }
202 197
203 - def finished_topic(self, user):  
204 - logger.debug(f'finished_topic({user})')  
205 -  
206 - # state = self.learn.get_student_state(user) # all topics  
207 - # current_topic = self.learn.get_student_topic(uid)  
208 - 198 + def finished_topic(self, user): # FIXME user unused
209 return { 199 return {
210 'method': 'finished_topic', 200 'method': 'finished_topic',
211 'params': { 201 'params': {
212 - 'question': '<img src="/static/trophy.png" alt="trophy" class="img-fluid mx-auto d-block" width="30%">',  
213 - 'progress': 1.0, 202 + 'question': f'<img src="/static/trophy.png" alt="trophy" class="img-fluid mx-auto d-block" width="30%">',
  203 + # 'progress': 1.0,
214 } 204 }
215 } 205 }
216 206
@@ -221,7 +211,7 @@ class QuestionHandler(BaseHandler): @@ -221,7 +211,7 @@ class QuestionHandler(BaseHandler):
221 211
222 question = self.learn.get_student_question(user) 212 question = self.learn.get_student_question(user)
223 template = self.templates[question['type']] 213 template = self.templates[question['type']]
224 - question_html = self.render_string(template, question=question, md=md) 214 + question_html = self.render_string(template, question=question, md=md_to_html)
225 215
226 self.write({ 216 self.write({
227 'method': 'new_question', 217 'method': 'new_question',
@@ -237,12 +227,9 @@ class QuestionHandler(BaseHandler): @@ -237,12 +227,9 @@ class QuestionHandler(BaseHandler):
237 logging.debug('QuestionHandler.post()') 227 logging.debug('QuestionHandler.post()')
238 user = self.current_user 228 user = self.current_user
239 229
240 - print(self.get_body_arguments())  
241 - # if self.learn.get_student_question(user) is None:  
242 -  
243 - answer = self.get_body_argument('answer')  
244 -  
245 # check answer and get next question (same, new or None) 230 # check answer and get next question (same, new or None)
  231 + answer = self.get_body_arguments('answer') # list
  232 + print(answer)
246 grade = self.learn.check_answer(user, answer) 233 grade = self.learn.check_answer(user, answer)
247 question = self.learn.get_student_question(user) 234 question = self.learn.get_student_question(user)
248 235
templates/maintopics.html
@@ -35,17 +35,26 @@ @@ -35,17 +35,26 @@
35 <span class="navbar-toggler-icon"></span> 35 <span class="navbar-toggler-icon"></span>
36 </button> 36 </button>
37 37
38 - <div class="collapse navbar-collapse" id="navbarText">  
39 - <ul class="navbar-nav mr-auto">  
40 - </ul>  
41 -  
42 - <span class="navbar-text">  
43 - <i class="fa fa-user" aria-hidden="true"></i>  
44 - <span id="name">{{ escape(name) }}</span>  
45 - (<span id="number">{{ escape(uid) }}</span>)  
46 - <span class="caret"></span>  
47 - </span>  
48 - </div> 38 +
  39 + <div class="collapse navbar-collapse" id="navbarText">
  40 + <ul class="navbar-nav mr-auto"></ul>
  41 +
  42 + <ul class="navbar-nav">
  43 + <li class="nav-item dropdown">
  44 + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
  45 + <i class="fa fa-user" aria-hidden="true"></i>
  46 + <span id="name">{{ escape(name) }}</span>
  47 + (<span id="number">{{ escape(uid) }}</span>)
  48 + <span class="caret"></span>
  49 + </a>
  50 + <div class="dropdown-menu" aria-labelledby="navbarDropdown">
  51 + <a class="dropdown-item" data-toggle="modal" data-target="#password_modal">Mudar Password</a>
  52 + <div class="dropdown-divider"></div>
  53 + <a class="dropdown-item" href="/logout">Sair</a>
  54 + </div>
  55 + </li>
  56 + </ul>
  57 + </div>
49 </nav> 58 </nav>
50 <!-- ===================================================================== --> 59 <!-- ===================================================================== -->
51 <div class="container"> 60 <div class="container">
@@ -63,7 +72,7 @@ @@ -63,7 +72,7 @@
63 {{ t['name'] }} 72 {{ t['name'] }}
64 </div> 73 </div>
65 <div class="ml-auto p-2"> 74 <div class="ml-auto p-2">
66 - <i class="fa fa-lock" aria-hidden="true"></i> 75 + <i class="fa fa-lock text-danger" aria-hidden="true"></i>
67 </div> 76 </div>
68 </div> 77 </div>
69 </a> 78 </a>
@@ -75,9 +84,10 @@ @@ -75,9 +84,10 @@
75 </div> 84 </div>
76 <div class="ml-auto p-2"> 85 <div class="ml-auto p-2">
77 {% if t['level'] < 0.01 %} 86 {% if t['level'] < 0.01 %}
78 - <i class="fa fa-unlock float-right" aria-hidden="true"></i> 87 +
  88 + <i class="fa fa-unlock text-success" aria-hidden="true"></i>
79 {% else %} 89 {% else %}
80 - {{ round(t['level']*5)*'<i class="fa fa-star text-success" aria-hidden="true"></i>' + round(5-t['level']*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }} 90 + {{ round(t['level']*5)*'<i class="fa fa-star text-warning" aria-hidden="true"></i>' + round(5-t['level']*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }}
81 {% end %} 91 {% end %}
82 </div> 92 </div>
83 </div> 93 </div>
@@ -93,8 +103,8 @@ @@ -93,8 +103,8 @@
93 <div class="modal-content"> 103 <div class="modal-content">
94 <!-- header --> 104 <!-- header -->
95 <div class="modal-header"> 105 <div class="modal-header">
96 - <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>  
97 - <h4 class="modal-title">Alterar Password</h4> 106 + <!-- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> -->
  107 + <h5 class="modal-title">Alterar Password</h5>
98 </div> 108 </div>
99 <!-- body --> 109 <!-- body -->
100 <div class="modal-body"> 110 <div class="modal-body">
templates/topic.html
@@ -36,6 +36,20 @@ @@ -36,6 +36,20 @@
36 body { 36 body {
37 margin: 0; 37 margin: 0;
38 padding-top: 55px; 38 padding-top: 55px;
  39 + margin-bottom: 60px; /* Margin bottom by footer height */
  40 + }
  41 + .footer {
  42 + position: absolute;
  43 + bottom: 0;
  44 + width: 100%;
  45 + /* Set the fixed height of the footer here */
  46 + height: 60px;
  47 + line-height: 60px; /* Vertically center the text there */
  48 + background-color: #f5f5f5;
  49 + }
  50 + html {
  51 + position: relative;
  52 + min-height: 100%;
39 } 53 }
40 </style> 54 </style>
41 55
@@ -45,12 +59,12 @@ @@ -45,12 +59,12 @@
45 59
46 <!-- Navbar --> 60 <!-- Navbar -->
47 <nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-dark"> 61 <nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-dark">
48 - <a class="navbar-brand" href="#"> 62 + <a class="navbar-brand" href="#">
49 <img src="/static/logo_horizontal.png" height="30" alt=""> 63 <img src="/static/logo_horizontal.png" height="30" alt="">
50 - </a>  
51 - <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"> 64 + </a>
  65 + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
52 <span class="navbar-toggler-icon"></span> 66 <span class="navbar-toggler-icon"></span>
53 - </button> 67 + </button>
54 68
55 <div class="collapse navbar-collapse" id="navbarText"> 69 <div class="collapse navbar-collapse" id="navbarText">
56 <ul class="navbar-nav mr-auto"></ul> 70 <ul class="navbar-nav mr-auto"></ul>
@@ -65,7 +79,7 @@ @@ -65,7 +79,7 @@
65 </a> 79 </a>
66 <div class="dropdown-menu" aria-labelledby="navbarDropdown"> 80 <div class="dropdown-menu" aria-labelledby="navbarDropdown">
67 <!-- <div class="dropdown-divider"></div> --> 81 <!-- <div class="dropdown-divider"></div> -->
68 - <a class="dropdown-item" href="#">Sair</a> 82 + <a class="dropdown-item" href="/logout">Sair</a>
69 </div> 83 </div>
70 </li> 84 </li>
71 </ul> 85 </ul>
@@ -82,17 +96,24 @@ @@ -82,17 +96,24 @@
82 96
83 <div id="notifications"></div> 97 <div id="notifications"></div>
84 98
85 - <div id="content"> 99 + <div class="my-5" id="content">
86 <form action="/question" method="post" id="question_form" autocomplete="off"> 100 <form action="/question" method="post" id="question_form" autocomplete="off">
87 {% module xsrf_form_html() %} 101 {% module xsrf_form_html() %}
88 -  
89 <div id="question_div"></div> 102 <div id="question_div"></div>
90 -  
91 </form> 103 </form>
92 - <button class="btn btn-primary" id="submit" data-toggle="tooltip" data-placement="right" title="Shift-Enter">Continuar</button> 104 +
  105 + <button class="btn btn-primary btn-lg btn-block my-3" id="submit" data-toggle="tooltip" data-placement="right" title="Shift-Enter">Continuar</button>
93 </div> 106 </div>
94 </div> 107 </div>
95 108
  109 +<footer class="footer">
  110 + <div class="container">
  111 + <span class="text-muted"></span>
  112 + </div>
  113 +</footer>
  114 +
  115 +
  116 +
96 <!-- ===================================================================== --> 117 <!-- ===================================================================== -->
97 <!-- JAVASCRIPT --> 118 <!-- JAVASCRIPT -->
98 <!-- ===================================================================== --> 119 <!-- ===================================================================== -->
@@ -154,8 +175,9 @@ @@ -154,8 +175,9 @@
154 break; 175 break;
155 176
156 case "finished_topic": 177 case "finished_topic":
  178 + $('#submit').css("visibility", "hidden");
  179 + $("#content").html(response["params"]["question"]);
157 $('#topic_progress').css('width', '100%').attr('aria-valuenow', 100); 180 $('#topic_progress').css('width', '100%').attr('aria-valuenow', 100);
158 - $("#content").html('<img src="/static/trophy.png" alt="trophy" class="img-fluid mx-auto my-5 d-block" width="25%">');  
159 $("#content").animateCSS('tada'); 181 $("#content").animateCSS('tada');
160 setTimeout(function(){ window.location.replace('/'); }, 2000); 182 setTimeout(function(){ window.location.replace('/'); }, 2000);
161 break; 183 break;
1 1
  2 +# builtin
  3 +from os import path
2 import subprocess 4 import subprocess
3 import logging 5 import logging
  6 +import re
  7 +
  8 +# packages
4 import yaml 9 import yaml
5 -import markdown 10 +import mistune
  11 +from pygments import highlight
  12 +from pygments.lexers import get_lexer_by_name
  13 +from pygments.formatters import HtmlFormatter
6 14
7 # setup logger for this module 15 # setup logger for this module
8 logger = logging.getLogger(__name__) 16 logger = logging.getLogger(__name__)
9 17
  18 +
  19 +# ---------------------------------------------------------------------------
  20 +# Markdown to HTML renderer with support for LaTeX equations
  21 +# Inline math: $x$
  22 +# Block math: $$x$$ or \begin{equation}x\end{equation}
  23 +# ---------------------------------------------------------------------------
  24 +class MathBlockGrammar(mistune.BlockGrammar):
  25 + block_math = re.compile(r"^\$\$(.*?)\$\$", re.DOTALL)
  26 + latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}", re.DOTALL)
  27 +
  28 +
  29 +class MathBlockLexer(mistune.BlockLexer):
  30 + default_rules = ['block_math', 'latex_environment'] + mistune.BlockLexer.default_rules
  31 +
  32 + def __init__(self, rules=None, **kwargs):
  33 + if rules is None:
  34 + rules = MathBlockGrammar()
  35 + super().__init__(rules, **kwargs)
  36 +
  37 + def parse_block_math(self, m):
  38 + """Parse a $$math$$ block"""
  39 + self.tokens.append({
  40 + 'type': 'block_math',
  41 + 'text': m.group(1)
  42 + })
  43 +
  44 + def parse_latex_environment(self, m):
  45 + self.tokens.append({
  46 + 'type': 'latex_environment',
  47 + 'name': m.group(1),
  48 + 'text': m.group(2)
  49 + })
  50 +
  51 +
  52 +class MathInlineGrammar(mistune.InlineGrammar):
  53 + math = re.compile(r"^\$(.+?)\$", re.DOTALL)
  54 + block_math = re.compile(r"^\$\$(.+?)\$\$", re.DOTALL)
  55 + text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|https?://| {2,}\n|$)')
  56 +
  57 +
  58 +class MathInlineLexer(mistune.InlineLexer):
  59 + default_rules = ['block_math', 'math'] + mistune.InlineLexer.default_rules
  60 +
  61 + def __init__(self, renderer, rules=None, **kwargs):
  62 + if rules is None:
  63 + rules = MathInlineGrammar()
  64 + super().__init__(renderer, rules, **kwargs)
  65 +
  66 + def output_math(self, m):
  67 + return self.renderer.inline_math(m.group(1))
  68 +
  69 + def output_block_math(self, m):
  70 + return self.renderer.block_math(m.group(1))
  71 +
  72 +
  73 +class MarkdownWithMath(mistune.Markdown):
  74 + def __init__(self, renderer, **kwargs):
  75 + if 'inline' not in kwargs:
  76 + kwargs['inline'] = MathInlineLexer
  77 + if 'block' not in kwargs:
  78 + kwargs['block'] = MathBlockLexer
  79 + super().__init__(renderer, **kwargs)
  80 +
  81 + def output_block_math(self):
  82 + return self.renderer.block_math(self.token['text'])
  83 +
  84 + def output_latex_environment(self):
  85 + return self.renderer.latex_environment(self.token['name'], self.token['text'])
  86 +
  87 +
  88 +
  89 +class HighlightRenderer(mistune.Renderer):
  90 + def block_code(self, code, lang='text'):
  91 + try:
  92 + lexer = get_lexer_by_name(lang, stripall=True)
  93 + except:
  94 + lexer = get_lexer_by_name('text', stripall=True)
  95 +
  96 + formatter = HtmlFormatter()
  97 + # FIXME indentation is striped from first line of code, seems bug in mistune
  98 + return highlight(code, lexer, formatter)
  99 +
  100 + # def table(self, header, body):
  101 + # return "<table class='table table-bordered table-hover'>" + header + body + "</table>"
  102 +
  103 + # def image(self, src, title, text):
  104 + # if src.startswith('javascript:'):
  105 + # src = ''
  106 + # text = mistune.escape(text, quote=True)
  107 + # if title:
  108 + # title = mistune.escape(title, quote=True)
  109 + # html = '<img class="img-responsive center-block" src="%s" alt="%s" title="%s"' % (src, text, title)
  110 + # else:
  111 + # html = '<img class="img-responsive center-block" src="%s" alt="%s"' % (src, text)
  112 + # if self.options.get('use_xhtml'):
  113 + # return '%s />' % html
  114 + # return '%s>' % html
  115 +
  116 +
  117 + # Pass math through unaltered - mathjax does the rendering in the browser
  118 + def block_math(self, text):
  119 + return fr'$$ {text} $$'
  120 +
  121 + def latex_environment(self, name, text):
  122 + return fr'\begin{{{name}}} {text} \end{{{name}}}'
  123 +
  124 + def inline_math(self, text):
  125 + return fr'$$$ {text} $$$'
  126 +
  127 +
  128 +markdown = MarkdownWithMath(HighlightRenderer(escape=False)) # hard_wrap=True to insert <br> on newline
  129 +
  130 +def md_to_html(text, q=None):
  131 + return markdown(text)
  132 +
10 # --------------------------------------------------------------------------- 133 # ---------------------------------------------------------------------------
11 # load data from yaml file 134 # load data from yaml file
12 # --------------------------------------------------------------------------- 135 # ---------------------------------------------------------------------------
13 def load_yaml(filename, default=None): 136 def load_yaml(filename, default=None):
14 try: 137 try:
15 - f = open(filename, 'r', encoding='utf-8') 138 + f = open(path.expanduser(filename), 'r', encoding='utf-8')
16 except IOError: 139 except IOError:
17 logger.error(f'Can\'t open file "{filename}"') 140 logger.error(f'Can\'t open file "{filename}"')
18 return default 141 return default
@@ -22,14 +145,14 @@ def load_yaml(filename, default=None): @@ -22,14 +145,14 @@ def load_yaml(filename, default=None):
22 return yaml.load(f) 145 return yaml.load(f)
23 except yaml.YAMLError as e: 146 except yaml.YAMLError as e:
24 mark = e.problem_mark 147 mark = e.problem_mark
25 - logger.error('In YAML file "{0}" near line {1}, column {2}.'.format(filename, mark.line, mark.column+1)) 148 + logger.error(f'In YAML file "{filename}" near line {mark.line}, column {mark.column+1}.')
26 return default 149 return default
27 150
28 # --------------------------------------------------------------------------- 151 # ---------------------------------------------------------------------------
29 # Runs a script and returns its stdout parsed as yaml, or None on error. 152 # Runs a script and returns its stdout parsed as yaml, or None on error.
30 -# Note: requires python 3.5+  
31 # --------------------------------------------------------------------------- 153 # ---------------------------------------------------------------------------
32 def run_script(script, stdin='', timeout=5): 154 def run_script(script, stdin='', timeout=5):
  155 + script = path.expanduser(script)
33 try: 156 try:
34 p = subprocess.run([script], 157 p = subprocess.run([script],
35 input=stdin, 158 input=stdin,
@@ -39,76 +162,20 @@ def run_script(script, stdin=&#39;&#39;, timeout=5): @@ -39,76 +162,20 @@ def run_script(script, stdin=&#39;&#39;, timeout=5):
39 timeout=timeout, 162 timeout=timeout,
40 ) 163 )
41 except FileNotFoundError: 164 except FileNotFoundError:
42 - logger.error('Script not found: "{0}".'.format(script)) 165 + logger.error(f'Could not execute script "{script}": not found.')
43 except PermissionError: 166 except PermissionError:
44 - logger.error('Script "{0}" not executable (wrong permissions?).'.format(script)) 167 + logger.error(f'Could not execute script "{script}": wrong permissions.')
  168 + except OSError:
  169 + logger.error(f'Could not execute script "{script}": unknown reason.')
45 except subprocess.TimeoutExpired: 170 except subprocess.TimeoutExpired:
46 - logger.error('Timeout {0}s exceeded while running script "{1}"'.format(timeout, script)) 171 + logger.error(f'Timeout exceeded ({timeout}s) while running "{script}".')
47 else: 172 else:
48 if p.returncode != 0: 173 if p.returncode != 0:
49 - logger.error('Script "{0}" returned error code {1}.'.format(script, p.returncode)) 174 + logger.error(f'Script "{script}" returned error code {p.returncode}.')
50 else: 175 else:
51 try: 176 try:
52 output = yaml.load(p.stdout) 177 output = yaml.load(p.stdout)
53 except: 178 except:
54 - logger.error('Error parsing yaml output of script "{0}"'.format(script)) 179 + logger.error(f'Error parsing yaml output of "{script}"')
55 else: 180 else:
56 return output 181 return output
57 -  
58 -  
59 -  
60 -# markdown helper  
61 -# returns a function md() that renders markdown with extensions  
62 -# this function is passed to templates for rendering  
63 -def md(text):  
64 - return markdown.markdown(text,  
65 - extensions=[  
66 - 'markdown.extensions.tables',  
67 - 'markdown.extensions.fenced_code',  
68 - 'markdown.extensions.codehilite',  
69 - 'markdown.extensions.def_list',  
70 - 'markdown.extensions.sane_lists'  
71 - ])  
72 -  
73 -  
74 -  
75 -  
76 -  
77 -  
78 -  
79 -  
80 -  
81 -  
82 -  
83 -  
84 -  
85 -  
86 -  
87 -  
88 -  
89 -  
90 -  
91 -# def md_to_html(text, ref=None, files={}):  
92 -# if ref is not None:  
93 -# # given q['ref'] and q['files'] replaces references to files by a  
94 -# # GET to /file?ref=???;name=???  
95 -# for k in files:  
96 -# text = text.replace(k, '/file?ref={};name={}'.format(ref, k))  
97 -# return markdown.markdown(text, extensions=[  
98 -# 'markdown.extensions.tables',  
99 -# 'markdown.extensions.fenced_code',  
100 -# 'markdown.extensions.codehilite',  
101 -# 'markdown.extensions.def_list',  
102 -# 'markdown.extensions.sane_lists'  
103 -# ])  
104 -  
105 -# def md_to_html_review(text, q):  
106 -# for k,f in q['files'].items():  
107 -# text = text.replace(k, '/absfile?name={}'.format(q['files'][k]))  
108 -# return markdown.markdown(text, extensions=[  
109 -# 'markdown.extensions.tables',  
110 -# 'markdown.extensions.fenced_code',  
111 -# 'markdown.extensions.codehilite',  
112 -# 'markdown.extensions.def_list',  
113 -# 'markdown.extensions.sane_lists'  
114 -# ])