Commit efdbe121d0ec2d33eb16d4345b744a7c0c2a1c9a

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

- added database column "topic" in answers table.

- some code refactoring and cleanup in learnapp.
@@ -3,6 +3,7 @@ BUGS: @@ -3,6 +3,7 @@ BUGS:
3 3
4 - servidor http com redirect para https. 4 - servidor http com redirect para https.
5 - servir imagens/ficheiros. 5 - servir imagens/ficheiros.
  6 +- codemirror em textarea.
6 - topicos virtuais nao deveriam aparecer. na construção da árvore os sucessores seriam ligados directamente aos predecessores. 7 - topicos virtuais nao deveriam aparecer. na construção da árvore os sucessores seriam ligados directamente aos predecessores.
7 8
8 9
@@ -29,6 +30,7 @@ TODO: @@ -29,6 +30,7 @@ TODO:
29 30
30 FIXED: 31 FIXED:
31 32
  33 +- database: answers não tem referencia para o topico, so para question_ref
32 - melhorar markdown das tabelas. 34 - melhorar markdown das tabelas.
33 - gravar evolucao na bd no final de cada topico. 35 - gravar evolucao na bd no final de cada topico.
34 - submeter questoes radio, da erro se nao escolher nenhuma opção. 36 - submeter questoes radio, da erro se nao escolher nenhuma opção.
@@ -19,11 +19,29 @@ def fix(name): @@ -19,11 +19,29 @@ def fix(name):
19 19
20 # =========================================================================== 20 # ===========================================================================
21 # Parse command line options 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') 22 +argparser = argparse.ArgumentParser(
  23 + description='Create new database from a CSV file (SIIUE format)')
  24 +
  25 +argparser.add_argument('--db',
  26 + default='students.db',
  27 + type=str,
  28 + help='database filename')
  29 +
  30 +argparser.add_argument('--demo',
  31 + action='store_true',
  32 + help='initialize database with a few fake students')
  33 +
  34 +argparser.add_argument('--pw',
  35 + default='',
  36 + type=str,
  37 + help='default password')
  38 +
  39 +argparser.add_argument('csvfile',
  40 + nargs='?',
  41 + type=str,
  42 + default='',
  43 + help='CSV filename')
  44 +
27 args = argparser.parse_args() 45 args = argparser.parse_args()
28 46
29 # =======================================================x==================== 47 # =======================================================x====================
@@ -82,7 +100,8 @@ try: @@ -82,7 +100,8 @@ try:
82 except Exception as e: 100 except Exception as e:
83 print(f'Error: Database "{args.db}" already exists?') 101 print(f'Error: Database "{args.db}" already exists?')
84 session.rollback() 102 session.rollback()
85 - exit(1) 103 + raise e
  104 + # exit(1)
86 105
87 else: 106 else:
88 # --- end session --- 107 # --- end session ---
@@ -55,7 +55,7 @@ class StudentKnowledge(object): @@ -55,7 +55,7 @@ class StudentKnowledge(object):
55 'level': 0.0, # then unlock 55 'level': 0.0, # then unlock
56 'date': datetime.now() 56 'date': datetime.now()
57 } 57 }
58 - logger.debug(f'unlocked {topic}') 58 + logger.debug(f'Unlocked "{topic}".')
59 59
60 60
61 # ------------------------------------------------------------------------ 61 # ------------------------------------------------------------------------
@@ -10,7 +10,6 @@ import bcrypt @@ -10,7 +10,6 @@ import bcrypt
10 from sqlalchemy import create_engine 10 from sqlalchemy import create_engine
11 from sqlalchemy.orm import sessionmaker 11 from sqlalchemy.orm import sessionmaker
12 import networkx as nx 12 import networkx as nx
13 -import yaml  
14 13
15 # this project 14 # this project
16 from models import Student, Answer, Topic, StudentTopic 15 from models import Student, Answer, Topic, StudentTopic
@@ -25,22 +24,25 @@ logger = logging.getLogger(__name__) @@ -25,22 +24,25 @@ logger = logging.getLogger(__name__)
25 class LearnAppException(Exception): 24 class LearnAppException(Exception):
26 pass 25 pass
27 26
  27 +
