Commit d1410958a5c4b53c8654156d31541831d01d3d52

Authored by Miguel Barão
1 parent 24f1051c
Exists in master and in 1 other branch dev

- starts do do something...

- many topics do not work yet.
knowledge.py
... ... @@ -20,7 +20,6 @@ class StudentKnowledge(object):
20 20 # =======================================================================
21 21 # methods that update state
22 22 # =======================================================================
23   -
24 23 def __init__(self, deps, state={}):
25 24 # graph with topic dependencies shared between all students
26 25 self.deps = deps
... ... @@ -38,12 +37,9 @@ class StudentKnowledge(object):
38 37 # compute recommended sequence of topics ['a', 'b', ...]
39 38 self.topic_sequence = list(nx.topological_sort(self.deps))
40 39  
41   - # select a topic to do and initialize questions
42   - # self.start_topic()
43   -
44 40  
45 41 # ------------------------------------------------------------------------
46   - # Unlock all topics whose dependencies are satisfied
  42 + # Unlock topics whose dependencies are satisfied (> min_level)
47 43 # ------------------------------------------------------------------------
48 44 def unlock_topics(self):
49 45 min_level = 0.01 # minimum level to unlock
... ... @@ -60,9 +56,10 @@ class StudentKnowledge(object):
60 56  
61 57 # ------------------------------------------------------------------------
62 58 # Recommends a topic to practice/learn from the state.
  59 + # FIXME untested
63 60 # ------------------------------------------------------------------------
64 61 def recommended_topic(self):
65   - return min(self.state.items(), key=lambda x: x[1])[0]
  62 + return min(self.state.items(), key=lambda x: x[1]['level'])[0]
66 63  
67 64  
68 65 # if not topic:
... ... @@ -81,14 +78,15 @@ class StudentKnowledge(object):
81 78 # logger.debug(f'Can\'t start topic "{topic}".')
82 79 # return
83 80  
  81 +
84 82 # ------------------------------------------------------------------------
85   - # Start a new topic. If not provided, selects the first with level < 0.8
86   - # If all levels > 0.8, will stay in the last one forever...
  83 + # Start a new topic. If not provided, gets a recommendation.
  84 + # questions: list of generated questions to do in the topic
  85 + # finished_questions: [] will contain correctly answered questions
  86 + # current_question: the current question to be presented
87 87 # ------------------------------------------------------------------------
88   - def start_topic(self, topic=''):
89   - # unlock topics whose dependencies are done
90   -
91   - self.unlock_topics()
  88 + def init_topic(self, topic=''):
  89 + # self.unlock_topics() # FIXME needed?
92 90  
93 91 if not topic:
94 92 topic = self.recommended_topic()
... ... @@ -97,26 +95,30 @@ class StudentKnowledge(object):
97 95 self.current_topic = topic
98 96 logger.info(f'Topic set to "{topic}"')
99 97  
100   -
101 98 # generate question instances for current topic
102 99 factory = self.deps.node[topic]['factory']
103 100 questionlist = self.deps.node[topic]['questions']
104 101  
105 102 self.questions = [factory[qref].generate() for qref in questionlist]
106   - self.current_question = self.questions.pop(0) # FIXME crashes if questions==[]
107   - self.current_question['start_time'] = datetime.now()
108 103 self.finished_questions = []
109 104  
  105 + self.current_question = self.questions.pop(0) # FIXME crash if empty
  106 + self.current_question['start_time'] = datetime.now()
  107 +
110 108 # ------------------------------------------------------------------------
111 109 # returns the current question with correction, time and comments updated
112 110 # ------------------------------------------------------------------------
113 111 def check_answer(self, answer):
  112 + logger.debug('StudentKnowledge.check_answer()')
  113 +
114 114 q = self.current_question
115 115 q['finish_time'] = datetime.now()
116   - grade = q.correct(answer)
  116 + q['answer'] = answer
  117 + grade = q.correct()
  118 +
