Commit d4557086649ceb9fdcb32ef1fb6d33ea86e322f7

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

- login working, but still has many bugs.

- questions.correct() accepts answer to correct.
- added models.py with sqlalchemy code.
1 BUGS: 1 BUGS:
2 2
  3 +- user name na barra de navegação.
  4 +- reset ao servidor mantem cookie no broser e rebenta. necessario fazer logout.
  5 +- generators not working: bcrypt (ver blog)
  6 +- primeira pergunta aparece a abanar.
3 - autenticacao. ver exemplo do blog 7 - autenticacao. ver exemplo do blog
4 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() 8 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]()
5 - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete. 9 - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete.
@@ -15,3 +19,7 @@ SOLVED: @@ -15,3 +19,7 @@ SOLVED:
15 - implementar template base das perguntas base e estender para cada tipo. 19 - implementar template base das perguntas base e estender para cada tipo.
16 - submissão com enter em perguntas text faz get? provavelmente está a fazer o submit do form em vez de ir pelo ajax. 20 - submissão com enter em perguntas text faz get? provavelmente está a fazer o submit do form em vez de ir pelo ajax.
17 21
  22 +
  23 +- login working, but still has many bugs.
  24 +- questions.correct() accepts answer to correct.
  25 +- added models.py with sqlalchemy code.
models.py 0 → 100644
@@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
  1 +
  2 +
  3 +from sqlalchemy import Table, Column, ForeignKey, Integer, Float, String, DateTime
  4 +from sqlalchemy.ext.declarative import declarative_base
  5 +from sqlalchemy.orm import relationship
  6 +
  7 +
  8 +# ===========================================================================
  9 +# Declare ORM
  10 +Base = declarative_base()
  11 +
  12 +# ---------------------------------------------------------------------------
  13 +class Student(Base):
  14 + __tablename__ = 'students'
  15 + id = Column(String, primary_key=True)
  16 + name = Column(String)
  17 + password = Column(String)
  18 +
  19 + # ---
  20 + tests = relationship('Test', back_populates='student')
  21 + questions = relationship('Question', back_populates='student')
  22 +
  23 + def __repr__(self):
  24 + return 'Student:\n id: "{0}"\n name: "{1}"\n password: "{2}"'.format(self.id, self.name, self.password)
  25 +
  26 +
  27 +# ---------------------------------------------------------------------------
  28 +class Test(Base):
  29 + __tablename__ = 'tests'
  30 + id = Column(Integer, primary_key=True) # auto_increment
  31 + ref = Column(String)
  32 + title = Column(String) # FIXME depends on ref and should come from another table...
  33 + grade = Column(Float)
  34 + state = Column(String) # ONGOING, FINISHED, QUIT, NULL
  35 + comment = Column(String)
  36 + starttime = Column(String)
  37 + finishtime = Column(String)
  38 + filename = Column(String)
  39 + student_id = Column(String, ForeignKey('students.id'))
  40 +
  41 + # ---
  42 + student = relationship('Student', back_populates='tests')
  43 + questions = relationship('Question', back_populates='test')
  44 +
  45 + def __repr__(self):
  46 + return 'Test:\n\
  47 + id: "{}"\n\
  48 + ref="{}"\n\
  49 + title="{}"\n\
  50 + grade="{}"\n\
  51 + state="{}"\n\
  52 + comment="{}"\n\
  53 + starttime="{}"\n\
  54 + finishtime="{}"\n\
  55 + filename="{}"\n\
  56 + student_id="{}"\n'.format(self.id, self.ref, self.title, self.grade, self.state, self.comment, self.starttime, self.finishtime, self.filename, self.student_id)
  57 +
  58 +
  59 +# ---------------------------------------------------------------------------
  60 +class Question(Base):
  61 + __tablename__ = 'questions'
  62 + id = Column(Integer, primary_key=True) # auto_increment
  63 + ref = Column(String)
  64 + grade = Column(Float)
  65 + starttime = Column(String)
  66 + finishtime = Column(String)
  67 + student_id = Column(String, ForeignKey('students.id'))
  68 + test_id = Column(String, ForeignKey('tests.id'))
  69 +
  70 + # ---
  71 + student = relationship('Student', back_populates='questions')
  72 + test = relationship('Test', back_populates='questions')
  73 +
  74 + def __repr__(self):
  75 + return 'Question:\n\
  76 + id: "{}"\n\
  77 + ref: "{}"\n\
  78 + grade: "{}"\n\
  79 + starttime: "{}"\n\
  80 + finishtime: "{}"\n\
  81 + student_id: "{}"\n\
  82 + test_id: "{}"\n'.fotmat(self.id, self.ref, self.grade, self.starttime, self.finishtime, self.student_id, self.test_id)
  83 +
  84 +
  85 +# ---------------------------------------------------------------------------
