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.
@@ -20,7 +20,6 @@ class StudentKnowledge(object): @@ -20,7 +20,6 @@ class StudentKnowledge(object):
20 # ======================================================================= 20 # =======================================================================
21 # methods that update state 21 # methods that update state
22 # ======================================================================= 22 # =======================================================================
23 -  
24 def __init__(self, deps, state={}): 23 def __init__(self, deps, state={}):
25 # graph with topic dependencies shared between all students 24 # graph with topic dependencies shared between all students
26 self.deps = deps 25 self.deps = deps
@@ -38,12 +37,9 @@ class StudentKnowledge(object): @@ -38,12 +37,9 @@ class StudentKnowledge(object):
38 # compute recommended sequence of topics ['a', 'b', ...] 37 # compute recommended sequence of topics ['a', 'b', ...]
39 self.topic_sequence = list(nx.topological_sort(self.deps)) 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 def unlock_topics(self): 44 def unlock_topics(self):
49 min_level = 0.01 # minimum level to unlock 45 min_level = 0.01 # minimum level to unlock
@@ -60,9 +56,10 @@ class StudentKnowledge(object): @@ -60,9 +56,10 @@ class StudentKnowledge(object):
60 56
61 # ------------------------------------------------------------------------ 57 # ------------------------------------------------------------------------
62 # Recommends a topic to practice/learn from the state. 58 # Recommends a topic to practice/learn from the state.
  59 + # FIXME untested
63 # ------------------------------------------------------------------------ 60 # ------------------------------------------------------------------------
64 def recommended_topic(self): 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 # if not topic: 65 # if not topic:
@@ -81,14 +78,15 @@ class StudentKnowledge(object): @@ -81,14 +78,15 @@ class StudentKnowledge(object):
81 # logger.debug(f'Can\'t start topic "{topic}".') 78 # logger.debug(f'Can\'t start topic "{topic}".')
82 # return 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 if not topic: 91 if not topic:
94 topic = self.recommended_topic() 92 topic = self.recommended_topic()
@@ -97,26 +95,30 @@ class StudentKnowledge(object): @@ -97,26 +95,30 @@ class StudentKnowledge(object):
97 self.current_topic = topic 95 self.current_topic = topic
98 logger.info(f'Topic set to "{topic}"') 96 logger.info(f'Topic set to "{topic}"')
99 97
100 -  
101 # generate question instances for current topic 98 # generate question instances for current topic
102 factory = self.deps.node[topic]['factory'] 99 factory = self.deps.node[topic]['factory']
103 questionlist = self.deps.node[topic]['questions'] 100 questionlist = self.deps.node[topic]['questions']
104 101
105 self.questions = [factory[qref].generate() for qref in questionlist] 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 self.finished_questions = [] 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 # returns the current question with correction, time and comments updated 109 # returns the current question with correction, time and comments updated
112 # ------------------------------------------------------------------------ 110 # ------------------------------------------------------------------------
113 def check_answer(self, answer): 111 def check_answer(self, answer):
  112 + logger.debug('StudentKnowledge.check_answer()')
  113 +
114 q = self.current_question 114 q = self.current_question
115 q['finish_time'] = datetime.now() 115 q['finish_time'] = datetime.now()
116 - grade = q.correct(answer) 116 + q['answer'] = answer
  117 + grade = q.correct()
  118 +
117 logger.debug(f'Grade = {grade:.2} ({q["ref"]})') 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 if grade > 0.999: 122 if grade > 0.999:
121 self.finished_questions.append(q) 123 self.finished_questions.append(q)
122 try: 124 try:
@@ -127,13 +129,15 @@ class StudentKnowledge(object): @@ -127,13 +129,15 @@ class StudentKnowledge(object):
127 'level': 1.0, 129 'level': 1.0,
128 'date': datetime.now() 130 'date': datetime.now()
129 } 131 }
130 - self.start_topic()  
131 else: 132 else:
132 self.current_question['start_time'] = datetime.now() 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 else: 136 else:
134 factory = self.deps.node[self.current_topic]['factory'] 137 factory = self.deps.node[self.current_topic]['factory']
135 self.questions.append(factory[q['ref']].generate()) 138 self.questions.append(factory[q['ref']].generate())
136 139
  140 + # returns answered and corrected question