117 119 logger.debug(f'Grade = {grade:.2} ({q["ref"]})')
118 120  
119   - # new question if answer is correct
  121 + # if answer is correct, get next question
120 122 if grade > 0.999:
121 123 self.finished_questions.append(q)
122 124 try:
... ... @@ -127,13 +129,15 @@ class StudentKnowledge(object):
127 129 'level': 1.0,
128 130 'date': datetime.now()
129 131 }
130   - self.start_topic()
131 132 else:
132 133 self.current_question['start_time'] = datetime.now()
  134 +
  135 + # if answer is wrong, keep same question and add a similar one at the end
133 136 else:
134 137 factory = self.deps.node[self.current_topic]['factory']
135 138 self.questions.append(factory[q['ref']].generate())
136 139  
  140 + # returns answered and corrected question
137 141 return q
138 142  
139 143  
... ...
learnapp.py
... ... @@ -60,8 +60,8 @@ class LearnApp(object):
60 60 # success
61 61 logger.info(f'User "{uid}" logged in')
62 62  
63   - state = {}
64 63 tt = s.query(StudentTopic).filter(StudentTopic.student_id == uid)
  64 + state = {}
65 65 for t in tt:
66 66 state[t.topic_id] = {
67 67 'level': t.level,
... ... @@ -141,7 +141,7 @@ class LearnApp(object):
141 141 # Start new topic
142 142 # ------------------------------------------------------------------------
143 143 def start_topic(self, uid, topic):
144   - self.online[uid]['state'].start_topic(topic)
  144 + self.online[uid]['state'].init_topic(topic)
145 145  
146 146 # ------------------------------------------------------------------------
147 147 # Fill db table 'Topic' with topics from the graph if not already there.
... ... @@ -276,13 +276,13 @@ def build_dependency_graph(config_file):
276 276 fullpath = path.expanduser(path.join(prefix, tref))
277 277 filename = path.join(fullpath, 'questions.yaml')
278 278  
279   - loaded_questions = load_yaml(filename, default=[])
  279 + loaded_questions = load_yaml(filename, default=[]) # list
280 280  
281   - # if questions not in configuration then load all, preserve order
  281 + # if questions not in configuration then load all, preserving order
282 282 if not tnode['questions']:
283 283 tnode['questions'] = [q['ref'] for q in loaded_questions]
284 284  
285   - # make questions factory (without repeting same question)
  285 + # make questions factory (without repeating same question)
286 286 tnode['factory'] = {}
287 287 for q in loaded_questions:
288 288 if q['ref'] in tnode['questions']:
... ...
questions.py
... ... @@ -11,9 +11,11 @@ import yaml
11 11 # this project
12 12 from tools import run_script
13 13  
  14 +
14 15 # regular expressions in yaml files, e.g. correct: !regex '[aA]zul'
15 16 yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n)))
16 17  
  18 +
17 19 # setup logger for this module
18 20 logger = logging.getLogger(__name__)
19 21  
... ... @@ -39,8 +41,9 @@ class Question(dict):
39 41 'files': {},
40 42 })
41 43  
42   - def updateAnswer(answer=None):
43   - self['answer'] = answer
  44 + # FIXME unused. does childs need do override this?
  45 + # def updateAnswer(answer=None):
  46 + # self['answer'] = answer
44 47  
45 48 def correct(self):
46 49 self['grade'] = 0.0
... ...
serve.py
... ... @@ -129,9 +129,9 @@ class TopicHandler(BaseHandler):
129 129 @tornado.web.authenticated
130 130 def get(self, topic):
131 131 uid = self.current_user
132   - # topic = self.get_query_argument('topic', default='')
133 132  
134 133 self.learn.start_topic(uid, topic)
  134 +
135 135 self.render('topic.html',
136 136 uid=uid,
137 137 name=self.learn.get_student_name(uid),
... ... @@ -159,12 +159,12 @@ class FileHandler(BaseHandler):
159 159 # ----------------------------------------------------------------------------
160 160 class QuestionHandler(BaseHandler):
161 161 templates = {
162   - 'checkbox': 'question-checkbox.html',
163   - 'radio': 'question-radio.html',
164   - 'text': 'question-text.html',
165   - 'text-regex': 'question-text.html',
  162 + 'checkbox': 'question-checkbox.html',
  163 + 'radio': 'question-radio.html',
  164 + 'text': 'question-text.html',
  165 + 'text-regex': 'question-text.html',
166 166 'numeric-interval': 'question-text.html',
167   - 'textarea': 'question-textarea.html',
  167 + 'textarea': 'question-textarea.html',
168 168 # -- information panels --
169 169 'information': 'question-information.html',
170 170 'info': 'question-information.html',
... ... @@ -175,24 +175,23 @@ class QuestionHandler(BaseHandler):
175 175 }
176 176  
177 177 def new_question(self, user):
178   - state = self.learn.get_student_state(user) # all topics [('a', 0.1), ...]
179   - current_topic = self.learn.get_student_topic(user) # str
180   - progress = self.learn.get_student_progress(user) # float
181   - question = self.learn.get_student_question(user) # dict?
182 178  
183   - question_html = self.render_string(self.templates[question['type']],question=question, md=md)
184   - topics_html = self.render_string('topics.html', state=state, current_topic=current_topic, gettopicname=self.learn.get_topic_name)
  179 + # state = self.learn.get_student_state(user) # [{ref, name, level},...]
  180 + # current_topic = self.learn.get_student_topic(user) # str
  181 +
  182 + question = self.learn.get_student_question(user) # Question
  183 + template = self.templates[question['type']]
  184 + question_html = self.render_string(template, question=question, md=md)
185 185  
186 186 return {
187 187 'method': 'new_question',
188 188 'params': {
189 189 'question': tornado.escape.to_unicode(question_html),
190   - 'state': tornado.escape.to_unicode(topics_html),
191   - 'progress': progress,
  190 + 'progress': self.learn.get_student_progress(user),
192 191 }
193 192 }
194 193  
195   - def shake(self, user):
  194 + def wrong_answer(self, user):
196 195 progress = self.learn.get_student_progress(user) # in the current topic
197 196 return {
198 197 'method': 'shake',
... ... @@ -202,68 +201,54 @@ class QuestionHandler(BaseHandler):
202 201 }
203 202  
204 203 def finished_topic(self, user):
205   - state = self.learn.get_student_state(user) # all topics
206   - current_topic = self.learn.get_student_topic(uid)
  204 + # state = self.learn.get_student_state(user) # all topics
  205 + # current_topic = self.learn.get_student_topic(uid)
207 206  
208   - topics_html = self.render_string('topics.html',
209   - state=state,
210   - current_topic=current_topic,
211   - topicname=self.learn.get_topic_name, # translate ref to names
212   - )
213 207 return {
214 208 'method': 'finished_topic',
215 209 'params': {
216   - 'state': tornado.escape.to_unicode(topics_html),
  210 + 'question': '<img src="/static/trophy.png" alt="trophy" class="img-fluid mx-auto d-block" width="30%">',
  211 + 'progress': 1.0,
217 212 }
218 213 }
219 214  
220 215 @tornado.web.authenticated
221 216 def get(self):
222 217 logging.debug('QuestionHandler.get')
  218 + user = self.current_user
223 219  
224   - uid = self.current_user
225   -
226   - question = self.learn.get_student_question(uid)
227   - print(question) # FIXME its returning different questions on reload...
  220 + question = self.learn.get_student_question(user)
228 221 template = self.templates[question['type']]
229 222 question_html = self.render_string(template, question=question, md=md)
230   - print(question_html)
231 223  
232 224 self.write({
233 225 'method': 'new_question',
234 226 'params': {
235 227 'question': tornado.escape.to_unicode(question_html),
236   - 'progress': 0.3, #self.learn.get_student_progress(uid) ,
  228 + 'progress': self.learn.get_student_progress(user) ,
237 229 }
238 230 })
239 231  
240   -
241   - # question_html = self.render_string(self.templates[question['type']],question=question, md=md)
242   - # response = {
243   - # 'method': 'new_question',
244   - # 'params': {
245   - # 'question': tornado.escape.to_unicode(question_html),
246   - # 'progress': progress,
247   - # }
248   - # }
249   - # self.write(response)
250   -
251 232 # handles answer posted
252 233 @tornado.web.authenticated
253 234 def post(self):
254 235 logging.debug('QuestionHandler.post')
255   -
256 236 user = self.current_user
257   - answer = self.get_body_arguments('answer')
  237 +
  238 + # if self.learn.get_student_question(user) is None:
  239 +
  240 + answer = self.get_body_argument('answer')
  241 +
  242 + # check answer and get next question (same, new or None)
258 243 grade = self.learn.check_answer(user, answer)
259   - question = self.learn.get_student_question(user) # same, new or None
  244 + question = self.learn.get_student_question(user)
260 245  
261 246 if question is None:
262 247 self.write(self.finished_topic(user))
263 248 elif grade > 0.999:
264 249 self.write(self.new_question(user))
265 250 else:
266   - self.write(self.shake(user))
  251 + self.write(self.wrong_answer(user))
267 252  
268 253  
269 254 # -------------------------------------------------------------------------
... ...
static/trophy.png 0 → 100644

163 KB

templates/maintopics.html
... ... @@ -149,4 +149,3 @@
149 149 </script>
150 150 </body>
151 151 </html>
152   -
... ...
templates/topic.html
... ... @@ -52,17 +52,24 @@
52 52 <span class="navbar-toggler-icon"></span>
53 53 </button>
54 54  
55   - <div class="collapse navbar-collapse" id="navbarText">
56   - <ul class="navbar-nav mr-auto">
57   - </ul>
58   -
59   - <span class="navbar-text">
60   - <i class="fa fa-user" aria-hidden="true"></i>
61   - <span id="name">{{ escape(name) }}</span>
62   - (<span id="number">{{ escape(uid) }}</span>)
63   - <span class="caret"></span>
64   - </span>
65   - </div>
  55 + <div class="collapse navbar-collapse" id="navbarText">
  56 + <ul class="navbar-nav mr-auto"></ul>
  57 +
  58 + <ul class="navbar-nav">
  59 + <li class="nav-item dropdown">
  60 + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
  61 + <i class="fa fa-user" aria-hidden="true"></i>
  62 + <span id="name">{{ escape(name) }}</span>
  63 + (<span id="number">{{ escape(uid) }}</span>)
  64 + <span class="caret"></span>
  65 + </a>
  66 + <div class="dropdown-menu" aria-labelledby="navbarDropdown">
  67 + <!-- <div class="dropdown-divider"></div> -->
  68 + <a class="dropdown-item" href="#">Sair</a>
  69 + </div>
  70 + </li>
  71 + </ul>
  72 + </div>
66 73 </nav>
67 74  
68 75 <!-- ===================================================================== -->
... ... @@ -71,18 +78,18 @@
71 78 </div>
72 79 <!-- ===================================================================== -->
73 80 <!-- main panel with questions -->
74   -<div class="container">
  81 +<div class="container" id="container">
75 82  
76 83 <div id="notifications"></div>
77 84  
78 85 <form action="/question" method="post" id="question_form" autocomplete="off">
79 86 {% module xsrf_form_html() %}
80 87  
81   - <div id="question_div">Question goes here!!!!</div>
  88 + <div id="question_div"></div>
82 89  
83 90 </form>
84 91 <button class="btn btn-primary" id="submit" data-toggle="tooltip" data-placement="right" title="Shift-Enter">Continuar</button>
85   -</div>
  92 +</div>
86 93  
87 94 <!-- ===================================================================== -->
88 95 <!-- JAVASCRIPT -->
... ... @@ -93,6 +100,15 @@
93 100 <script src="/static/bootstrap/js/bootstrap.min.js"></script>
94 101  
95 102 <script>
  103 + $.fn.extend({
  104 + animateCSS: function (animation) {
  105 + var animationEnd = 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend';
  106 + this.addClass('animated ' + animation).one(animationEnd, function() {
  107 + $(this).removeClass('animated ' + animation);
  108 + });
  109 + }
  110 + });
  111 +
96 112 // Process the response given by the server
97 113 function updateQuestion(response){
98 114 switch (response["method"]) {
... ... @@ -137,13 +153,13 @@
137 153  
138 154 case "finished_topic":
139 155 $('#topic_progress').css('width', '100%').attr('aria-valuenow', 100);
140   - $("#question_div").html('<img src="/static/trophy.png" alt="trophy" class="img-rounded img-responsive center-block">'); // FIXME size
  156 + $("#container").html('<img src="/static/trophy.png" alt="trophy" class="img-fluid mx-auto my-5 d-block" width="25%">');
  157 + $("#container").animateCSS('tada');
  158 + setTimeout(function(){ window.location.replace('/'); }, 2000);
141 159 break;
142 160 }
143 161 }
144 162  
145   -
146   -
147 163 // Get current question
148 164 function getQuestion() {
149 165 $.ajax({
... ... @@ -156,14 +172,23 @@
156 172 });
157 173 }
158 174  
159   -
  175 + // Send answer and receive a response.
  176 + // The response can be a new_question or a shake if the answer is wrong.
  177 + function postQuestion() {
  178 + $.ajax({
  179 + type: "POST",
  180 + url: "/question",
  181 + // headers: {"X-XSRFToken": token},
  182 + data: $("#question_form").serialize(), // {'a':10,'b':20},
  183 + dataType: "json", // expected from server
  184 + success: updateQuestion,
  185 + error: function() {alert("O servidor não responde.");}
  186 + });
  187 + }
160 188  
161 189 $(document).ready(function() {
162 190 getQuestion();
163   - // $("#submit").click(postQuestion);
164   - // $('[data-toggle=offcanvas]').click(function() {
165   - // $('.row-offcanvas').toggleClass('active');
166   - // });
  191 + $("#submit").click(postQuestion);
167 192 // $("#change_password").click(change_password);
168 193 });
169 194 </script>
... ...
templates/topics.html
... ... @@ -1,40 +0,0 @@
1   -{% autoescape %}
2   -
3   -<h3>Tópicos</h3>
4   -
5   -<ul class="nav nav-pills nav-stacked">
6   - {% for t in state %}
7   -
8   - {% if t[0] == current_topic %}
9   - <li class="active"> <!-- class="active" class="disabled" -->
10   -
11   - <a href="#">
12   - {{ gettopicname(t[0]) }}
13   - </a>
14   -
15   - {% elif t[1] is None %}
16   -
17   - <li class="disabled">
18   -
19   - <a href="#">
20   - {{ gettopicname(t[0]) }} <br>
21   - <i class="fa fa-lock" aria-hidden="true"></i>
22   - </a>
23   -
24   - {% else %}
25   - <li>
26   -
27   - <a href="/?topic={{ t[0] }}">
28   - {{ gettopicname(t[0]) }} <br>
29   - {% if t[1] < 0.01 %}
30   - <i class="fa fa-unlock" aria-hidden="true"></i>
31   -
32   - {% else %}
33   - {{ round(t[1]*5)*'<i class="fa fa-star text-success" aria-hidden="true"></i>' + round(5-t[1]*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }}
34   - {% end %}
35   - </a>
36   -
37   - {% end %}
38   - </li>
39   - {% end %}
40   -</ul>