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.
BUGS.md
1 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 7 - autenticacao. ver exemplo do blog
4 8 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]()
5 9 - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete.
... ... @@ -15,3 +19,7 @@ SOLVED:
15 19 - implementar template base das perguntas base e estender para cada tipo.
16 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 @@
  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 +# ---------------------------------------------------------------------------
... ...
questions.py
... ... @@ -185,7 +185,12 @@ class Question(dict):
185 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 194 self['grade'] = 0.0
190 195 self['comments'] = ''
191 196 return 0.0
... ... @@ -240,9 +245,8 @@ class QuestionRadio(Question):
240 245  
241 246 #------------------------------------------------------------------------
242 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 251 if self['answer']:
248 252 x = self['correct'][int(self['answer'][0])]
... ... @@ -294,8 +298,8 @@ class QuestionCheckbox(Question):
294 298  
295 299 #------------------------------------------------------------------------
296 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 304 if self['answer'] is not None:
301 305 sum_abs = sum(abs(p) for p in self['correct'])
... ... @@ -344,8 +348,8 @@ class QuestionText(Question):
344 348  
345 349 #------------------------------------------------------------------------
346 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 354 if self['answer']:
351 355 self['grade'] = 1.0 if self['answer'][0] in self['correct'] else 0.0
... ... @@ -373,8 +377,8 @@ class QuestionTextRegex(Question):
373 377  
374 378 #------------------------------------------------------------------------
375 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 382 if self['answer']:
379 383 try:
380 384 self['grade'] = 1.0 if re.match(self['correct'], self['answer'][0]) else 0.0
... ... @@ -405,8 +409,8 @@ class QuestionTextNumeric(Question):
405 409  
406 410 #------------------------------------------------------------------------
407 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 414 if self['answer']:
411 415 lower, upper = self['correct']
412 416 try:
... ... @@ -443,8 +447,8 @@ class QuestionTextArea(Question):
443 447  
444 448 #------------------------------------------------------------------------
445 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 453 if self['answer']:
450 454 # correct answer
... ... @@ -484,7 +488,7 @@ class QuestionInformation(Question):
484 488  
485 489 #------------------------------------------------------------------------
486 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 493 self['grade'] = 1.0 # always "correct" but points should be zero!
490 494 return self['grade']
... ...
serve.py
1 1 #!/opt/local/bin/python3.6
2 2  
  3 +# python standard library
3 4 import os
4 5 import json
5 6 import random
  7 +from contextlib import contextmanager # `with` statement in db sessions
6 8  
  9 +# installed libraries
  10 +import bcrypt
7 11 import markdown
8 12 import tornado.ioloop
9 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 20 import questions
  21 +from models import Student # DataBase,
  22 +
  23 +
13 24  
14 25 # markdown helper
15 26 def md(text):
... ... @@ -22,36 +33,91 @@ def md(text):
22 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 42 class LearnApp(object):
27 43 def __init__(self):
  44 + print('LearnApp.__init__')
28 45 self.factory = questions.QuestionFactory()
29   - self.factory.load_files(['questions.yaml'], 'demo')
  46 + self.factory.load_files(['questions.yaml'], 'demo') # FIXME
30 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 90 # returns dictionary
34   - def next_question(self):
  91 + def next_question(self, uid):
35 92 # print('next question')
36 93 # q = self.factory.generate('math-expressions')
37 94 questions = list(self.factory)
38   - # print(questions)
39 95 q = self.factory.generate(random.choice(questions))
40   - # print(q)
41   - self.q = q
  96 + self.online[uid]['current'] = q
