Commit b339e748cf090ba8ebd1f42d858dbaca8fb73b2b
1 parent
f749fa17
Exists in
master
and in
1 other branch
- added support for images (or other files).
Showing
6 changed files
with
43 additions
and
23 deletions
Show diff stats
BUGS.md
1 | 1 | |
2 | 2 | # BUGS |
3 | 3 | |
4 | +- implementar practice mode. | |
4 | 5 | - usar thread.Lock para aceder a variaveis de estado? |
5 | 6 | |
6 | 7 | # TODO |
7 | 8 | |
8 | -- lidar com focus. aviso em /admin | |
9 | -- implementar practice mode. | |
10 | -- permitir adicionar imagens nas perguntas. | |
11 | 9 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova. |
12 | 10 | - detectar scroll e enviar posição para servidor (analise de scroll para detectar copianço? ou simplesmente para analisar como os alunos percorrem o teste) |
13 | 11 | - detectar se janela perde focus e alertar o prof (http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active) |
... | ... | @@ -24,6 +22,7 @@ |
24 | 22 | |
25 | 23 | # FIXED |
26 | 24 | |
25 | +- permitir adicionar imagens nas perguntas. | |
27 | 26 | - detect_unfocus.js so funciona se estiver inline no html. porquê??? |
28 | 27 | - inserir novo aluno /admin não fecha. |
29 | 28 | - se aluno desistir, ainda fica marcado como online | ... | ... |
app.py
... | ... | @@ -6,7 +6,7 @@ import bcrypt |
6 | 6 | from sqlalchemy import create_engine |
7 | 7 | from sqlalchemy.orm import sessionmaker, scoped_session |
8 | 8 | from models import Base, Student, Test, Question |
9 | -from contextlib import contextmanager # to create `with` statement for db sessions | |
9 | +from contextlib import contextmanager # to use `with` statement for db sessions | |
10 | 10 | |
11 | 11 | import test |
12 | 12 | import threading |
... | ... | @@ -28,7 +28,7 @@ class App(object): |
28 | 28 | # } |
29 | 29 | logger.info('============= Running perguntations =============') |
30 | 30 | self.lock = threading.Lock() |
31 | - self.online = dict() # {uid: {'student':{}}} | |
31 | + self.online = dict() # {uid: {'student':{}}} | |
32 | 32 | self.allowed = set([]) # '0' is hardcoded to allowed elsewhere |
33 | 33 | |
34 | 34 | self.testfactory = test.TestFactory(filename, conf=conf) |
... | ... | @@ -226,6 +226,13 @@ class App(object): |
226 | 226 | # set of 'uid' allowed to login |
227 | 227 | return self.allowed |
228 | 228 | |
229 | + def get_file(self, uid, ref, key): | |
230 | + # return filename corresponding to (uid, ref, name) if declared in the question | |
231 | + t = self.get_test(uid) | |
232 | + for q in t['questions']: | |
233 | + if q['ref'] == ref and key in q['files']: | |
234 | + return path.abspath(path.join(q['path'], q['files'][key])) | |
235 | + | |
229 | 236 | # --- helpers (change state) |
230 | 237 | def allow_student(self, uid): |
231 | 238 | self.allowed.add(uid) | ... | ... |
questions.py
... | ... | @@ -38,6 +38,7 @@ logger = logging.getLogger(__name__) |
38 | 38 | |
39 | 39 | try: |
40 | 40 | import yaml |
41 | + # import markdown | |
41 | 42 | except ImportError: |
42 | 43 | logger.critical('Python package missing. See README.md for instructions.') |
43 | 44 | sys.exit(1) |
... | ... | @@ -46,7 +47,7 @@ else: |
46 | 47 | # correct: !regex '[aA]zul' |
47 | 48 | yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n))) |
48 | 49 | |
49 | -from tools import load_yaml, run_script | |
50 | +from tools import load_yaml, run_script, md_to_html | |
50 | 51 | |
51 | 52 | |
52 | 53 | # =========================================================================== |
... | ... | @@ -179,6 +180,7 @@ class Question(dict): |
179 | 180 | self.set_defaults({ |
180 | 181 | 'title': '', |
181 | 182 | 'answer': None, |
183 | + 'files': {}, | |
182 | 184 | }) |
183 | 185 | |
184 | 186 | def correct(self): | ... | ... |
serve.py
... | ... | @@ -231,6 +231,14 @@ class Root(object): |
231 | 231 | allgrades=self.app.get_student_grades_from_all_tests(uid) |
232 | 232 | ) |
233 | 233 | |
234 | + # --- FILE --------------------------------------------------------------- | |
235 | + @cherrypy.expose | |
236 | + @require() | |
237 | + def file(self, ref, name): | |
238 | + # serve a static file: userid, question ref, file name | |
239 | + uid = cherrypy.session.get(SESSION_KEY) | |
240 | + filename = self.app.get_file(uid, ref, name) | |
241 | + return cherrypy.lib.static.serve_file(filename) | |
234 | 242 | |
235 | 243 | # --- ADMIN -------------------------------------------------------------- |
236 | 244 | @cherrypy.expose | ... | ... |
templates/test.html
... | ... | @@ -99,17 +99,9 @@ |
99 | 99 | |
100 | 100 | <form action="/correct/" method="post" id="test"> |
101 | 101 | <%! |
102 | - import markdown as md | |
103 | 102 | import yaml |
104 | - import random | |
103 | + from tools import md_to_html | |
105 | 104 | %> |
106 | - <%def name="pretty(text)"> | |
107 | - ${md.markdown(str(text), extensions=['markdown.extensions.tables', | |
108 | - 'markdown.extensions.fenced_code', | |
109 | - 'markdown.extensions.codehilite', | |
110 | - 'markdown.extensions.def_list', | |
111 | - 'markdown.extensions.sane_lists'])} | |
112 | - </%def> | |
113 | 105 | <% |
114 | 106 | total_points = sum(q['points'] for q in t['questions']) |
115 | 107 | %> |
... | ... | @@ -135,7 +127,7 @@ |
135 | 127 | <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> ${q['title']} |
136 | 128 | </h4> |
137 | 129 | <p> |
138 | - ${pretty(q['text'])} | |
130 | + ${md_to_html(q['text'], q['ref'], q['files'])} | |
139 | 131 | </p> |
140 | 132 | </div> |
141 | 133 | % elif q['type'] == 'warning': |
... | ... | @@ -144,7 +136,7 @@ |
144 | 136 | <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ${q['title']} |
145 | 137 | </h4> |
146 | 138 | <p> |
147 | - ${pretty(q['text'])} | |
139 | + ${md_to_html(q['text'], q['ref'], q['files'])} | |
148 | 140 | </p> |
149 | 141 | </div> |
150 | 142 | % elif q['type'] == 'alert': |
... | ... | @@ -153,7 +145,7 @@ |
153 | 145 | <span class="glyphicon glyphicon-alert" aria-hidden="true"></span> ${q['title']} |
154 | 146 | </h4> |
155 | 147 | <p> |
156 | - ${pretty(q['text'])} | |
148 | + ${md_to_html(q['text'], q['ref'], q['files'])} | |
157 | 149 | </p> |
158 | 150 | </div> |
159 | 151 | |
... | ... | @@ -170,7 +162,7 @@ |
170 | 162 | </div> |
171 | 163 | <div class="panel-body" id="example${i}"> |
172 | 164 | <div class="question"> |
173 | - ${pretty(q['text'])} | |
165 | + ${md_to_html(q['text'], q['ref'], q['files'])} | |
174 | 166 | </div> |
175 | 167 | |
176 | 168 | <fieldset data-role="controlgroup"> |
... | ... | @@ -179,7 +171,7 @@ |
179 | 171 | <div class="radio"> |
180 | 172 | <label class="option"> |
181 | 173 | <input type="radio" name="${q['ref']}" id="${q['ref']}${loop.index}" value="${loop.index}" ${'checked' if q['answer'] is not None and str(loop.index) == q['answer'] else ''}> |
182 | - ${pretty(opt)} | |
174 | + ${md_to_html(opt, q['ref'], q['files'])} | |
183 | 175 | </label> |
184 | 176 | </div> |
185 | 177 | % endfor |
... | ... | @@ -189,7 +181,7 @@ |
189 | 181 | <div class="checkbox"> |
190 | 182 | <label> |
191 | 183 | <input type="checkbox" name="${q['ref']}" id="${q['ref']}${loop.index}" value="${loop.index}" ${'checked' if q['answer'] is not None and str(loop.index) in q['answer'] else ''}> |
192 | - ${pretty(opt)} | |
184 | + ${md_to_html(opt, q['ref'], q['files'])} | |
193 | 185 | </label> |
194 | 186 | </div> |
195 | 187 | % endfor |
... | ... | @@ -207,7 +199,7 @@ |
207 | 199 | </button> |
208 | 200 | <div class="collapse" id="hint-${q['ref']}"> |
209 | 201 | <div class="well"> |
210 | - ${pretty(q['hint'])} | |
202 | + ${md_to_html(q['hint'], q['ref'], q['files'])} | |
211 | 203 | </div> |
212 | 204 | </div> |
213 | 205 | % endif # hint |
... | ... | @@ -223,7 +215,7 @@ |
223 | 215 | <h4 class="modal-title">Anexo</h4> |
224 | 216 | </div> |
225 | 217 | <div class="modal-body"> |
226 | - ${pretty(q['modal'])} | |
218 | + ${md_to_html(q['modal'], q['ref'], q['files'])} | |
227 | 219 | </div> |
228 | 220 | <div class="modal-footer"> |
229 | 221 | <button type="button" class="btn btn-default" data-dismiss="modal">Fechar</button> | ... | ... |
tools.py
... | ... | @@ -3,6 +3,7 @@ |
3 | 3 | import subprocess |
4 | 4 | import logging |
5 | 5 | import yaml |
6 | +import markdown | |
6 | 7 | |
7 | 8 | # setup logger for this module |
8 | 9 | logger = logging.getLogger(__name__) |
... | ... | @@ -56,3 +57,14 @@ def run_script(script, stdin='', timeout=5): |
56 | 57 | else: |
57 | 58 | return output |
58 | 59 | |
60 | +def md_to_html(text, ref=None, files={}): | |
61 | + if ref is not None: | |
62 | + for k,f in files.items(): | |
63 | + text = text.replace(k, '/file?ref={};name={}'.format(ref, k)) | |
64 | + return markdown.markdown(text, extensions=[ | |
65 | + 'markdown.extensions.tables', | |
66 | + 'markdown.extensions.fenced_code', | |
67 | + 'markdown.extensions.codehilite', | |
68 | + 'markdown.extensions.def_list', | |
69 | + 'markdown.extensions.sane_lists' | |
70 | + ]) | ... | ... |