diff --git a/BUGS.md b/BUGS.md index a43f0c4..9b9b3bd 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,5 +1,9 @@ BUGS: +- user name na barra de navegação. +- reset ao servidor mantem cookie no broser e rebenta. necessario fazer logout. +- generators not working: bcrypt (ver blog) +- primeira pergunta aparece a abanar. - autenticacao. ver exemplo do blog - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete. @@ -15,3 +19,7 @@ SOLVED: - implementar template base das perguntas base e estender para cada tipo. - submissão com enter em perguntas text faz get? provavelmente está a fazer o submit do form em vez de ir pelo ajax. + +- login working, but still has many bugs. +- questions.correct() accepts answer to correct. +- added models.py with sqlalchemy code. diff --git a/models.py b/models.py new file mode 100644 index 0000000..d3f1a9c --- /dev/null +++ b/models.py @@ -0,0 +1,85 @@ + + +from sqlalchemy import Table, Column, ForeignKey, Integer, Float, String, DateTime +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship + + +# =========================================================================== +# Declare ORM +Base = declarative_base() + +# --------------------------------------------------------------------------- +class Student(Base): + __tablename__ = 'students' + id = Column(String, primary_key=True) + name = Column(String) + password = Column(String) + + # --- + tests = relationship('Test', back_populates='student') + questions = relationship('Question', back_populates='student') + + def __repr__(self): + return 'Student:\n id: "{0}"\n name: "{1}"\n password: "{2}"'.format(self.id, self.name, self.password) + + +# --------------------------------------------------------------------------- +class Test(Base): + __tablename__ = 'tests' + id = Column(Integer, primary_key=True) # auto_increment + ref = Column(String) + title = Column(String) # FIXME depends on ref and should come from another table... + grade = Column(Float) + state = Column(String) # ONGOING, FINISHED, QUIT, NULL + comment = Column(String) + starttime = Column(String) + finishtime = Column(String) + filename = Column(String) + student_id = Column(String, ForeignKey('students.id')) + + # --- + student = relationship('Student', back_populates='tests') + questions = relationship('Question', back_populates='test') + + def __repr__(self): + return 'Test:\n\ + id: "{}"\n\ + ref="{}"\n\ + title="{}"\n\ + grade="{}"\n\ + state="{}"\n\ + comment="{}"\n\ + starttime="{}"\n\ + finishtime="{}"\n\ + filename="{}"\n\ + 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) + + +# --------------------------------------------------------------------------- +class Question(Base): + __tablename__ = 'questions' + id = Column(Integer, primary_key=True) # auto_increment + ref = Column(String) + grade = Column(Float) + starttime = Column(String) + finishtime = Column(String) + student_id = Column(String, ForeignKey('students.id')) + test_id = Column(String, ForeignKey('tests.id')) + + # --- + student = relationship('Student', back_populates='questions') + test = relationship('Test', back_populates='questions') + + def __repr__(self): + return 'Question:\n\ + id: "{}"\n\ + ref: "{}"\n\ + grade: "{}"\n\ + starttime: "{}"\n\ + finishtime: "{}"\n\ + student_id: "{}"\n\ + test_id: "{}"\n'.fotmat(self.id, self.ref, self.grade, self.starttime, self.finishtime, self.student_id, self.test_id) + + +# --------------------------------------------------------------------------- diff --git a/questions.py b/questions.py index fef0303..494e4d5 100644 --- a/questions.py +++ b/questions.py @@ -185,7 +185,12 @@ class Question(dict): 'files': {}, }) - def correct(self): + def updateAnswer(answer=None): + self['answer'] = answer + + def correct(self, answer=None): + if answer is not None: + self['answer'] = answer self['grade'] = 0.0 self['comments'] = '' return 0.0 @@ -240,9 +245,8 @@ class QuestionRadio(Question): #------------------------------------------------------------------------ # can return negative values for wrong answers - def correct(self): - super().correct() - + def correct(self, answer=None): + super().correct(answer) if self['answer']: x = self['correct'][int(self['answer'][0])] @@ -294,8 +298,8 @@ class QuestionCheckbox(Question): #------------------------------------------------------------------------ # can return negative values for wrong answers - def correct(self): - super().correct() + def correct(self, answer=None): + super().correct(answer) if self['answer'] is not None: sum_abs = sum(abs(p) for p in self['correct']) @@ -344,8 +348,8 @@ class QuestionText(Question): #------------------------------------------------------------------------ # can return negative values for wrong answers - def correct(self): - super().correct() + def correct(self, answer=None): + super().correct(answer) if self['answer']: self['grade'] = 1.0 if self['answer'][0] in self['correct'] else 0.0 @@ -373,8 +377,8 @@ class QuestionTextRegex(Question): #------------------------------------------------------------------------ # can return negative values for wrong answers - def correct(self): - super().correct() + def correct(self, answer=None): + super().correct(answer) if self['answer']: try: self['grade'] = 1.0 if re.match(self['correct'], self['answer'][0]) else 0.0 @@ -405,8 +409,8 @@ class QuestionTextNumeric(Question): #------------------------------------------------------------------------ # can return negative values for wrong answers - def correct(self): - super().correct() + def correct(self, answer=None): + super().correct(answer) if self['answer']: lower, upper = self['correct'] try: @@ -443,8 +447,8 @@ class QuestionTextArea(Question): #------------------------------------------------------------------------ # can return negative values for wrong answers - def correct(self): - super().correct() + def correct(self, answer=None): + super().correct(answer) if self['answer']: # correct answer @@ -484,7 +488,7 @@ class QuestionInformation(Question): #------------------------------------------------------------------------ # can return negative values for wrong answers - def correct(self): - super().correct() + def correct(self, answer=None): + super().correct(answer) self['grade'] = 1.0 # always "correct" but points should be zero! return self['grade'] diff --git a/serve.py b/serve.py index 7d813c3..03b76bc 100755 --- a/serve.py +++ b/serve.py @@ -1,15 +1,26 @@ #!/opt/local/bin/python3.6 +# python standard library import os import json import random +from contextlib import contextmanager # `with` statement in db sessions +# installed libraries +import bcrypt import markdown import tornado.ioloop import tornado.web -from tornado import template +from tornado import template, gen +import concurrent.futures +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, scoped_session +# this project import questions +from models import Student # DataBase, + + # markdown helper def md(text): @@ -22,36 +33,91 @@ def md(text): 'markdown.extensions.sane_lists' ]) -# ---------------------------------------------------------------------------- +# A thread pool to be used for password hashing with bcrypt. +executor = concurrent.futures.ThreadPoolExecutor(2) + +# ============================================================================ +# LearnApp - application logic +# ============================================================================ class LearnApp(object): def __init__(self): + print('LearnApp.__init__') self.factory = questions.QuestionFactory() - self.factory.load_files(['questions.yaml'], 'demo') + self.factory.load_files(['questions.yaml'], 'demo') # FIXME self.online = {} - self.q = None + # connect to database and check registered students + engine = create_engine('sqlite:///{}'.format('students.db'), echo=False) + self.Session = scoped_session(sessionmaker(bind=engine)) + try: + with self.db_session() as s: + n = s.query(Student).filter(Student.id != '0').count() + except Exception as e: + print('Database not usable.') + raise e + else: + print('Database has {} students registered.'.format(n)) + + # ------------------------------------------------------------------------ + def login_ok(self, uid, try_pw): + print('LearnApp.login') + + with self.db_session() as s: + student = s.query(Student).filter(Student.id == uid).one_or_none() + + if student is None or student in self.online: + # student does not exist + return False + + # hashedtry = yield executor.submit(bcrypt.hashpw, + # try_pw.encode('utf-8'), student.password) + hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password) + + if hashedtry != student.password: + # wrong password + return False + + # success + self.online[uid] = { + 'name': student.name, + 'number': student.id, + 'current': None, + } + print(self.online) + return True + + # ------------------------------------------------------------------------ # returns dictionary - def next_question(self): + def next_question(self, uid): # print('next question') # q = self.factory.generate('math-expressions') questions = list(self.factory) - # print(questions) q = self.factory.generate(random.choice(questions)) - # print(q) - self.q = q + self.online[uid]['current'] = q return q - def login(self, uid): - print('LearnApp.login') - self.online[uid] = { - 'name': 'john', - } - print(self.online) + # ------------------------------------------------------------------------ + # helper to manage db sessions using the `with` statement, for example + # with self.db_session() as s: s.query(...) + @contextmanager + def db_session(self): + try: + yield self.Session() + finally: + self.Session.remove() - -# ---------------------------------------------------------------------------- -class Application(tornado.web.Application): +# ============================================================================ +# WebApplication - Tornado Web Server +# ============================================================================ +class WebApplication(tornado.web.Application): def __init__(self): + handlers = [ + (r'/', MainHandler), + (r'/login', LoginHandler), + (r'/logout', LogoutHandler), + (r'/learn', LearnHandler), + (r'/question', QuestionHandler), + ] settings = { 'template_path': os.path.join(os.path.dirname(__file__), 'templates'), 'static_path': os.path.join(os.path.dirname(__file__), 'static'), @@ -61,16 +127,13 @@ class Application(tornado.web.Application): 'login_url': '/login', 'debug': True, } - handlers = [ - (r'/', MainHandler), - (r'/login', LoginHandler), - (r'/logout', LogoutHandler), - (r'/learn', LearnHandler), - (r'/question', QuestionHandler), - ] super().__init__(handlers, **settings) self.learn = LearnApp() +# ============================================================================ +# Handlers +# ============================================================================ + # ---------------------------------------------------------------------------- # Base handler common to all handlers. class BaseHandler(tornado.web.RequestHandler): @@ -79,10 +142,9 @@ class BaseHandler(tornado.web.RequestHandler): return self.application.learn def get_current_user(self): - user_cookie = self.get_secure_cookie("user") - # print('base.get_current_user -> ', user_cookie) - if user_cookie: - return user_cookie # json.loads(user_cookie) + cookie = self.get_secure_cookie("user") + if cookie: + return cookie.decode('utf-8') # ---------------------------------------------------------------------------- class MainHandler(BaseHandler): @@ -97,21 +159,26 @@ class LoginHandler(BaseHandler): def get(self): self.render('login.html') + # @gen.coroutine def post(self): - print('login.post') uid = self.get_body_argument('uid') pw = self.get_body_argument('pw') - # FIXME check password ver examplo do blog tornado. - self.set_secure_cookie('user', uid) - self.application.learn.login(uid) - self.redirect('/learn') + print(f'login.post: user={uid}, pw={pw}') + + x = self.learn.login_ok(uid, pw) + print(x) + if x: # hashedtry == student.password: + print('login ok') + self.set_secure_cookie("user", str(uid)) + self.redirect(self.get_argument("next", "/")) + else: + print('login failed') + self.render("login.html", error="incorrect password") # ---------------------------------------------------------------------------- class LogoutHandler(BaseHandler): @tornado.web.authenticated def get(self): - name = tornado.escape.xhtml_escape(self.current_user) - print('logout '+name) self.clear_cookie('user') self.redirect(self.get_argument('next', '/')) @@ -121,12 +188,13 @@ class LogoutHandler(BaseHandler): class LearnHandler(BaseHandler): @tornado.web.authenticated def get(self): - print('learn.get') - user = self.current_user.decode('utf-8') + print('GET /learn') + user = self.current_user # name = self.application.learn.online[user] - print(' user = '+str(user)) - self.render('learn.html', name='aa', uid=user) # FIXME - + print(' user = '+user) + print(self.learn.online) + self.render('learn.html', name='dhsjdhsj', uid=user) # FIXME + # self.learn.online[user]['name'] # ---------------------------------------------------------------------------- # respond to AJAX to get a JSON question class QuestionHandler(BaseHandler): @@ -136,31 +204,32 @@ class QuestionHandler(BaseHandler): @tornado.web.authenticated def post(self): - print('---------------\nquestion.post') + print('---------------> question.post') # experiment answering one question and correct it ref = self.get_body_arguments('question_ref') - print('Reference' + str(ref)) + # print('Reference' + str(ref)) - question = self.application.learn.q # get current question + user = self.current_user + userdata = self.learn.online[user] + question = userdata['current'] # get current question print('=====================================') print(' ' + str(question)) print('-------------------------------------') - if question is not None: - ans = self.get_body_arguments('answer') - print(' answer = ' + str(ans)) - question['answer'] = ans # insert answer - grade = question.correct() # correct answer + answer = self.get_body_arguments('answer') + print(' answer = ' + str(answer)) + # question['answer'] = ans # insert answer + grade = question.correct(answer) # correct answer print(' grade = ' + str(grade)) correct = grade > 0.99999 if correct: - question = self.application.learn.next_question() + question = self.application.learn.next_question(user) else: correct = True # to animate correctly - question = self.application.learn.next_question() + question = self.application.learn.next_question(user) templates = { 'checkbox': 'question-checkbox.html', @@ -182,7 +251,7 @@ class QuestionHandler(BaseHandler): # ---------------------------------------------------------------------------- def main(): - server = Application() + server = WebApplication() server.listen(8080) try: print('--- start ---') diff --git a/templates/learn.html b/templates/learn.html index 0949469..1e0f4cd 100644 --- a/templates/learn.html +++ b/templates/learn.html @@ -29,25 +29,24 @@ - - - -
+ +