42 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 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 121 settings = {
56 122 'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
57 123 'static_path': os.path.join(os.path.dirname(__file__), 'static'),
... ... @@ -61,16 +127,13 @@ class Application(tornado.web.Application):
61 127 'login_url': '/login',
62 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 130 super().__init__(handlers, **settings)
72 131 self.learn = LearnApp()
73 132  
  133 +# ============================================================================
  134 +# Handlers
  135 +# ============================================================================
  136 +
74 137 # ----------------------------------------------------------------------------
75 138 # Base handler common to all handlers.
76 139 class BaseHandler(tornado.web.RequestHandler):
... ... @@ -79,10 +142,9 @@ class BaseHandler(tornado.web.RequestHandler):
79 142 return self.application.learn
80 143  
81 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 150 class MainHandler(BaseHandler):
... ... @@ -97,21 +159,26 @@ class LoginHandler(BaseHandler):
97 159 def get(self):
98 160 self.render('login.html')
99 161  
  162 + # @gen.coroutine
100 163 def post(self):
101   - print('login.post')
102 164 uid = self.get_body_argument('uid')
103 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 179 class LogoutHandler(BaseHandler):
111 180 @tornado.web.authenticated
112 181 def get(self):
113   - name = tornado.escape.xhtml_escape(self.current_user)
114   - print('logout '+name)
115 182 self.clear_cookie('user')
116 183 self.redirect(self.get_argument('next', '/'))
117 184  
... ... @@ -121,12 +188,13 @@ class LogoutHandler(BaseHandler):
121 188 class LearnHandler(BaseHandler):
122 189 @tornado.web.authenticated
123 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 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 199 # respond to AJAX to get a JSON question
132 200 class QuestionHandler(BaseHandler):
... ... @@ -136,31 +204,32 @@ class QuestionHandler(BaseHandler):
136 204  
137 205 @tornado.web.authenticated
138 206 def post(self):
139   - print('---------------\nquestion.post')
  207 + print('---------------> question.post')
140 208 # experiment answering one question and correct it
141 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 215 print('=====================================')
146 216 print(' ' + str(question))
147 217 print('-------------------------------------')
148 218  
149   -
150 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 224 print(' grade = ' + str(grade))
156 225  
157 226 correct = grade > 0.99999
158 227 if correct:
159   - question = self.application.learn.next_question()
  228 + question = self.application.learn.next_question(user)
160 229  
161 230 else:
162 231 correct = True # to animate correctly
163   - question = self.application.learn.next_question()
  232 + question = self.application.learn.next_question(user)
164 233  
165 234 templates = {
166 235 'checkbox': 'question-checkbox.html',
... ... @@ -182,7 +251,7 @@ class QuestionHandler(BaseHandler):
182 251  
183 252 # ----------------------------------------------------------------------------
184 253 def main():
185   - server = Application()
  254 + server = WebApplication()
186 255 server.listen(8080)
187 256 try:
188 257 print('--- start ---')
... ...
templates/learn.html
... ... @@ -29,25 +29,24 @@
29 29  
30 30 <script src="/static/js/jquery.min.js"></script>
31 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 32 </head>
37 33 <!-- ===================================================================== -->
38 34 <body>
  35 +<!-- ===================================================================== -->
  36 +<!-- Navbar -->
39 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 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 46 </button>
47   - <a class="navbar-brand" href="#">
48   - Penalty
49   - </a>
  47 + <a class="navbar-brand" href="#">BrainPower</a>
50 48 </div>
  49 +
51 50 <div class="collapse navbar-collapse" id="myNavbar">
52 51 <ul class="nav navbar-nav navbar-right">
53 52 <li class="dropdown">
... ... @@ -66,6 +65,9 @@
66 65 </div>
67 66 </nav>
68 67 <!-- ===================================================================== -->
  68 +
  69 +<!-- ===================================================================== -->
  70 +<!-- Container -->
69 71 <div class="container">
70 72  
71 73 <form action="/question" method="post" id="question_form" autocomplete="off">
... ... @@ -80,6 +82,10 @@
80 82  
81 83 </div> <!-- container -->
82 84  
  85 +
  86 +<!-- ===================================================================== -->
  87 +<!-- JAVASCRIP -->
  88 +<!-- ===================================================================== -->
83 89 <script>
84 90 $.fn.extend({
85 91 animateCSS: function (animation) {
... ...
templates/question-text.html
... ... @@ -3,6 +3,6 @@
3 3 {% block answer %}
4 4 <fieldset data-role="controlgroup">
5 5 <input type="text" class="form-control" id="answer" name="answer" value="" autofocus>
6   -</fieldset>
  6 +</fieldset><br />
7 7 <input type="hidden" name="question_ref" value="{{ question['ref'] }}">
8   -{% end %}
9 8 \ No newline at end of file
  9 +{% end %}
... ...