Commit d91da251445ae6bcfd5fd6c02871e9c46c8eb0e0

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

- added https support.

- fixed models.py. There are two tables: students and answers.
- fixed logout: Remove from online.
- fixed get_current_user so that old cookies are invalidated.
Showing 4 changed files with 127 additions and 54 deletions   Show diff stats
BUGS.md
1 1 BUGS:
2 2  
3   -- models.py tabela de testes não faz sentido.
4   -- reset ao servidor mantem cookie no broser e rebenta. necessario fazer logout.
5 3 - generators not working: bcrypt (ver blog)
6 4 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]()
7 5 - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete.
8 6  
9 7 SOLVED:
10 8  
  9 +- https. certificados selfsigned, no-ip nao suporta certificados
  10 +- reset ao servidor mantem cookie no broser e rebenta. necessario fazer logout.
  11 +- models.py tabela de testes não faz sentido.
11 12 - autenticacao. ver exemplo do blog
12 13 - primeira pergunta aparece a abanar.
13 14 - user name na barra de navegação.
... ...
initdb.py 0 → 100755
... ... @@ -0,0 +1,87 @@
  1 +#!/usr/bin/env python3
  2 +
  3 +import csv
  4 +import argparse
  5 +import re
  6 +import string
  7 +import sys
  8 +
  9 +import bcrypt
  10 +from sqlalchemy import create_engine
  11 +from sqlalchemy.orm import sessionmaker
  12 +
  13 +from models import Base, Student, Answer
  14 +
  15 +# SIIUE names have alien strings like "(TE)" and are sometimes capitalized
  16 +# We remove them so that students dont keep asking what it means
  17 +def fix(name):
  18 + return string.capwords(re.sub('\(.*\)', '', name).strip())
  19 +
  20 +# ===========================================================================
  21 +# Parse command line options
  22 +argparser = argparse.ArgumentParser(description='Create new database from a CSV file (SIIUE format)')
  23 +argparser.add_argument('--db', default='students.db', type=str, help='database filename')
  24 +argparser.add_argument('--demo', action='store_true', help='initialize database with a few fake students')
  25 +argparser.add_argument('--pw', default='', type=str, help='default password')
  26 +argparser.add_argument('csvfile', nargs='?', type=str, default='', help='CSV filename')
  27 +args = argparser.parse_args()
  28 +
  29 +# ===========================================================================
  30 +hashed_pw = bcrypt.hashpw(args.pw.encode('utf-8'), bcrypt.gensalt())
  31 +
  32 +engine = create_engine('sqlite:///{}'.format(args.db), echo=False)
  33 +Base.metadata.create_all(engine) # Criate schema if needed
  34 +Session = sessionmaker(bind=engine)
  35 +
  36 +# --- start session ---
  37 +try:
  38 + session = Session()
  39 +
  40 + # add administrator
  41 + session.add(Student(id='0', name='Professor', password=hashed_pw))
  42 +
  43 + # add students
  44 + if args.csvfile:
  45 + # from csv file if available
  46 + try:
  47 + csvreader = csv.DictReader(open(args.csvfile, encoding='iso-8859-1'), delimiter=';', quotechar='"', skipinitialspace=True)
  48 + except EnvironmentError:
  49 + print('Error: CSV file "{0}" not found.'.format(args.csvfile))
  50 + session.rollback()
  51 + sys.exit(1)
  52 + else:
  53 + session.add_all([Student(id=r['N.º'], name=fix(r['Nome']), password=hashed_pw) for r in csvreader])
  54 + elif args.demo:
  55 + # add a few fake students
  56 + fakes = [
  57 + ['1915', 'Alan Turing'],
  58 + ['1938', 'Donald Knuth'],
  59 + ['1815', 'Ada Lovelace'],
  60 + ['1969', 'Linus Torvalds'],
  61 + ['1955', 'Tim Burners-Lee'],
  62 + ['1916', 'Claude Shannon'],
  63 + ['1903', 'John von Neumann'],
  64 + ]
  65 + session.add_all([Student(id=i, name=name, password=hashed_pw) for i,name in fakes])
  66 +
  67 + session.commit()
  68 +
  69 +except Exception:
  70 + print('Error: Database already exists.')
  71 + session.rollback()
  72 + sys.exit(1)
  73 +
  74 +else:
  75 + n = session.query(Student).count()
  76 + print('New database created: {0}\n{1} user(s) inserted:'.format(args.db, n))
  77 +
  78 + users = session.query(Student).order_by(Student.id).all()
  79 + print(' {0:8} - {1} (administrator)'.format(users[0].id, users[0].name))
  80 + if n > 1:
  81 + print(' {0:8} - {1}'.format(users[1].id, users[1].name))
  82 + if n > 3:
  83 + print(' ... ...')
  84 + if n > 2:
  85 + print(' {0:8} - {1}'.format(users[-1].id, users[-1].name))
  86 +
  87 +# --- end session ---
