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.
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:
... ...