Commit efdbe121d0ec2d33eb16d4345b744a7c0c2a1c9a
1 parent
0a01fe13
Exists in
master
and in
1 other branch
- added database column "topic" in answers table.
- some code refactoring and cleanup in learnapp.
Showing
10 changed files
with
86 additions
and
80 deletions
Show diff stats
BUGS.md
... | ... | @@ -3,6 +3,7 @@ BUGS: |
3 | 3 | |
4 | 4 | - servidor http com redirect para https. |
5 | 5 | - servir imagens/ficheiros. |
6 | +- codemirror em textarea. | |
6 | 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 | 30 | |
30 | 31 | FIXED: |
31 | 32 | |
33 | +- database: answers não tem referencia para o topico, so para question_ref | |
32 | 34 | - melhorar markdown das tabelas. |
33 | 35 | - gravar evolucao na bd no final de cada topico. |
34 | 36 | - submeter questoes radio, da erro se nao escolher nenhuma opção. | ... | ... |
initdb.py
... | ... | @@ -19,11 +19,29 @@ def fix(name): |
19 | 19 | |
20 | 20 | # =========================================================================== |
21 | 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 | 45 | args = argparser.parse_args() |
28 | 46 | |
29 | 47 | # =======================================================x==================== |
... | ... | @@ -82,7 +100,8 @@ try: |
82 | 100 | except Exception as e: |
83 | 101 | print(f'Error: Database "{args.db}" already exists?') |
84 | 102 | session.rollback() |
85 | - exit(1) | |
103 | + raise e | |
104 | + # exit(1) | |
86 | 105 | |
87 | 106 | else: |
88 | 107 | # --- end session --- | ... | ... |
knowledge.py
... | ... | @@ -55,7 +55,7 @@ class StudentKnowledge(object): |
55 | 55 | 'level': 0.0, # then unlock |
56 | 56 | 'date': datetime.now() |
57 | 57 | } |
58 | - logger.debug(f'unlocked {topic}') | |
58 | + logger.debug(f'Unlocked "{topic}".') | |
59 | 59 | |
60 | 60 | |
61 | 61 | # ------------------------------------------------------------------------ | ... | ... |
learnapp.py
... | ... | @@ -10,7 +10,6 @@ import bcrypt |
10 | 10 | from sqlalchemy import create_engine |
11 | 11 | from sqlalchemy.orm import sessionmaker |
12 | 12 | import networkx as nx |
13 | -import yaml | |
14 | 13 | |
15 | 14 | # this project |
16 | 15 | from models import Student, Answer, Topic, StudentTopic |
... | ... | @@ -25,22 +24,25 @@ logger = logging.getLogger(__name__) |
25 | 24 | class LearnAppException(Exception): |
26 | 25 | pass |
27 | 26 | |
27 | + | |
28 | 28 | # ============================================================================ |
29 | 29 | # LearnApp - application logic |
30 | 30 | # ============================================================================ |
31 | 31 | class LearnApp(object): |
32 | - def __init__(self, conffile): | |
32 | + def __init__(self, config_file): | |
33 | 33 | # state of online students |
34 | 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 | 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 | 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 | 48 | # login |
... | ... | @@ -79,29 +81,6 @@ class LearnApp(object): |
79 | 81 | # logout |
80 | 82 | # ------------------------------------------------------------------------ |
81 | 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 | 84 | del self.online[uid] |
106 | 85 | logger.info(f'User "{uid}" logged out') |
107 | 86 | |
... | ... | @@ -126,23 +105,14 @@ class LearnApp(object): |
126 | 105 | knowledge = self.online[uid]['state'] |
127 | 106 | grade = knowledge.check_answer(answer) |
128 | 107 | |
129 | - # if finished topic, save in database | |
130 | 108 | if knowledge.get_current_question() is None: |
109 | + # finished topic, save into database | |
131 | 110 | finished_topic = knowledge.get_current_topic() |
132 | 111 | level = knowledge.get_topic_level(finished_topic) |
133 | 112 | date = str(knowledge.get_topic_date(finished_topic)) |
113 | + finished_questions = knowledge.get_finished_questions() | |
134 | 114 | |
135 | 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 | 116 | # save topic |
147 | 117 | a = s.query(StudentTopic).filter_by(student_id=uid, topic_id=finished_topic).one_or_none() |
148 | 118 | if a is None: |
... | ... | @@ -152,12 +122,23 @@ class LearnApp(object): |
152 | 122 | t = s.query(Topic).get(finished_topic) |
153 | 123 | a.topic = t |
154 | 124 | u.topics.append(a) |
155 | - s.add(a) | |
156 | 125 | else: |
157 | 126 | # update studenttopic in database |
158 | 127 | a.level = level |
159 | 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 | 143 | return grade |
163 | 144 | |
... | ... | @@ -170,26 +151,33 @@ class LearnApp(object): |
170 | 151 | # ------------------------------------------------------------------------ |
171 | 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 | 155 | with self.db_session() as s: |
175 | 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 | 163 | # setup and check database |
181 | 164 | # ------------------------------------------------------------------------ |
182 | 165 | def db_setup(self, db): |
166 | + logger.info(f'Checking database "{db}":') | |
183 | 167 | engine = create_engine(f'sqlite:///{db}', echo=False) |
184 | 168 | self.Session = sessionmaker(bind=engine) |
185 | 169 | try: |
186 | 170 | with self.db_session() as s: |
187 | 171 | n = s.query(Student).count() |
172 | + m = s.query(Topic).count() | |
173 | + q = s.query(Answer).count() | |
188 | 174 | except Exception as e: |
189 | 175 | logger.critical(f'Database "{db}" not usable.') |
190 | 176 | sys.exit(1) |
191 | 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 | 183 | # helper to manage db sessions using the `with` statement, for example |
... | ... | @@ -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 | 248 | # First, topics such as `computer/mips/exceptions` are added as nodes |
260 | 249 | # together with dependencies. Then, questions are loaded to a factory. |
261 | 250 | # |
... | ... | @@ -268,19 +257,8 @@ class LearnApp(object): |
268 | 257 | # g.node['my/topic']['questions'] list of question refs defined in YAML |
269 | 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 | 263 | # create graph |
286 | 264 | prefix = config.get('path', '.') | ... | ... |
models.py
... | ... | @@ -51,9 +51,11 @@ class Answer(Base): |
51 | 51 | starttime = Column(String) |
52 | 52 | finishtime = Column(String) |
53 | 53 | student_id = Column(String, ForeignKey('students.id')) |
54 | + topic_id = Column(String, ForeignKey('topics.id')) | |
54 | 55 | |
55 | 56 | # --- |
56 | 57 | student = relationship('Student', back_populates='answers') |
58 | + topic = relationship('Topic', back_populates='answers') | |
57 | 59 | |
58 | 60 | def __repr__(self): |
59 | 61 | return '''Question: |
... | ... | @@ -73,6 +75,7 @@ class Topic(Base): |
73 | 75 | |
74 | 76 | # --- |
75 | 77 | students = relationship('StudentTopic', back_populates='topic') |
78 | + answers = relationship('Answer', back_populates='topic') | |
76 | 79 | |
77 | 80 | # def __init__(self, id): |
78 | 81 | # self.id = id | ... | ... |
serve.py
... | ... | @@ -197,7 +197,7 @@ class QuestionHandler(BaseHandler): |
197 | 197 | return { |
198 | 198 | 'method': 'finished_topic', |
199 | 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 | 1 | {% autoescape %} |
2 | 2 | |
3 | -<!DOCTYPE html> | |
3 | +<!doctype html> | |
4 | 4 | <html lang="pt-PT"> |
5 | 5 | <head> |
6 | 6 | <title>iLearn</title> |
... | ... | @@ -11,7 +11,7 @@ |
11 | 11 | <meta name="author" content="Miguel Barão"> |
12 | 12 | |
13 | 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 | 15 | <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css"> |
16 | 16 | |
17 | 17 | <!-- Other --> |
... | ... | @@ -27,7 +27,7 @@ |
27 | 27 | </head> |
28 | 28 | <!-- ===================================================================== --> |
29 | 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 | 31 | <a class="navbar-brand" href="#"> |
32 | 32 | <img src="/static/logo_horizontal.png" height="30" alt=""> |
33 | 33 | </a> |
... | ... | @@ -66,13 +66,13 @@ |
66 | 66 | <div class="list-group my-3"> |
67 | 67 | {% for t in state %} |
68 | 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 | 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 | 72 | {{ t['name'] }} |
73 | 73 | </div> |
74 | 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 | 76 | </div> |
77 | 77 | </div> |
78 | 78 | </a> |
... | ... | @@ -85,7 +85,7 @@ |
85 | 85 | <div class="ml-auto p-2"> |
86 | 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 | 89 | {% else %} |
90 | 90 | <span class="text-nowrap"> |
91 | 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 | 19 | <script type="text/javascript" src="/static/mathjax/MathJax.js?delayStartupUntil=onload&config=TeX-AMS_CHTML-full"></script> |
20 | 20 | |
21 | 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 | 23 | <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css"> |
24 | 24 | <link rel="stylesheet" href="/static/css/animate.min.css"> |
25 | 25 | <link rel="stylesheet" href="/static/css/github.css"> |
... | ... | @@ -58,7 +58,7 @@ |
58 | 58 | <body> |
59 | 59 | |
60 | 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 | 62 | <a class="navbar-brand" href="#"> |
63 | 63 | <img src="/static/logo_horizontal.png" height="30" alt=""> |
64 | 64 | </a> |
... | ... | @@ -96,8 +96,6 @@ |
96 | 96 | |
97 | 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 | 99 | <div class="my-5" id="content"> |
102 | 100 | <form action="/question" method="post" id="question_form" autocomplete="off"> |
103 | 101 | {% module xsrf_form_html() %} | ... | ... |
tools.py
... | ... | @@ -135,8 +135,14 @@ def md_to_html(text, q=None): |
135 | 135 | def load_yaml(filename, default=None): |
136 | 136 | try: |
137 | 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 | 144 | except IOError: |
139 | - logger.error(f'Can\'t open file "{filename}"') | |
145 | + logger.error(f'Can\'t open file "{filename}".') | |
140 | 146 | return default |
141 | 147 | else: |
142 | 148 | with f: | ... | ... |