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).

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
@@ -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)
@@ -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):
@@ -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>
@@ -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=&#39;&#39;, timeout=5): @@ -56,3 +57,14 @@ def run_script(script, stdin=&#39;&#39;, 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 + ])