Commit b339e748cf090ba8ebd1f42d858dbaca8fb73b2b

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

- added support for images (or other files).

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=&#39;&#39;, 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 + ])
... ...