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
1 BUGS: 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 - generators not working: bcrypt (ver blog) 3 - generators not working: bcrypt (ver blog)
6 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() 4 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]()
7 - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete. 5 - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete.
8 6
9 SOLVED: 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 - autenticacao. ver exemplo do blog 12 - autenticacao. ver exemplo do blog
12 - primeira pergunta aparece a abanar. 13 - primeira pergunta aparece a abanar.
13 - user name na barra de navegação. 14 - user name na barra de navegação.
initdb.py 0 → 100755
@@ -0,0 +1,87 @@ @@ -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 ---
@@ -10,6 +10,8 @@ from sqlalchemy.orm import relationship @@ -10,6 +10,8 @@ from sqlalchemy.orm import relationship
10 Base = declarative_base() 10 Base = declarative_base()
11 11
12 # --------------------------------------------------------------------------- 12 # ---------------------------------------------------------------------------
  13 +# Registered students
  14 +# ---------------------------------------------------------------------------
13 class Student(Base): 15 class Student(Base):
14 __tablename__ = 'students' 16 __tablename__ = 'students'
15 id = Column(String, primary_key=True) 17 id = Column(String, primary_key=True)
@@ -17,69 +19,36 @@ class Student(Base): @@ -17,69 +19,36 @@ class Student(Base):
17 password = Column(String) 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 def __repr__(self): 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 id = Column(Integer, primary_key=True) # auto_increment 36 id = Column(Integer, primary_key=True) # auto_increment
63 ref = Column(String) 37 ref = Column(String)
64 grade = Column(Float) 38 grade = Column(Float)
65 starttime = Column(String) 39 starttime = Column(String)
66 finishtime = Column(String) 40 finishtime = Column(String)
67 student_id = Column(String, ForeignKey('students.id')) 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 def __repr__(self): 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 -# ---------------------------------------------------------------------------  
@@ -11,6 +11,7 @@ import bcrypt @@ -11,6 +11,7 @@ import bcrypt
11 import markdown 11 import markdown
12 import tornado.ioloop 12 import tornado.ioloop
13 import tornado.web 13 import tornado.web
  14 +import tornado.httpserver
14 from tornado import template, gen 15 from tornado import template, gen
15 import concurrent.futures 16 import concurrent.futures
16 from sqlalchemy import create_engine 17 from sqlalchemy import create_engine
@@ -87,6 +88,11 @@ class LearnApp(object): @@ -87,6 +88,11 @@ class LearnApp(object):
87 return True 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 # returns dictionary 96 # returns dictionary
91 def next_question(self, uid): 97 def next_question(self, uid):
92 # print('next question') 98 # print('next question')
@@ -136,6 +142,7 @@ class WebApplication(tornado.web.Application): @@ -136,6 +142,7 @@ class WebApplication(tornado.web.Application):
136 142
137 # ---------------------------------------------------------------------------- 143 # ----------------------------------------------------------------------------
138 # Base handler common to all handlers. 144 # Base handler common to all handlers.
  145 +# ----------------------------------------------------------------------------
139 class BaseHandler(tornado.web.RequestHandler): 146 class BaseHandler(tornado.web.RequestHandler):
140 @property 147 @property
141 def learn(self): 148 def learn(self):
@@ -144,7 +151,10 @@ class BaseHandler(tornado.web.RequestHandler): @@ -144,7 +151,10 @@ class BaseHandler(tornado.web.RequestHandler):
144 def get_current_user(self): 151 def get_current_user(self):
145 cookie = self.get_secure_cookie("user") 152 cookie = self.get_secure_cookie("user")
146 if cookie: 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 # class MainHandler(BaseHandler): 160 # class MainHandler(BaseHandler):
@@ -179,6 +189,7 @@ class LoginHandler(BaseHandler): @@ -179,6 +189,7 @@ class LoginHandler(BaseHandler):
179 class LogoutHandler(BaseHandler): 189 class LogoutHandler(BaseHandler):
180 @tornado.web.authenticated 190 @tornado.web.authenticated
181 def get(self): 191 def get(self):
  192 + self.learn.logout(self.current_user)
182 self.clear_cookie('user') 193 self.clear_cookie('user')
183 self.redirect(self.get_argument('next', '/')) 194 self.redirect(self.get_argument('next', '/'))
184 195
@@ -251,8 +262,13 @@ class QuestionHandler(BaseHandler): @@ -251,8 +262,13 @@ class QuestionHandler(BaseHandler):
251 262
252 # ---------------------------------------------------------------------------- 263 # ----------------------------------------------------------------------------
253 def main(): 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 try: 272 try:
257 print('--- start ---') 273 print('--- start ---')
258 tornado.ioloop.IOLoop.current().start() 274 tornado.ioloop.IOLoop.current().start()