28 # ============================================================================ 28 # ============================================================================
29 # LearnApp - application logic 29 # LearnApp - application logic
30 # ============================================================================ 30 # ============================================================================
31 class LearnApp(object): 31 class LearnApp(object):
32 - def __init__(self, conffile): 32 + def __init__(self, config_file):
33 # state of online students 33 # state of online students
34 self.online = {} 34 self.online = {}
35 35
36 - # dependency graph shared by all students  
37 - self.deps = build_dependency_graph(conffile) 36 + config = load_yaml(config_file)
38 37
39 # connect to database and checks for registered students 38 # connect to database and checks for registered students
40 - self.db_setup(self.deps.graph['database']) 39 + self.db_setup(config['database'])
  40 +
  41 + # dependency graph shared by all students
  42 + self.deps = build_dependency_graph(config)
41 43
42 # add topics from dependency graph to the database, if missing 44 # add topics from dependency graph to the database, if missing
43 - self.db_add_topics() 45 + self.db_add_missing_topics(self.deps.nodes())
44 46
45 # ------------------------------------------------------------------------ 47 # ------------------------------------------------------------------------
46 # login 48 # login
@@ -79,29 +81,6 @@ class LearnApp(object): @@ -79,29 +81,6 @@ class LearnApp(object):
79 # logout 81 # logout
80 # ------------------------------------------------------------------------ 82 # ------------------------------------------------------------------------
81 def logout(self, uid): 83 def logout(self, uid):
82 - # state = self.online[uid]['state'].state # dict {node:level,...}  
83 - # # save topics state to database  
84 - # with self.db_session(autoflush=False) as s:  
85 -  
86 - # # update existing associations and remove from state dict  
87 - # for a in s.query(StudentTopic).filter_by(student_id=uid):  
88 - # if a.topic_id in state:  
89 - # d = state.pop(a.topic_id)  
90 - # a.level = d['level'] #state.pop(a.topic_id) # update  
91 - # a.date = str(d['date'])  
92 - # s.add(a)  
93 -  
94 - # # insert the remaining ones  
95 - # u = s.query(Student).get(uid)  
96 - # for n,d in state.items():  
97 - # a = StudentTopic(level=d['level'], date=str(d['date']))  
98 - # t = s.query(Topic).get(n)  
99 - # if t is None: # create if topic doesn't exist yet  
100 - # t = Topic(id=n)  
101 - # a.topic = t  
102 - # u.topics.append(a)  
103 - # s.add(a)  
104 -  
105 del self.online[uid] 84 del self.online[uid]
106 logger.info(f'User "{uid}" logged out') 85 logger.info(f'User "{uid}" logged out')
107 86
@@ -126,23 +105,14 @@ class LearnApp(object): @@ -126,23 +105,14 @@ class LearnApp(object):
126 knowledge = self.online[uid]['state'] 105 knowledge = self.online[uid]['state']
127 grade = knowledge.check_answer(answer) 106 grade = knowledge.check_answer(answer)
128 107
129 - # if finished topic, save in database  
130 if knowledge.get_current_question() is None: 108 if knowledge.get_current_question() is None:
  109 + # finished topic, save into database
131 finished_topic = knowledge.get_current_topic() 110 finished_topic = knowledge.get_current_topic()
132 level = knowledge.get_topic_level(finished_topic) 111 level = knowledge.get_topic_level(finished_topic)
133 date = str(knowledge.get_topic_date(finished_topic)) 112 date = str(knowledge.get_topic_date(finished_topic))
  113 + finished_questions = knowledge.get_finished_questions()
134 114
135 with self.db_session(autoflush=False) as s: 115 with self.db_session(autoflush=False) as s:
136 - # save questions from finished_questions list  
137 - s.add_all([  
138 - Answer(  
139 - ref=q['ref'],  
140 - grade=q['grade'],  
141 - starttime=str(q['start_time']),  
142 - finishtime=str(q['finish_time']),  
143 - student_id=uid)  
144 - for q in knowledge.get_finished_questions()])  
145 -  
146 # save topic 116 # save topic
147 a = s.query(StudentTopic).filter_by(student_id=uid, topic_id=finished_topic).one_or_none() 117 a = s.query(StudentTopic).filter_by(student_id=uid, topic_id=finished_topic).one_or_none()
148 if a is None: 118 if a is None:
@@ -152,12 +122,23 @@ class LearnApp(object): @@ -152,12 +122,23 @@ class LearnApp(object):
152 t = s.query(Topic).get(finished_topic) 122 t = s.query(Topic).get(finished_topic)
153 a.topic = t 123 a.topic = t
154 u.topics.append(a) 124 u.topics.append(a)
155 - s.add(a)  
156 else: 125 else:
157 # update studenttopic in database 126 # update studenttopic in database
158 a.level = level 127 a.level = level
159 a.date = date 128 a.date = date
160 - s.add(a) 129 +
  130 + s.add(a)
  131 +
  132 + # save answered questions from finished_questions list
  133 + s.add_all([
  134 + Answer(
  135 + ref=q['ref'],
  136 + grade=q['grade'],
  137 + starttime=str(q['start_time']),
  138 + finishtime=str(q['finish_time']),
  139 + student_id=uid,
  140 + topic_id=finished_topic)
  141 + for q in finished_questions])