137 return q 141 return q
138 142
139 143
@@ -60,8 +60,8 @@ class LearnApp(object): @@ -60,8 +60,8 @@ class LearnApp(object):
60 # success 60 # success
61 logger.info(f'User "{uid}" logged in') 61 logger.info(f'User "{uid}" logged in')
62 62
63 - state = {}  
64 tt = s.query(StudentTopic).filter(StudentTopic.student_id == uid) 63 tt = s.query(StudentTopic).filter(StudentTopic.student_id == uid)
  64 + state = {}
65 for t in tt: 65 for t in tt:
66 state[t.topic_id] = { 66 state[t.topic_id] = {
67 'level': t.level, 67 'level': t.level,
@@ -141,7 +141,7 @@ class LearnApp(object): @@ -141,7 +141,7 @@ class LearnApp(object):
141 # Start new topic 141 # Start new topic
142 # ------------------------------------------------------------------------ 142 # ------------------------------------------------------------------------
143 def start_topic(self, uid, topic): 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 # Fill db table 'Topic' with topics from the graph if not already there. 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,13 +276,13 @@ def build_dependency_graph(config_file):
276 fullpath = path.expanduser(path.join(prefix, tref)) 276 fullpath = path.expanduser(path.join(prefix, tref))
277 filename = path.join(fullpath, 'questions.yaml') 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 if not tnode['questions']: 282 if not tnode['questions']:
283 tnode['questions'] = [q['ref'] for q in loaded_questions] 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 tnode['factory'] = {} 286 tnode['factory'] = {}
287 for q in loaded_questions: 287 for q in loaded_questions:
288 if q['ref'] in tnode['questions']: 288 if q['ref'] in tnode['questions']:
@@ -11,9 +11,11 @@ import yaml @@ -11,9 +11,11 @@ import yaml
11 # this project 11 # this project
12 from tools import run_script 12 from tools import run_script
13 13
  14 +
14 # regular expressions in yaml files, e.g. correct: !regex '[aA]zul' 15 # regular expressions in yaml files, e.g. correct: !regex '[aA]zul'
15 yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n))) 16 yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n)))
16 17
  18 +
17 # setup logger for this module 19 # setup logger for this module
18 logger = logging.getLogger(__name__) 20 logger = logging.getLogger(__name__)
19 21
@@ -39,8 +41,9 @@ class Question(dict): @@ -39,8 +41,9 @@ class Question(dict):
39 'files': {}, 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 def correct(self): 48 def correct(self):
46 self['grade'] = 0.0 49 self['grade'] = 0.0
@@ -129,9 +129,9 @@ class TopicHandler(BaseHandler): @@ -129,9 +129,9 @@ class TopicHandler(BaseHandler):
129 @tornado.web.authenticated 129 @tornado.web.authenticated
130 def get(self, topic): 130 def get(self, topic):
131 uid = self.current_user 131 uid = self.current_user
132 - # topic = self.get_query_argument('topic', default='')  
133 132
134 self.learn.start_topic(uid, topic) 133 self.learn.start_topic(uid, topic)
  134 +
