Commit d91da251445ae6bcfd5fd6c02871e9c46c8eb0e0
1 parent
b2918629
Exists in
master
and in
1 other branch
- 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 | 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. |
@@ -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 --- |
models.py
@@ -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 | -# --------------------------------------------------------------------------- |
serve.py
@@ -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() |