161 142
162 return grade 143 return grade
163 144
@@ -170,26 +151,33 @@ class LearnApp(object): @@ -170,26 +151,33 @@ class LearnApp(object):
170 # ------------------------------------------------------------------------ 151 # ------------------------------------------------------------------------
171 # Fill db table 'Topic' with topics from the graph if not already there. 152 # Fill db table 'Topic' with topics from the graph if not already there.
172 # ------------------------------------------------------------------------ 153 # ------------------------------------------------------------------------
173 - def db_add_topics(self): 154 + def db_add_missing_topics(self, nn):
174 with self.db_session() as s: 155 with self.db_session() as s:
175 tt = [t[0] for t in s.query(Topic.id)] # db list of topics 156 tt = [t[0] for t in s.query(Topic.id)] # db list of topics
176 - nn = self.deps.nodes() # topics in the graph  
177 - s.add_all([Topic(id=n) for n in nn if n not in tt]) 157 + missing_topics = [Topic(id=n) for n in nn if n not in tt]
  158 + if missing_topics:
  159 + s.add_all(missing_topics)
  160 + logger.info(f'Added {len(missing_topics)} new topics to the database.')
178 161
179 # ------------------------------------------------------------------------ 162 # ------------------------------------------------------------------------
180 # setup and check database 163 # setup and check database
181 # ------------------------------------------------------------------------ 164 # ------------------------------------------------------------------------
182 def db_setup(self, db): 165 def db_setup(self, db):
  166 + logger.info(f'Checking database "{db}":')
183 engine = create_engine(f'sqlite:///{db}', echo=False) 167 engine = create_engine(f'sqlite:///{db}', echo=False)
184 self.Session = sessionmaker(bind=engine) 168 self.Session = sessionmaker(bind=engine)
185 try: 169 try:
186 with self.db_session() as s: 170 with self.db_session() as s:
187 n = s.query(Student).count() 171 n = s.query(Student).count()
  172 + m = s.query(Topic).count()
  173 + q = s.query(Answer).count()
188 except Exception as e: 174 except Exception as e:
189 logger.critical(f'Database "{db}" not usable.') 175 logger.critical(f'Database "{db}" not usable.')
190 sys.exit(1) 176 sys.exit(1)
191 else: 177 else:
192 - logger.info(f'Database "{db}" has {n} students.') 178 + logger.info(f'{n:4} students.')
  179 + logger.info(f'{m:4} topics.')
  180 + logger.info(f'{q:4} questions answered.')
193 181
194 # ------------------------------------------------------------------------ 182 # ------------------------------------------------------------------------
195 # helper to manage db sessions using the `with` statement, for example 183 # helper to manage db sessions using the `with` statement, for example
@@ -255,7 +243,8 @@ class LearnApp(object): @@ -255,7 +243,8 @@ class LearnApp(object):
255 243
256 244
257 # ============================================================================ 245 # ============================================================================
258 -# Given configuration file, loads YAML on that file and builds a digraph. 246 +# Builds a digraph.
  247 +#
259 # First, topics such as `computer/mips/exceptions` are added as nodes 248 # First, topics such as `computer/mips/exceptions` are added as nodes
260 # together with dependencies. Then, questions are loaded to a factory. 249 # together with dependencies. Then, questions are loaded to a factory.
261 # 250 #
@@ -268,19 +257,8 @@ class LearnApp(object): @@ -268,19 +257,8 @@ class LearnApp(object):
268 # g.node['my/topic']['questions'] list of question refs defined in YAML 257 # g.node['my/topic']['questions'] list of question refs defined in YAML
269 # g.node['my/topic']['factory'] dict with question factories 258 # g.node['my/topic']['factory'] dict with question factories
270 # ---------------------------------------------------------------------------- 259 # ----------------------------------------------------------------------------
271 -def build_dependency_graph(config_file):  
272 - # Load configuration file to a dict  
273 - try:  
274 - with open(config_file, 'r') as f:  
275 - config = yaml.load(f)  
276 - except FileNotFoundError:  
277 - logger.critical(f'File not found: "{config_file}"')  
278 - raise LearnAppException  
279 - except yaml.scanner.ScannerError as err:  
280 - logger.critical(f'Parsing YAML file "{config_file}": {err}')  
281 - raise LearnAppException  
282 - else:  
283 - logger.info(f'Configuration file "{config_file}"') 260 +def build_dependency_graph(config={}):
  261 + logger.info('Building topic dependency graph.')
