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