@@ -185,7 +185,12 @@ class Question(dict): @@ -185,7 +185,12 @@ class Question(dict):
185 'files': {}, 185 'files': {},
186 }) 186 })
187 187
188 - def correct(self): 188 + def updateAnswer(answer=None):
  189 + self['answer'] = answer
  190 +
  191 + def correct(self, answer=None):
  192 + if answer is not None:
  193 + self['answer'] = answer
189 self['grade'] = 0.0 194 self['grade'] = 0.0
190 self['comments'] = '' 195 self['comments'] = ''
191 return 0.0 196 return 0.0
@@ -240,9 +245,8 @@ class QuestionRadio(Question): @@ -240,9 +245,8 @@ class QuestionRadio(Question):
240 245
241 #------------------------------------------------------------------------ 246 #------------------------------------------------------------------------
242 # can return negative values for wrong answers 247 # can return negative values for wrong answers
243 - def correct(self):  
244 - super().correct()  
245 - 248 + def correct(self, answer=None):
  249 + super().correct(answer)
246 250
247 if self['answer']: 251 if self['answer']:
248 x = self['correct'][int(self['answer'][0])] 252 x = self['correct'][int(self['answer'][0])]
@@ -294,8 +298,8 @@ class QuestionCheckbox(Question): @@ -294,8 +298,8 @@ class QuestionCheckbox(Question):
294 298
295 #------------------------------------------------------------------------ 299 #------------------------------------------------------------------------
296 # can return negative values for wrong answers 300 # can return negative values for wrong answers
297 - def correct(self):  
298 - super().correct() 301 + def correct(self, answer=None):
  302 + super().correct(answer)
299 303
300 if self['answer'] is not None: 304 if self['answer'] is not None:
301 sum_abs = sum(abs(p) for p in self['correct']) 305 sum_abs = sum(abs(p) for p in self['correct'])
@@ -344,8 +348,8 @@ class QuestionText(Question): @@ -344,8 +348,8 @@ class QuestionText(Question):
344 348
345 #------------------------------------------------------------------------ 349 #------------------------------------------------------------------------
346 # can return negative values for wrong answers 350 # can return negative values for wrong answers
347 - def correct(self):  
348 - super().correct() 351 + def correct(self, answer=None):
  352 + super().correct(answer)
349 353
350 if self['answer']: 354 if self['answer']:
351 self['grade'] = 1.0 if self['answer'][0] in self['correct'] else 0.0 355 self['grade'] = 1.0 if self['answer'][0] in self['correct'] else 0.0
@@ -373,8 +377,8 @@ class QuestionTextRegex(Question): @@ -373,8 +377,8 @@ class QuestionTextRegex(Question):
373 377
374 #------------------------------------------------------------------------ 378 #------------------------------------------------------------------------
375 # can return negative values for wrong answers 379 # can return negative values for wrong answers
376 - def correct(self):  
377 - super().correct() 380 + def correct(self, answer=None):
  381 + super().correct(answer)
378 if self['answer']: 382 if self['answer']:
379 try: 383 try:
380 self['grade'] = 1.0 if re.match(self['correct'], self['answer'][0]) else 0.0 384 self['grade'] = 1.0 if re.match(self['correct'], self['answer'][0]) else 0.0
@@ -405,8 +409,8 @@ class QuestionTextNumeric(Question): @@ -405,8 +409,8 @@ class QuestionTextNumeric(Question):
405 409
406 #------------------------------------------------------------------------ 410 #------------------------------------------------------------------------
407 # can return negative values for wrong answers 411 # can return negative values for wrong answers
408 - def correct(self):  
409 - super().correct() 412 + def correct(self, answer=None):
  413 + super().correct(answer)
410 if self['answer']: 414 if self['answer']:
411 lower, upper = self['correct'] 415 lower, upper = self['correct']
412 try: 416 try:
@@ -443,8 +447,8 @@ class QuestionTextArea(Question): @@ -443,8 +447,8 @@ class QuestionTextArea(Question):
443 447
444 #------------------------------------------------------------------------ 448 #------------------------------------------------------------------------
445 # can return negative values for wrong answers 449 # can return negative values for wrong answers
446 - def correct(self):  
447 - super().correct() 450 + def correct(self, answer=None):
  451 + super().correct(answer)
448 452
449 if self['answer']: 453 if self['answer']:
450 # correct answer 454 # correct answer
@@ -484,7 +488,7 @@ class QuestionInformation(Question): @@ -484,7 +488,7 @@ class QuestionInformation(Question):
484 488
485 #------------------------------------------------------------------------ 489 #------------------------------------------------------------------------
486 # can return negative values for wrong answers 490 # can return negative values for wrong answers
487 - def correct(self):  
488 - super().correct() 491 + def correct(self, answer=None):
  492 + super().correct(answer)
489 self['grade'] = 1.0 # always "correct" but points should be zero! 493 self['grade'] = 1.0 # always "correct" but points should be zero!
490 return self['grade'] 494 return self['grade']
1 #!/opt/local/bin/python3.6 1 #!/opt/local/bin/python3.6
2 2
  3 +# python standard library
3 import os 4 import os
4 import json 5 import json
5 import random 6 import random
  7 +from contextlib import contextmanager # `with` statement in db sessions
6 8
  9 +# installed libraries
  10 +import bcrypt
7 import markdown 11 import markdown
8 import tornado.ioloop 12 import tornado.ioloop
9 import tornado.web 13 import tornado.web
10 -from tornado import template 14 +from tornado import template, gen
  15 +import concurrent.futures
  16 +from sqlalchemy import create_engine
  17 +from sqlalchemy.orm import sessionmaker, scoped_session
11 18
  19 +# this project
12 import questions 20 import questions
  21 +from models import Student # DataBase,
  22 +
  23 +
13 24
14 # markdown helper 25 # markdown helper
15 def md(text): 26 def md(text):
@@ -22,36 +33,91 @@ def md(text): @@ -22,36 +33,91 @@ def md(text):
22 'markdown.extensions.sane_lists' 33 'markdown.extensions.sane_lists'
23 ]) 34 ])
24 35
25 -# ---------------------------------------------------------------------------- 36 +# A thread pool to be used for password hashing with bcrypt.
  37 +executor = concurrent.futures.ThreadPoolExecutor(2)
  38 +
  39 +# ============================================================================
  40 +# LearnApp - application logic
  41 +# ============================================================================
26 class LearnApp(object): 42 class LearnApp(object):
27 def __init__(self): 43 def __init__(self):
  44 + print('LearnApp.__init__')
28 self.factory = questions.QuestionFactory() 45 self.factory = questions.QuestionFactory()
29 - self.factory.load_files(['questions.yaml'], 'demo') 46 + self.factory.load_files(['questions.yaml'], 'demo') # FIXME
30 self.online = {} 47 self.online = {}
31 - self.q = None  
32 48
  49 + # connect to database and check registered students
  50 + engine = create_engine('sqlite:///{}'.format('students.db'), echo=False)
  51 + self.Session = scoped_session(sessionmaker(bind=engine))
  52 + try:
  53 + with self.db_session() as s:
  54 + n = s.query(Student).filter(Student.id != '0').count()
  55 + except Exception as e:
  56 + print('Database not usable.')
  57 + raise e
  58 + else:
  59 + print('Database has {} students registered.'.format(n))
  60 +
  61 + # ------------------------------------------------------------------------
  62 + def login_ok(self, uid, try_pw):
  63 + print('LearnApp.login')
  64 +
  65 + with self.db_session() as s:
  66 + student = s.query(Student).filter(Student.id == uid).one_or_none()
  67 +
  68 + if student is None or student in self.online:
  69 + # student does not exist
  70 + return False
  71 +
  72 + # hashedtry = yield executor.submit(bcrypt.hashpw,
  73 + # try_pw.encode('utf-8'), student.password)
  74 + hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password)
  75 +
  76 + if hashedtry != student.password:
  77 + # wrong password
  78 + return False
  79 +
  80 + # success
  81 + self.online[uid] = {
  82 + 'name': student.name,
  83 + 'number': student.id,
  84 + 'current': None,
  85 + }
  86 + print(self.online)
  87 + return True
  88 +
  89 + # ------------------------------------------------------------------------