284 262
285 # create graph 263 # create graph
286 prefix = config.get('path', '.') 264 prefix = config.get('path', '.')
@@ -51,9 +51,11 @@ class Answer(Base): @@ -51,9 +51,11 @@ class Answer(Base):
51 starttime = Column(String) 51 starttime = Column(String)
52 finishtime = Column(String) 52 finishtime = Column(String)
53 student_id = Column(String, ForeignKey('students.id')) 53 student_id = Column(String, ForeignKey('students.id'))
  54 + topic_id = Column(String, ForeignKey('topics.id'))
54 55
55 # --- 56 # ---
56 student = relationship('Student', back_populates='answers') 57 student = relationship('Student', back_populates='answers')
  58 + topic = relationship('Topic', back_populates='answers')
57 59
58 def __repr__(self): 60 def __repr__(self):
59 return '''Question: 61 return '''Question:
@@ -73,6 +75,7 @@ class Topic(Base): @@ -73,6 +75,7 @@ class Topic(Base):
73 75
74 # --- 76 # ---
75 students = relationship('StudentTopic', back_populates='topic') 77 students = relationship('StudentTopic', back_populates='topic')
  78 + answers = relationship('Answer', back_populates='topic')
76 79
77 # def __init__(self, id): 80 # def __init__(self, id):
78 # self.id = id 81 # self.id = id
@@ -197,7 +197,7 @@ class QuestionHandler(BaseHandler): @@ -197,7 +197,7 @@ class QuestionHandler(BaseHandler):
197 return { 197 return {
198 'method': 'finished_topic', 198 'method': 'finished_topic',
199 'params': { # FIXME no html here please! 199 'params': { # FIXME no html here please!
200 - 'question': f'<img src="/static/trophy.svg" alt="trophy" class="img-fluid mx-auto d-block" width="50%">' 200 + 'question': f'<img src="/static/trophy.svg" alt="trophy" class="img-fluid mx-auto d-block" width="35%">'
201 } 201 }
202 } 202 }
203 203
static/trophy.png

163 KB