135 self.render('topic.html', 135 self.render('topic.html',
136 uid=uid, 136 uid=uid,
137 name=self.learn.get_student_name(uid), 137 name=self.learn.get_student_name(uid),
@@ -159,12 +159,12 @@ class FileHandler(BaseHandler): @@ -159,12 +159,12 @@ class FileHandler(BaseHandler):
159 # ---------------------------------------------------------------------------- 159 # ----------------------------------------------------------------------------
160 class QuestionHandler(BaseHandler): 160 class QuestionHandler(BaseHandler):
161 templates = { 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 'numeric-interval': 'question-text.html', 166 'numeric-interval': 'question-text.html',
167 - 'textarea': 'question-textarea.html', 167 + 'textarea': 'question-textarea.html',
168 # -- information panels -- 168 # -- information panels --
169 'information': 'question-information.html', 169 'information': 'question-information.html',
170 'info': 'question-information.html', 170 'info': 'question-information.html',
@@ -175,24 +175,23 @@ class QuestionHandler(BaseHandler): @@ -175,24 +175,23 @@ class QuestionHandler(BaseHandler):
175 } 175 }
176 176
177 def new_question(self, user): 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 return { 186 return {
187 'method': 'new_question', 187 'method': 'new_question',
188 'params': { 188 'params': {
189 'question': tornado.escape.to_unicode(question_html), 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 progress = self.learn.get_student_progress(user) # in the current topic 195 progress = self.learn.get_student_progress(user) # in the current topic
197 return { 196 return {
198 'method': 'shake', 197 'method': 'shake',
@@ -202,68 +201,54 @@ class QuestionHandler(BaseHandler): @@ -202,68 +201,54 @@ class QuestionHandler(BaseHandler):
202 } 201 }
203 202
204 def finished_topic(self, user): 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 return { 207 return {
214 'method': 'finished_topic', 208 'method': 'finished_topic',
215 'params': { 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 @tornado.web.authenticated 215 @tornado.web.authenticated
221 def get(self): 216 def get(self):
222 logging.debug('QuestionHandler.get') 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 template = self.templates[question['type']] 221 template = self.templates[question['type']]
229 question_html = self.render_string(template, question=question, md=md) 222 question_html = self.render_string(template, question=question, md=md)
230 - print(question_html)  
231 223
232 self.write({ 224 self.write({
233 'method': 'new_question', 225 'method': 'new_question',
234 'params': { 226 'params': {
235 'question': tornado.escape.to_unicode(question_html), 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 # handles answer posted 232 # handles answer posted
252 @tornado.web.authenticated 233 @tornado.web.authenticated
253 def post(self): 234 def post(self):
254 logging.debug('QuestionHandler.post') 235 logging.debug('QuestionHandler.post')
255 -  
256 user = self.current_user 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 grade = self.learn.check_answer(user, answer) 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 if question is None: 246 if question is None:
262 self.write(self.finished_topic(user)) 247 self.write(self.finished_topic(user))
263 elif grade > 0.999: 248 elif grade > 0.999:
264 self.write(self.new_question(user)) 249 self.write(self.new_question(user))
265 else: 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,4 +149,3 @@
149 </script> 149 </script>
150 </body> 150 </body>
151 </html> 151 </html>
152 -  
templates/topic.html
@@ -52,17 +52,24 @@ @@ -52,17 +52,24 @@
52 <span class="navbar-toggler-icon"></span> 52 <span class="navbar-toggler-icon"></span>
53 </button> 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 </nav> 73 </nav>
67 74
68 <!-- ===================================================================== --> 75 <!-- ===================================================================== -->
@@ -71,18 +78,18 @@ @@ -71,18 +78,18 @@
71 </div> 78 </div>
72 <!-- ===================================================================== --> 79 <!-- ===================================================================== -->
73 <!-- main panel with questions --> 80 <!-- main panel with questions -->
74 -<div class="container"> 81 +<div class="container" id="container">
75 82
76 <div id="notifications"></div> 83 <div id="notifications"></div>
77 84
78 <form action="/question" method="post" id="question_form" autocomplete="off"> 85 <form action="/question" method="post" id="question_form" autocomplete="off">
79 {% module xsrf_form_html() %} 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 </form> 90 </form>
84 <button class="btn btn-primary" id="submit" data-toggle="tooltip" data-placement="right" title="Shift-Enter">Continuar</button> 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 <!-- JAVASCRIPT --> 95 <!-- JAVASCRIPT -->
@@ -93,6 +100,15 @@ @@ -93,6 +100,15 @@
93 <script src="/static/bootstrap/js/bootstrap.min.js"></script> 100 <script src="/static/bootstrap/js/bootstrap.min.js"></script>
94 101
95 <script> 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 // Process the response given by the server 112 // Process the response given by the server
97 function updateQuestion(response){ 113 function updateQuestion(response){
98 switch (response["method"]) { 114 switch (response["method"]) {
@@ -137,13 +153,13 @@ @@ -137,13 +153,13 @@
137 153
138 case "finished_topic": 154 case "finished_topic":
139 $('#topic_progress').css('width', '100%').attr('aria-valuenow', 100); 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 break; 159 break;
142 } 160 }
143 } 161 }
144 162
145 -  
146 -  
147 // Get current question 163 // Get current question
148 function getQuestion() { 164 function getQuestion() {
149 $.ajax({ 165 $.ajax({
@@ -156,14 +172,23 @@ @@ -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 $(document).ready(function() { 189 $(document).ready(function() {
162 getQuestion(); 190 getQuestion();
163 - // $("#submit").click(postQuestion);  
164 - // $('[data-toggle=offcanvas]').click(function() {  
165 - // $('.row-offcanvas').toggleClass('active');  
166 - // }); 191 + $("#submit").click(postQuestion);
167 // $("#change_password").click(change_password); 192 // $("#change_password").click(change_password);
168 }); 193 });
169 </script> 194 </script>
templates/topics.html
@@ -1,40 +0,0 @@ @@ -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>