... ...
models.py
... ... @@ -10,6 +10,8 @@ from sqlalchemy.orm import relationship
10 10 Base = declarative_base()
11 11  
12 12 # ---------------------------------------------------------------------------
  13 +# Registered students
  14 +# ---------------------------------------------------------------------------
13 15 class Student(Base):
14 16 __tablename__ = 'students'
15 17 id = Column(String, primary_key=True)
... ... @@ -17,69 +19,36 @@ class Student(Base):
17 19 password = Column(String)
18 20  
19 21 # ---
20   - tests = relationship('Test', back_populates='student')
21   - questions = relationship('Question', back_populates='student')
  22 + answers = relationship('Answer', back_populates='student')
22 23  
23 24 def __repr__(self):
24   - return 'Student:\n id: "{0}"\n name: "{1}"\n password: "{2}"'.format(self.id, self.name, self.password)
  25 + return f'''Student:
  26 + id: "{self.id}"
  27 + name: "{self.name}"
  28 + password: "{self.password}"'''
25 29  
26 30  
27 31 # ---------------------------------------------------------------------------
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   -
  32 +# Table with every answer given
59 33 # ---------------------------------------------------------------------------
60   -class Question(Base):
61   - __tablename__ = 'questions'
  34 +class Answer(Base):
  35 + __tablename__ = 'answers'
62 36 id = Column(Integer, primary_key=True) # auto_increment
63 37 ref = Column(String)
64 38 grade = Column(Float)
65 39 starttime = Column(String)
66 40 finishtime = Column(String)
67 41 student_id = Column(String, ForeignKey('students.id'))
68   - test_id = Column(String, ForeignKey('tests.id'))
69 42  
70 43 # ---
71   - student = relationship('Student', back_populates='questions')
72   - test = relationship('Test', back_populates='questions')
  44 + student = relationship('Student', back_populates='answers')
73 45  
74 46 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   -
  47 + return '''Question:
  48 + id: "{self.id}"
  49 + ref: "{self.ref}"
  50 + grade: "{self.grade}"
  51 + starttime: "{self.starttime}"
  52 + finishtime: "{self.finishtime}"
  53 + student_id: "{self.student_id}"'''
84 54  
85   -# ---------------------------------------------------------------------------
... ...
serve.py
... ... @@ -11,6 +11,7 @@ import bcrypt
11 11 import markdown
12 12 import tornado.ioloop
13 13 import tornado.web
  14 +import tornado.httpserver
14 15 from tornado import template, gen
15 16 import concurrent.futures
16 17 from sqlalchemy import create_engine
... ... @@ -87,6 +88,11 @@ class LearnApp(object):
87 88 return True
88 89  
89 90 # ------------------------------------------------------------------------
  91 + # logout
  92 + def logout(self, uid):
  93 + del self.online[uid] # FIXME save current question?
  94 +
  95 + # ------------------------------------------------------------------------
90 96 # returns dictionary
91 97 def next_question(self, uid):
92 98 # print('next question')
... ... @@ -136,6 +142,7 @@ class WebApplication(tornado.web.Application):
136 142  
137 143 # ----------------------------------------------------------------------------
138 144 # Base handler common to all handlers.
  145 +# ----------------------------------------------------------------------------
139 146 class BaseHandler(tornado.web.RequestHandler):
140 147 @property
141 148 def learn(self):
... ... @@ -144,7 +151,10 @@ class BaseHandler(tornado.web.RequestHandler):
144 151 def get_current_user(self):
145 152 cookie = self.get_secure_cookie("user")
146 153 if cookie:
147   - return cookie.decode('utf-8')
  154 + user = cookie.decode('utf-8')
  155 + # FIXME if the cookie exists but user is not in learn.online, this will force new login and store new (duplicate?) cookie. is this correct??
  156 + if user in self.learn.online:
  157 + return user
148 158  
149 159 # # ----------------------------------------------------------------------------
150 160 # class MainHandler(BaseHandler):
... ... @@ -179,6 +189,7 @@ class LoginHandler(BaseHandler):
179 189 class LogoutHandler(BaseHandler):
180 190 @tornado.web.authenticated
181 191 def get(self):
  192 + self.learn.logout(self.current_user)
182 193 self.clear_cookie('user')
183 194 self.redirect(self.get_argument('next', '/'))
184 195  
... ... @@ -251,8 +262,13 @@ class QuestionHandler(BaseHandler):
251 262  
252 263 # ----------------------------------------------------------------------------
253 264 def main():
254   - server = WebApplication()
255   - server.listen(8080)
  265 + webapp = WebApplication()
  266 + http_server = tornado.httpserver.HTTPServer(webapp, ssl_options={
  267 + "certfile": "certs/cert.pem",
  268 + "keyfile": "certs/key.pem"
  269 + })
  270 + http_server.listen(8443)
  271 +
256 272 try:
257 273 print('--- start ---')
258 274 tornado.ioloop.IOLoop.current().start()
... ...