33 # returns dictionary 90 # returns dictionary
34 - def next_question(self): 91 + def next_question(self, uid):
35 # print('next question') 92 # print('next question')
36 # q = self.factory.generate('math-expressions') 93 # q = self.factory.generate('math-expressions')
37 questions = list(self.factory) 94 questions = list(self.factory)
38 - # print(questions)  
39 q = self.factory.generate(random.choice(questions)) 95 q = self.factory.generate(random.choice(questions))
40 - # print(q)  
41 - self.q = q 96 + self.online[uid]['current'] = q
42 return q 97 return q
43 98
44 - def login(self, uid):  
45 - print('LearnApp.login')  
46 - self.online[uid] = {  
47 - 'name': 'john',  
48 - }  
49 - print(self.online) 99 + # ------------------------------------------------------------------------
  100 + # helper to manage db sessions using the `with` statement, for example
  101 + # with self.db_session() as s: s.query(...)
  102 + @contextmanager
  103 + def db_session(self):
  104 + try:
  105 + yield self.Session()
  106 + finally:
  107 + self.Session.remove()
50 108
51 -  
52 -# ----------------------------------------------------------------------------  
53 -class Application(tornado.web.Application): 109 +# ============================================================================
  110 +# WebApplication - Tornado Web Server
  111 +# ============================================================================
  112 +class WebApplication(tornado.web.Application):
54 def __init__(self): 113 def __init__(self):
  114 + handlers = [
  115 + (r'/', MainHandler),
  116 + (r'/login', LoginHandler),
  117 + (r'/logout', LogoutHandler),
  118 + (r'/learn', LearnHandler),
  119 + (r'/question', QuestionHandler),
  120 + ]
55 settings = { 121 settings = {
56 'template_path': os.path.join(os.path.dirname(__file__), 'templates'), 122 'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
57 'static_path': os.path.join(os.path.dirname(__file__), 'static'), 123 'static_path': os.path.join(os.path.dirname(__file__), 'static'),
@@ -61,16 +127,13 @@ class Application(tornado.web.Application): @@ -61,16 +127,13 @@ class Application(tornado.web.Application):
61 'login_url': '/login', 127 'login_url': '/login',
62 'debug': True, 128 'debug': True,
63 } 129 }
64 - handlers = [  
65 - (r'/', MainHandler),  
66 - (r'/login', LoginHandler),  
67 - (r'/logout', LogoutHandler),  
68 - (r'/learn', LearnHandler),  
69 - (r'/question', QuestionHandler),  
70 - ]  
71 super().__init__(handlers, **settings) 130 super().__init__(handlers, **settings)
72 self.learn = LearnApp() 131 self.learn = LearnApp()
73 132
  133 +# ============================================================================
  134 +# Handlers
  135 +# ============================================================================
  136 +
74 # ---------------------------------------------------------------------------- 137 # ----------------------------------------------------------------------------
75 # Base handler common to all handlers. 138 # Base handler common to all handlers.
76 class BaseHandler(tornado.web.RequestHandler): 139 class BaseHandler(tornado.web.RequestHandler):
@@ -79,10 +142,9 @@ class BaseHandler(tornado.web.RequestHandler): @@ -79,10 +142,9 @@ class BaseHandler(tornado.web.RequestHandler):
79 return self.application.learn 142 return self.application.learn
80 143
81 def get_current_user(self): 144 def get_current_user(self):
82 - user_cookie = self.get_secure_cookie("user")  
83 - # print('base.get_current_user -> ', user_cookie)  
84 - if user_cookie:  
85 - return user_cookie # json.loads(user_cookie) 145 + cookie = self.get_secure_cookie("user")
  146 + if cookie:
  147 + return cookie.decode('utf-8')
86 148
87 # ---------------------------------------------------------------------------- 149 # ----------------------------------------------------------------------------
88 class MainHandler(BaseHandler): 150 class MainHandler(BaseHandler):
@@ -97,21 +159,26 @@ class LoginHandler(BaseHandler): @@ -97,21 +159,26 @@ class LoginHandler(BaseHandler):
97 def get(self): 159 def get(self):
98 self.render('login.html') 160 self.render('login.html')
99 161
  162 + # @gen.coroutine
