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,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. |
initdb.py
| @@ -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 --- |
knowledge.py
| @@ -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 | # ------------------------------------------------------------------------ |
learnapp.py
| @@ -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', '.') |
models.py
| @@ -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 |
serve.py
| @@ -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() %} |
tools.py
| @@ -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: |