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 | # BUGS | 2 | # BUGS |
3 | 3 | ||
4 | +- implementar practice mode. | ||
4 | - usar thread.Lock para aceder a variaveis de estado? | 5 | - usar thread.Lock para aceder a variaveis de estado? |
5 | 6 | ||
6 | # TODO | 7 | # TODO |
7 | 8 | ||
8 | -- lidar com focus. aviso em /admin | ||
9 | -- implementar practice mode. | ||
10 | -- permitir adicionar imagens nas perguntas. | ||
11 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova. | 9 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova. |
12 | - 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) | 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 | - 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) | 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,6 +22,7 @@ | ||
24 | 22 | ||
25 | # FIXED | 23 | # FIXED |
26 | 24 | ||
25 | +- permitir adicionar imagens nas perguntas. | ||
27 | - detect_unfocus.js so funciona se estiver inline no html. porquê??? | 26 | - detect_unfocus.js so funciona se estiver inline no html. porquê??? |
28 | - inserir novo aluno /admin não fecha. | 27 | - inserir novo aluno /admin não fecha. |
29 | - se aluno desistir, ainda fica marcado como online | 28 | - se aluno desistir, ainda fica marcado como online |
app.py
@@ -6,7 +6,7 @@ import bcrypt | @@ -6,7 +6,7 @@ import bcrypt | ||
6 | from sqlalchemy import create_engine | 6 | from sqlalchemy import create_engine |
7 | from sqlalchemy.orm import sessionmaker, scoped_session | 7 | from sqlalchemy.orm import sessionmaker, scoped_session |
8 | from models import Base, Student, Test, Question | 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 | import test | 11 | import test |
12 | import threading | 12 | import threading |
@@ -28,7 +28,7 @@ class App(object): | @@ -28,7 +28,7 @@ class App(object): | ||
28 | # } | 28 | # } |
29 | logger.info('============= Running perguntations =============') | 29 | logger.info('============= Running perguntations =============') |
30 | self.lock = threading.Lock() | 30 | self.lock = threading.Lock() |
31 | - self.online = dict() # {uid: {'student':{}}} | 31 | + self.online = dict() # {uid: {'student':{}}} |
32 | self.allowed = set([]) # '0' is hardcoded to allowed elsewhere | 32 | self.allowed = set([]) # '0' is hardcoded to allowed elsewhere |
33 | 33 | ||
34 | self.testfactory = test.TestFactory(filename, conf=conf) | 34 | self.testfactory = test.TestFactory(filename, conf=conf) |
@@ -226,6 +226,13 @@ class App(object): | @@ -226,6 +226,13 @@ class App(object): | ||
226 | # set of 'uid' allowed to login | 226 | # set of 'uid' allowed to login |
227 | return self.allowed | 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 | # --- helpers (change state) | 236 | # --- helpers (change state) |
230 | def allow_student(self, uid): | 237 | def allow_student(self, uid): |
231 | self.allowed.add(uid) | 238 | self.allowed.add(uid) |
questions.py
@@ -38,6 +38,7 @@ logger = logging.getLogger(__name__) | @@ -38,6 +38,7 @@ logger = logging.getLogger(__name__) | ||
38 | 38 | ||
39 | try: | 39 | try: |
40 | import yaml | 40 | import yaml |
41 | + # import markdown | ||
41 | except ImportError: | 42 | except ImportError: |
42 | logger.critical('Python package missing. See README.md for instructions.') | 43 | logger.critical('Python package missing. See README.md for instructions.') |
43 | sys.exit(1) | 44 | sys.exit(1) |
@@ -46,7 +47,7 @@ else: | @@ -46,7 +47,7 @@ else: | ||
46 | # correct: !regex '[aA]zul' | 47 | # correct: !regex '[aA]zul' |
47 | yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n))) | 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,6 +180,7 @@ class Question(dict): | ||
179 | self.set_defaults({ | 180 | self.set_defaults({ |
180 | 'title': '', | 181 | 'title': '', |
181 | 'answer': None, | 182 | 'answer': None, |
183 | + 'files': {}, | ||
182 | }) | 184 | }) |
183 | 185 | ||
184 | def correct(self): | 186 | def correct(self): |
serve.py
@@ -231,6 +231,14 @@ class Root(object): | @@ -231,6 +231,14 @@ class Root(object): | ||
231 | allgrades=self.app.get_student_grades_from_all_tests(uid) | 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 | # --- ADMIN -------------------------------------------------------------- | 243 | # --- ADMIN -------------------------------------------------------------- |
236 | @cherrypy.expose | 244 | @cherrypy.expose |
templates/test.html
@@ -99,17 +99,9 @@ | @@ -99,17 +99,9 @@ | ||
99 | 99 | ||
100 | <form action="/correct/" method="post" id="test"> | 100 | <form action="/correct/" method="post" id="test"> |
101 | <%! | 101 | <%! |
102 | - import markdown as md | ||
103 | import yaml | 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 | total_points = sum(q['points'] for q in t['questions']) | 106 | total_points = sum(q['points'] for q in t['questions']) |
115 | %> | 107 | %> |
@@ -135,7 +127,7 @@ | @@ -135,7 +127,7 @@ | ||
135 | <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> ${q['title']} | 127 | <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> ${q['title']} |
136 | </h4> | 128 | </h4> |
137 | <p> | 129 | <p> |
138 | - ${pretty(q['text'])} | 130 | + ${md_to_html(q['text'], q['ref'], q['files'])} |
139 | </p> | 131 | </p> |
140 | </div> | 132 | </div> |
141 | % elif q['type'] == 'warning': | 133 | % elif q['type'] == 'warning': |
@@ -144,7 +136,7 @@ | @@ -144,7 +136,7 @@ | ||
144 | <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ${q['title']} | 136 | <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ${q['title']} |
145 | </h4> | 137 | </h4> |
146 | <p> | 138 | <p> |
147 | - ${pretty(q['text'])} | 139 | + ${md_to_html(q['text'], q['ref'], q['files'])} |
148 | </p> | 140 | </p> |
149 | </div> | 141 | </div> |
150 | % elif q['type'] == 'alert': | 142 | % elif q['type'] == 'alert': |
@@ -153,7 +145,7 @@ | @@ -153,7 +145,7 @@ | ||
153 | <span class="glyphicon glyphicon-alert" aria-hidden="true"></span> ${q['title']} | 145 | <span class="glyphicon glyphicon-alert" aria-hidden="true"></span> ${q['title']} |
154 | </h4> | 146 | </h4> |
155 | <p> | 147 | <p> |
156 | - ${pretty(q['text'])} | 148 | + ${md_to_html(q['text'], q['ref'], q['files'])} |
157 | </p> | 149 | </p> |
158 | </div> | 150 | </div> |
159 | 151 | ||
@@ -170,7 +162,7 @@ | @@ -170,7 +162,7 @@ | ||
170 | </div> | 162 | </div> |
171 | <div class="panel-body" id="example${i}"> | 163 | <div class="panel-body" id="example${i}"> |
172 | <div class="question"> | 164 | <div class="question"> |
173 | - ${pretty(q['text'])} | 165 | + ${md_to_html(q['text'], q['ref'], q['files'])} |
174 | </div> | 166 | </div> |
175 | 167 | ||
176 | <fieldset data-role="controlgroup"> | 168 | <fieldset data-role="controlgroup"> |
@@ -179,7 +171,7 @@ | @@ -179,7 +171,7 @@ | ||
179 | <div class="radio"> | 171 | <div class="radio"> |
180 | <label class="option"> | 172 | <label class="option"> |
181 | <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 ''}> | 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 | </label> | 175 | </label> |
184 | </div> | 176 | </div> |
185 | % endfor | 177 | % endfor |
@@ -189,7 +181,7 @@ | @@ -189,7 +181,7 @@ | ||
189 | <div class="checkbox"> | 181 | <div class="checkbox"> |
190 | <label> | 182 | <label> |
191 | <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 ''}> | 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 | </label> | 185 | </label> |
194 | </div> | 186 | </div> |
195 | % endfor | 187 | % endfor |
@@ -207,7 +199,7 @@ | @@ -207,7 +199,7 @@ | ||
207 | </button> | 199 | </button> |
208 | <div class="collapse" id="hint-${q['ref']}"> | 200 | <div class="collapse" id="hint-${q['ref']}"> |
209 | <div class="well"> | 201 | <div class="well"> |
210 | - ${pretty(q['hint'])} | 202 | + ${md_to_html(q['hint'], q['ref'], q['files'])} |
211 | </div> | 203 | </div> |
212 | </div> | 204 | </div> |
213 | % endif # hint | 205 | % endif # hint |
@@ -223,7 +215,7 @@ | @@ -223,7 +215,7 @@ | ||
223 | <h4 class="modal-title">Anexo</h4> | 215 | <h4 class="modal-title">Anexo</h4> |
224 | </div> | 216 | </div> |
225 | <div class="modal-body"> | 217 | <div class="modal-body"> |
226 | - ${pretty(q['modal'])} | 218 | + ${md_to_html(q['modal'], q['ref'], q['files'])} |
227 | </div> | 219 | </div> |
228 | <div class="modal-footer"> | 220 | <div class="modal-footer"> |
229 | <button type="button" class="btn btn-default" data-dismiss="modal">Fechar</button> | 221 | <button type="button" class="btn btn-default" data-dismiss="modal">Fechar</button> |
tools.py
@@ -3,6 +3,7 @@ | @@ -3,6 +3,7 @@ | ||
3 | import subprocess | 3 | import subprocess |
4 | import logging | 4 | import logging |
5 | import yaml | 5 | import yaml |
6 | +import markdown | ||
6 | 7 | ||
7 | # setup logger for this module | 8 | # setup logger for this module |
8 | logger = logging.getLogger(__name__) | 9 | logger = logging.getLogger(__name__) |
@@ -56,3 +57,14 @@ def run_script(script, stdin='', timeout=5): | @@ -56,3 +57,14 @@ def run_script(script, stdin='', timeout=5): | ||
56 | else: | 57 | else: |
57 | return output | 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 | + ]) |