100 def post(self): 163 def post(self):
101 - print('login.post')  
102 uid = self.get_body_argument('uid') 164 uid = self.get_body_argument('uid')
103 pw = self.get_body_argument('pw') 165 pw = self.get_body_argument('pw')
104 - # FIXME check password ver examplo do blog tornado.  
105 - self.set_secure_cookie('user', uid)  
106 - self.application.learn.login(uid)  
107 - self.redirect('/learn') 166 + print(f'login.post: user={uid}, pw={pw}')
  167 +
  168 + x = self.learn.login_ok(uid, pw)
  169 + print(x)
  170 + if x: # hashedtry == student.password:
  171 + print('login ok')
  172 + self.set_secure_cookie("user", str(uid))
  173 + self.redirect(self.get_argument("next", "/"))
  174 + else:
  175 + print('login failed')
  176 + self.render("login.html", error="incorrect password")
108 177
109 # ---------------------------------------------------------------------------- 178 # ----------------------------------------------------------------------------
110 class LogoutHandler(BaseHandler): 179 class LogoutHandler(BaseHandler):
111 @tornado.web.authenticated 180 @tornado.web.authenticated
112 def get(self): 181 def get(self):
113 - name = tornado.escape.xhtml_escape(self.current_user)  
114 - print('logout '+name)  
115 self.clear_cookie('user') 182 self.clear_cookie('user')
116 self.redirect(self.get_argument('next', '/')) 183 self.redirect(self.get_argument('next', '/'))
117 184
@@ -121,12 +188,13 @@ class LogoutHandler(BaseHandler): @@ -121,12 +188,13 @@ class LogoutHandler(BaseHandler):
121 class LearnHandler(BaseHandler): 188 class LearnHandler(BaseHandler):
122 @tornado.web.authenticated 189 @tornado.web.authenticated
123 def get(self): 190 def get(self):
124 - print('learn.get')  
125 - user = self.current_user.decode('utf-8') 191 + print('GET /learn')
  192 + user = self.current_user
126 # name = self.application.learn.online[user] 193 # name = self.application.learn.online[user]
127 - print(' user = '+str(user))  
128 - self.render('learn.html', name='aa', uid=user) # FIXME  
129 - 194 + print(' user = '+user)
  195 + print(self.learn.online)
  196 + self.render('learn.html', name='dhsjdhsj', uid=user) # FIXME
  197 + # self.learn.online[user]['name']
130 # ---------------------------------------------------------------------------- 198 # ----------------------------------------------------------------------------
131 # respond to AJAX to get a JSON question 199 # respond to AJAX to get a JSON question
132 class QuestionHandler(BaseHandler): 200 class QuestionHandler(BaseHandler):
@@ -136,31 +204,32 @@ class QuestionHandler(BaseHandler): @@ -136,31 +204,32 @@ class QuestionHandler(BaseHandler):
136 204
137 @tornado.web.authenticated 205 @tornado.web.authenticated
138 def post(self): 206 def post(self):
139 - print('---------------\nquestion.post') 207 + print('---------------> question.post')
140 # experiment answering one question and correct it 208 # experiment answering one question and correct it
141 ref = self.get_body_arguments('question_ref') 209 ref = self.get_body_arguments('question_ref')
142 - print('Reference' + str(ref)) 210 + # print('Reference' + str(ref))
143 211
144 - question = self.application.learn.q # get current question 212 + user = self.current_user
  213 + userdata = self.learn.online[user]
  214 + question = userdata['current'] # get current question
145 print('=====================================') 215 print('=====================================')
146 print(' ' + str(question)) 216 print(' ' + str(question))
147 print('-------------------------------------') 217 print('-------------------------------------')
148 218
149 -  
150 if question is not None: 219 if question is not None:
151 - ans = self.get_body_arguments('answer')  
152 - print(' answer = ' + str(ans))  
153 - question['answer'] = ans # insert answer  
154 - grade = question.correct() # correct answer 220 + answer = self.get_body_arguments('answer')
  221 + print(' answer = ' + str(answer))
  222 + # question['answer'] = ans # insert answer
  223 + grade = question.correct(answer) # correct answer