templates/maintopics.html
1 {% autoescape %} 1 {% autoescape %}
2 2
3 -<!DOCTYPE html> 3 +<!doctype html>
4 <html lang="pt-PT"> 4 <html lang="pt-PT">
5 <head> 5 <head>
6 <title>iLearn</title> 6 <title>iLearn</title>
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 <meta name="author" content="Miguel Barão"> 11 <meta name="author" content="Miguel Barão">
12 12
13 <!-- Bootstrap, Fontawesome --> 13 <!-- Bootstrap, Fontawesome -->
14 - <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> 14 + <link rel="stylesheet" href="/static/bootstrap/css/bootstrap-materia.min.css">
15 <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css"> 15 <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css">
16 16
17 <!-- Other --> 17 <!-- Other -->
@@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
27 </head> 27 </head>
28 <!-- ===================================================================== --> 28 <!-- ===================================================================== -->
29 <body> 29 <body>
30 -<nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-dark"> 30 +<nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-primary">
31 <a class="navbar-brand" href="#"> 31 <a class="navbar-brand" href="#">
32 <img src="/static/logo_horizontal.png" height="30" alt=""> 32 <img src="/static/logo_horizontal.png" height="30" alt="">
33 </a> 33 </a>
@@ -66,13 +66,13 @@ @@ -66,13 +66,13 @@
66 <div class="list-group my-3"> 66 <div class="list-group my-3">
67 {% for t in state %} 67 {% for t in state %}
68 {% if t['level'] is None %} 68 {% if t['level'] is None %}
69 - <a class="list-group-item list-group-item-action bg-light disabled" href="#"> 69 + <a class="list-group-item list-group-item-action bg-light disabled">
70 <div class="d-flex justify-content-start"> 70 <div class="d-flex justify-content-start">
71 - <div class="p-2 font-italic"> 71 + <div class="p-2 font-italic text-muted">
72 {{ t['name'] }} 72 {{ t['name'] }}
73 </div> 73 </div>
74 <div class="ml-auto p-2"> 74 <div class="ml-auto p-2">
75 - <i class="fa fa-lock text-danger" aria-hidden="true"></i> 75 + <i class="fa fa-lock text-danger fa-lg" aria-hidden="true"></i>
76 </div> 76 </div>
77 </div> 77 </div>
78 </a> 78 </a>
@@ -85,7 +85,7 @@ @@ -85,7 +85,7 @@
85 <div class="ml-auto p-2"> 85 <div class="ml-auto p-2">
86 {% if t['level'] < 0.01 %} 86 {% if t['level'] < 0.01 %}
87 87
88 - <i class="fa fa-unlock text-success" aria-hidden="true"></i> 88 + <i class="fa fa-unlock text-success fa-lg" aria-hidden="true"></i>
89 {% else %} 89 {% else %}
90 <span class="text-nowrap"> 90 <span class="text-nowrap">
91 {{ round(t['level']*5)*'<i class="fa fa-star text-warning" aria-hidden="true"></i>' + round(5-t['level']*5)*'<i class="fa fa-star-o text-muted" aria-hidden="true"></i>' }} 91 {{ round(t['level']*5)*'<i class="fa fa-star text-warning" aria-hidden="true"></i>' + round(5-t['level']*5)*'<i class="fa fa-star-o text-muted" aria-hidden="true"></i>' }}
templates/topic.html
@@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
19 <script type="text/javascript" src="/static/mathjax/MathJax.js?delayStartupUntil=onload&config=TeX-AMS_CHTML-full"></script> 19 <script type="text/javascript" src="/static/mathjax/MathJax.js?delayStartupUntil=onload&config=TeX-AMS_CHTML-full"></script>
20 20
21 <!-- Bootstrap --> 21 <!-- Bootstrap -->
22 - <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> 22 + <link rel="stylesheet" href="/static/bootstrap/css/bootstrap-materia.min.css">
23 <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css"> 23 <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css">
24 <link rel="stylesheet" href="/static/css/animate.min.css"> 24 <link rel="stylesheet" href="/static/css/animate.min.css">
25 <link rel="stylesheet" href="/static/css/github.css"> 25 <link rel="stylesheet" href="/static/css/github.css">
@@ -58,7 +58,7 @@ @@ -58,7 +58,7 @@
58 <body> 58 <body>
59 59
60 <!-- Navbar --> 60 <!-- Navbar -->
61 -<nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-dark"> 61 +<nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-primary">
62 <a class="navbar-brand" href="#"> 62 <a class="navbar-brand" href="#">
63 <img src="/static/logo_horizontal.png" height="30" alt=""> 63 <img src="/static/logo_horizontal.png" height="30" alt="">
64 </a> 64 </a>
@@ -96,8 +96,6 @@ @@ -96,8 +96,6 @@
96 96
97 <div id="notifications"></div> 97 <div id="notifications"></div>
98 98
99 - <!-- <img src="/static/trophy.svg" alt="trophy" class="img-fluid mx-auto d-block hidden" width="50%"> -->  
100 -  
101 <div class="my-5" id="content"> 99 <div class="my-5" id="content">
102 <form action="/question" method="post" id="question_form" autocomplete="off"> 100 <form action="/question" method="post" id="question_form" autocomplete="off">
103 {% module xsrf_form_html() %} 101 {% module xsrf_form_html() %}
@@ -135,8 +135,14 @@ def md_to_html(text, q=None): @@ -135,8 +135,14 @@ def md_to_html(text, q=None):
135 def load_yaml(filename, default=None): 135 def load_yaml(filename, default=None):
136 try: 136 try:
137 f = open(path.expanduser(filename), 'r', encoding='utf-8') 137 f = open(path.expanduser(filename), 'r', encoding='utf-8')
  138 + except FileNotFoundError:
  139 + logger.error(f'Can\'t open "{script}": not found.')
  140 + return default
  141 + except PermissionError:
  142 + logger.error(f'Can\'t open "{script}": no permission.')
  143 + return default
138 except IOError: 144 except IOError:
139 - logger.error(f'Can\'t open file "{filename}"') 145 + logger.error(f'Can\'t open file "{filename}".')
140 return default 146 return default
141 else: 147 else:
142 with f: 148 with f: