app.py
7.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# python standard library
from contextlib import contextmanager # `with` statement in db sessions
import logging
from os import path
# user installed libraries
try:
import bcrypt
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import networkx as nx
import yaml
except ImportError:
logger.critical('Python package missing. See README.md for instructions.')
sys.exit(1)
# this project
from models import Student, Answer, Topic, StudentTopic
from knowledge import Knowledge
from questions import QFactory
from tools import load_yaml
# setup logger for this module
logger = logging.getLogger(__name__)
# ============================================================================
# LearnApp - application logic
# ============================================================================
class LearnApp(object):
def __init__(self):
# online students
self.online = {}
# connect to database and check registered students
self.setup_db('students.db') # FIXME
# build dependency graph
self.build_dependency_graph('demo/config.yaml') # FIXME
# ------------------------------------------------------------------------
def login(self, uid, try_pw):
with self.db_session() as s:
student = s.query(Student).filter(Student.id == uid).one_or_none()
if student is None:
logger.info(f'User "{uid}" does not exist.')
return False # student does not exist or already loggeg in
hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password)
if hashedtry != student.password:
logger.info(f'User "{uid}" wrong password.')
return False # wrong password
student_state = s.query(StudentTopic).filter(StudentTopic.student_id == uid).all()
# success
self.online[uid] = {
'name': student.name,
'number': student.id,
'state': Knowledge(self.depgraph, student_state),
}
logger.info(f'User "{uid}" logged in')
return True
# ------------------------------------------------------------------------
# logout
def logout(self, uid):
state = self.online[uid]['state'].state # dict {node:level,...}
print(state)
with self.db_session(autoflush=False) as s:
# update existing associations and remove from state dict
aa = s.query(StudentTopic).filter_by(student_id=uid).all()
for a in aa:
print('update ', a)
a.level = state.pop(a.topic_id) # update
s.add_all(aa)
# insert the remaining ones
u = s.query(Student).get(uid)
for n,l in state.items():
a = StudentTopic(level=l)
t = s.query(Topic).get(n)
if t is None: # create if topic doesn't exist yet
t = Topic(n)
a.topic = t
u.topics.append(a)
s.add(a)
del self.online[uid]
logger.info(f'User "{uid}" logged out')
# ------------------------------------------------------------------------
def get_student_name(self, uid):
return self.online[uid].get('name', '')
# ------------------------------------------------------------------------
def get_current_public_dir(self, uid):
topic = self.online[uid]['state'].topic
p = self.depgraph.graph['path']
return path.join(p, topic, 'public')
# ------------------------------------------------------------------------
# check answer and if correct returns new question, otherise returns None
def check_answer(self, uid, answer):
logger.debug(f'check_answer("{uid}", "{answer}")')
knowledge = self.online[uid]['state']
current_question = knowledge.check_answer(answer)
if current_question is not None:
logger.debug('check_answer: saving answer to db ...')
with self.db_session() as s:
s.add(Answer(
ref=current_question['ref'],
grade=current_question['grade'],
starttime=str(current_question['start_time']),
finishtime=str(current_question['finish_time']),
student_id=uid))
s.commit()
logger.debug('check_answer: saving done')
logger.debug('check_answer: will return knowledge.new_question')
return knowledge.new_question()
# ------------------------------------------------------------------------
# helper to manage db sessions using the `with` statement, for example
# with self.db_session() as s: s.query(...)
@contextmanager
def db_session(self, **kw):
session = self.Session(**kw)
try:
yield session
session.commit()
except Exception as e:
session.rollback()
raise e
finally:
session.close()
# ------------------------------------------------------------------------
# Receives a set of topics (strings like "math/algebra"),
# and recursively adds dependencies to the dependency graph
def build_dependency_graph(self, config_file):
logger.debug(f'build_dependency_graph("{config_file}")')
# Load configuration file
try:
with open(config_file, 'r') as f:
logger.info(f'Loading configuration file "{config_file}"')
config = yaml.load(f)
except FileNotFoundError as e:
logger.error(f'File not found: "{config_file}"')
raise e
prefix = config['path'] # FIXME default if does not exist?
g = nx.DiGraph(path=prefix)
# Build dependency graph
deps = config.get('dependencies', {})
for n,dd in deps.items():
g.add_edges_from((n,d) for d in dd)
# Builds factories for each node
for n in g.nodes_iter():
fullpath = path.join(prefix, n)
# if name is directory defaults to "prefix/questions.yaml"
if path.isdir(fullpath):
filename = path.join(fullpath, "questions.yaml")
if path.isfile(filename):
logger.info(f'Loading questions from "{filename}"')
questions = load_yaml(filename, default=[])
for q in questions:
q['path'] = fullpath
g.node[n]['factory'] = [QFactory(q) for q in questions]
self.depgraph = g
# ------------------------------------------------------------------------
# setup and check database
def setup_db(self, db):
engine = create_engine(f'sqlite:///{db}', echo=False)
self.Session = sessionmaker(bind=engine)
try:
with self.db_session() as s:
n = s.query(Student).count()
except Exception as e:
logger.critical('Database not usable.')
raise e
else:
logger.info(f'Database has {n} students registered.')