155 print(' grade = ' + str(grade)) 224 print(' grade = ' + str(grade))
156 225
157 correct = grade > 0.99999 226 correct = grade > 0.99999
158 if correct: 227 if correct:
159 - question = self.application.learn.next_question() 228 + question = self.application.learn.next_question(user)
160 229
161 else: 230 else:
162 correct = True # to animate correctly 231 correct = True # to animate correctly
163 - question = self.application.learn.next_question() 232 + question = self.application.learn.next_question(user)
164 233
165 templates = { 234 templates = {
166 'checkbox': 'question-checkbox.html', 235 'checkbox': 'question-checkbox.html',
@@ -182,7 +251,7 @@ class QuestionHandler(BaseHandler): @@ -182,7 +251,7 @@ class QuestionHandler(BaseHandler):
182 251
183 # ---------------------------------------------------------------------------- 252 # ----------------------------------------------------------------------------
184 def main(): 253 def main():
185 - server = Application() 254 + server = WebApplication()
186 server.listen(8080) 255 server.listen(8080)
187 try: 256 try:
188 print('--- start ---') 257 print('--- start ---')
templates/learn.html
@@ -29,25 +29,24 @@ @@ -29,25 +29,24 @@
29 29
30 <script src="/static/js/jquery.min.js"></script> 30 <script src="/static/js/jquery.min.js"></script>
31 <script src="/static/bootstrap/js/bootstrap.min.js"></script> 31 <script src="/static/bootstrap/js/bootstrap.min.js"></script>
32 - <script src="/static/js/nunjucks.min.js"></script>  
33 -  
34 - <script src="/static/js/learn.js"></script>  
35 -  
36 </head> 32 </head>
37 <!-- ===================================================================== --> 33 <!-- ===================================================================== -->
38 <body> 34 <body>
  35 +<!-- ===================================================================== -->
  36 +<!-- Navbar -->
39 <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> 37 <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
40 - <div class="container-fluid drop-shadow"> 38 + <div class="container-fluid">
  39 +
41 <div class="navbar-header"> 40 <div class="navbar-header">
42 - <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar">  
43 - <span class="icon-bar"></span>  
44 - <span class="icon-bar"></span>  
45 - <span class="icon-bar"></span> 41 + <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#myNavbar" aria-expanded="false">
  42 + <span class="sr-only">Toggle navigation</span>
  43 + <span class="icon-bar"></span>
  44 + <span class="icon-bar"></span>
  45 + <span class="icon-bar"></span>
46 </button> 46 </button>
47 - <a class="navbar-brand" href="#">  
48 - Penalty  
49 - </a> 47 + <a class="navbar-brand" href="#">BrainPower</a>
50 </div> 48 </div>
  49 +
51 <div class="collapse navbar-collapse" id="myNavbar"> 50 <div class="collapse navbar-collapse" id="myNavbar">
52 <ul class="nav navbar-nav navbar-right"> 51 <ul class="nav navbar-nav navbar-right">
53 <li class="dropdown"> 52 <li class="dropdown">
@@ -66,6 +65,9 @@ @@ -66,6 +65,9 @@
66 </div> 65 </div>
67 </nav> 66 </nav>
68 <!-- ===================================================================== --> 67 <!-- ===================================================================== -->
  68 +
  69 +<!-- ===================================================================== -->
  70 +<!-- Container -->
69 <div class="container"> 71 <div class="container">
70 72
71 <form action="/question" method="post" id="question_form" autocomplete="off"> 73 <form action="/question" method="post" id="question_form" autocomplete="off">
@@ -80,6 +82,10 @@ @@ -80,6 +82,10 @@
80 82
81 </div> <!-- container --> 83 </div> <!-- container -->
82 84
  85 +
  86 +<!-- ===================================================================== -->
  87 +<!-- JAVASCRIP -->
  88 +<!-- ===================================================================== -->
83 <script> 89 <script>
84 $.fn.extend({ 90 $.fn.extend({
85 animateCSS: function (animation) { 91 animateCSS: function (animation) {
templates/question-text.html
@@ -3,6 +3,6 @@ @@ -3,6 +3,6 @@
3 {% block answer %} 3 {% block answer %}
4 <fieldset data-role="controlgroup"> 4 <fieldset data-role="controlgroup">
5 <input type="text" class="form-control" id="answer" name="answer" value="" autofocus> 5 <input type="text" class="form-control" id="answer" name="answer" value="" autofocus>
6 -</fieldset> 6 +</fieldset><br />
7 <input type="hidden" name="question_ref" value="{{ question['ref'] }}"> 7 <input type="hidden" name="question_ref" value="{{ question['ref'] }}">
8 -{% end %}  
9 \ No newline at end of file 8 \ No newline at end of file
  9 +{% end %}