Allow for only one item in a bucket in questions of type "przyporzadkuj"
[edumed.git] / wtem / static / wtem / edumed.coffee
1
2 $ = jQuery
3
4 class Binding
5   constructor: (@handler, @element) ->
6     $(@element).data(@handler, this)
7
8
9 class EduModule extends Binding
10   constructor: (element) ->
11     super 'edumodule', element
12
13     # $("[name=teacher-toggle]").change (ev) =>
14     #   if $(ev.target).is(":checked")
15     #     $(".teacher", @element).addClass "show"
16     #   else
17     #     $(".teacher", @element).removeClass "show"
18
19
20 class Exercise extends Binding
21   constructor: (element) ->
22     super 'exercise', element
23     # just save the html to reset the exercise
24     $(@element).data("exercise-html", $(@element).html())
25
26     $(".check", @element).click (ev) =>
27       @check()
28       $(".retry", @element).show()
29       $(".check", @element).hide()
30     $(".retry", @element).click (ev) =>
31       @retry()
32     $('.solutions', @element).click =>
33       @show_solutions()
34       $(".comment", @element).show()
35     $('.reset', @element).click =>
36       @reset()
37
38   retry: ->
39     $(".correct, .incorrect", @element).removeClass("correct incorrect")
40     $(".check", @element).show()
41     $(".retry", @element).hide()
42
43   reset: ->
44     $(@element).html($(@element).data('exercise-html'))
45     exercise @element
46
47   piece_correct: (qpiece) ->
48     $(qpiece).removeClass('incorrect').addClass('correct')
49
50   piece_incorrect: (qpiece) ->
51     $(qpiece).removeClass('correct').addClass('incorrect')
52
53   check: ->
54     scores = []
55     $(".question", @element).each (i, question) =>
56       scores.push(@check_question question)
57
58     score = [0, 0, 0]
59     $.each scores, (i, s) ->
60       score[0] += s[0]
61       score[1] += s[1]
62       score[2] += s[2]
63     @show_score(score)
64
65   show_solutions: ->
66     @reset()
67     $(".question", @element).each (i, question) =>
68       @solve_question question
69
70   get_answers: ->
71     answers = []
72     $('.question', @element).each (i, question) =>
73       answers.push(@get_answer question)
74     return answers
75
76   # Parses a list of values, separated by space or comma.
77   # The list is read from data attribute of elem using data_key
78   # Returns a list with elements
79   # eg.: things_i_need: "house bike tv playstation"
80   # yields ["house", "bike", "tv", "playstation"]
81   # If optional numbers argument is true, returns list of numbers
82   # instead of strings
83   get_value_list: (elem, data_key, numbers) ->
84     vl = $(elem).attr("data-" + data_key).split(/[ ,]+/).map($.trim) #.map((x) -> parseInt(x))
85     if numbers
86       vl = vl.map((x) -> parseInt(x))
87     return vl
88
89   # Parses a list of values, separated by space or comma.
90   # The list is read from data attribute of elem using data_key
91   # Returns a 2-element list with mandatory and optional
92   # items. optional items are marked with a question mark on the end
93   # eg.: things_i_need: "house bike tv? playstation?"
94   # yields [[ "house", "bike"], ["tv", "playstation"]]
95   get_value_optional_list: (elem, data_key) ->
96     vals = @get_value_list(elem, data_key)
97     mandat = []
98     opt = []
99     for v in vals
100       if v.slice(-1) == "?"
101         opt.push v.slice(0, -1)
102       else
103         mandat.push v
104     return [mandat, opt]
105
106   show_score: (score) ->
107     $msg = $(".message", @element)
108     $msg.text("Wynik: #{score[0]} / #{score[2]}")
109     if score[0] >= score[2] and score[1] == 0
110       $msg.addClass("maxscore")
111     else
112       $msg.removeClass("maxscore")
113
114
115   draggable_equal: ($draggable1, $draggable2) ->
116     return false
117
118   draggable_accept: ($draggable, $droppable) ->
119     dropped = $droppable.closest("ul, ol").find(".draggable")
120     for d in dropped
121       if @draggable_equal $draggable, $(d)
122         return false
123     return true
124
125   draggable_move: ($draggable, $placeholder, ismultiple) ->
126     $added = $draggable.clone()
127     $added.data("original", $draggable.get(0))
128     if not ismultiple
129       $draggable.addClass('disabled').draggable('disable')
130
131     $placeholder.after($added)
132     if not $placeholder.hasClass('multiple')
133       $placeholder.hide()
134     if $added.is(".add-li")
135       $added.wrap("<li/>")
136
137     $added.append('<span class="remove">x</span><div class="clr"></div>')
138     $('.remove', $added).click (ev) =>
139       @retry()
140       if not ismultiple
141         $($added.data('original')).removeClass('disabled').draggable('enable')
142
143       if $added.is(".add-li")
144         $added = $added.closest('li')
145       $added.prev(".placeholder:not(.multiple)").show()
146       $added.remove()
147
148
149 ## XXX co z issortable?
150   dragging: (ismultiple, issortable) ->
151     $(".question", @element).each (i, question) =>
152       draggable_opts =
153         revert: 'invalid'
154         helper: 'clone'
155         start: @retry
156
157       $(".draggable", question).draggable(draggable_opts)
158       self = this
159       $(".placeholder", question).droppable
160         accept: (draggable) ->
161           $draggable = $(draggable)
162           is_accepted = true
163
164           if not $draggable.is(".draggable")
165             is_accepted = false
166
167           if is_accepted
168             is_accepted= self.draggable_accept $draggable, $(this)
169
170           if is_accepted
171             $(this).addClass 'accepting'
172           else
173             $(this).removeClass 'accepting'
174           return is_accepted
175
176         drop: (ev, ui) =>
177           $(ev.target).removeClass 'accepting dragover'
178
179           @draggable_move $(ui.draggable), $(ev.target), ismultiple
180
181           # $added = $(ui.draggable).clone()
182           # $added.data("original", ui.draggable)
183           # if not ismultiple
184           #   $(ui.draggable).addClass('disabled').draggable('disable')
185
186           # $(ev.target).after(added)
187           # if not $(ev.target).hasClass('multiple')
188           #   $(ev.target).hide()
189           # $added.append('<span class="remove">x</span>')
190           # $('.remove', added).click (ev) =>
191           #   $added.prev(".placeholder:not(.multiple)").show()
192           #   if not ismultiple
193           #     $added.data('original').removeClass('disabled').draggable('enable')
194           #   $(added).remove()
195
196         over: (ev, ui) ->
197           $(ev.target).addClass 'dragover'
198
199
200         out: (ev, ui) ->
201           $(ev.target).removeClass 'dragover'
202
203
204
205 class Wybor extends Exercise
206   constructor: (element) ->
207     super element
208     $(".question-piece input", element).change(@retry);
209
210
211   check_question: (question) ->
212     all = 0
213     good = 0
214     bad = 0
215     solution = @get_value_list(question, 'solution')
216     $(".question-piece", question).each (i, qpiece) =>
217       piece_no = $(qpiece).attr 'data-no'
218       piece_name = $(qpiece).attr 'data-name'
219       if piece_name
220         should_be_checked = solution.indexOf(piece_name) >= 0
221       else
222         should_be_checked = solution.indexOf(piece_no) >= 0
223       is_checked = $("input", qpiece).is(":checked")
224
225       if should_be_checked
226         all += 1
227
228       if is_checked
229         if should_be_checked
230           good += 1
231           @piece_correct qpiece
232         else
233           bad += 1
234           @piece_incorrect qpiece
235       else
236         $(qpiece).removeClass("correct,incorrect")
237
238     return [good, bad, all]
239
240   solve_question: (question) ->
241     solution = @get_value_list(question, 'solution')
242     $(".question-piece", question).each (i, qpiece) =>
243       piece_no = $(qpiece).attr 'data-no'
244       piece_name = $(qpiece).attr 'data-name'
245       if piece_name
246         should_be_checked = solution.indexOf(piece_name) >= 0
247       else
248         should_be_checked = solution.indexOf(piece_no) >= 0
249       console.log("check " + $("input[type=checkbox]", qpiece).attr("id") + " -> " + should_be_checked)
250       $("input[type=checkbox],input[type=radio]", qpiece).prop 'checked', should_be_checked
251
252   get_answer: (question) ->
253     answer = []
254     $('.question-piece', question).each (i, qpiece) =>
255       $qpiece = $(qpiece)
256       if $("input[type=checkbox],input[type=radio]", qpiece).is(':checked')
257         answer.push($qpiece.attr('data-name'))
258     return answer
259
260
261 class Uporzadkuj extends Exercise
262   constructor: (element) ->
263     super element
264     $('ol, ul', @element).sortable({ items: "> li", start: @retry })
265
266   check_question: (question) ->
267     positions = @get_value_list(question, 'original', true)
268     sorted = positions.sort()
269     pkts = $('.question-piece', question)
270
271     correct = 0
272     bad = 0
273     all = 0
274
275     for pkt in [0...pkts.length]
276       all += 1
277       if pkts.eq(pkt).data('pos') == sorted[pkt]
278         correct += 1
279         @piece_correct pkts.eq(pkt)
280       else
281         bad += 1
282         @piece_incorrect pkts.eq(pkt)
283     return [correct, bad, all]
284
285   solve_question: (question) ->
286     positions = @get_value_list(question, 'original', true)
287     sorted = positions.sort()
288     pkts = $('.question-piece', question)
289     pkts.sort (a, b) ->
290       q = $(a).data('pos')
291       w = $(b).data('pos')
292       return 1 if q < w
293       return -1 if q > w
294       return 0
295
296     parent = pkts.eq(0).parent()
297     for p in pkts
298       parent.prepend(p)
299
300   get_answer: (question) ->
301     answer = []
302     $(".question-piece", @element).each (i, qpiece) =>
303       answer.push($(qpiece).attr('data-pos'))
304     return answer
305
306
307 # XXX propozycje="1/0"
308 class Luki extends Exercise
309   constructor: (element) ->
310     super element
311     @dragging false, false
312
313   check: ->
314     all = $(".placeholder", @element).length
315     correct = 0
316     bad = 0
317     $(".placeholder + .question-piece", @element).each (i, qpiece) =>
318       $placeholder = $(qpiece).prev(".placeholder")
319       if $placeholder.data('solution') == $(qpiece).data('no')
320         @piece_correct qpiece
321         correct += 1
322       else
323         bad += 1
324         @piece_incorrect qpiece
325
326     @show_score [correct, bad, all]
327
328   solve_question: (question) ->
329     $(".placeholder", question).each (i, placeholder) =>
330       $qp = $(".question-piece[data-no=" + $(placeholder).data('solution') + "]", question)
331       @draggable_move $qp, $(placeholder), false
332
333
334 class Zastap extends Exercise
335   constructor: (element) ->
336     super element
337     $(".paragraph", @element).each (i, par) =>
338       @wrap_words $(par), $('<span class="placeholder zastap"/>')
339     @dragging false, false
340
341   check: ->
342     all = 0
343     correct = 0
344     bad = 0
345
346     $(".paragraph", @element).each (i, par) =>
347       $(".placeholder", par).each (j, qpiece) =>
348         $qp = $(qpiece)
349         $dragged = $qp.next(".draggable")
350         if $qp.data("solution")
351           if $dragged and $qp.data("solution") == $dragged.data("no")
352             @piece_correct $dragged
353             correct += 1
354 #          else -- we dont mark enything here, so not to hint user about solution. He sees he hasn't used all the draggables
355
356           all += 1
357
358     @show_score [correct, bad, all]
359
360   show_solutions: ->
361     @reset()
362     $(".paragraph", @element).each (i, par) =>
363       $(".placeholder[data-solution]", par).each (j, qpiece) =>
364         $qp = $(qpiece)
365         $dr = $(".draggable[data-no=" + $qp.data('solution') + "]", @element)
366         @draggable_move $dr, $qp, false
367
368
369   wrap_words: (element, wrapper) ->
370     # This function wraps each word of element in wrapper, but does not descend into child-tags of element.
371     # It doesn't wrap things between words (defined by ignore RE below). Warning - ignore must begin with ^
372     ignore = /^[ \t.,:;()]+/
373
374     insertWrapped = (txt, elem) ->
375       nw = wrapper.clone()
376       $(document.createTextNode(txt))
377         .wrap(nw).parent().attr("data-original", txt).insertBefore(elem)
378
379     for j in [element.get(0).childNodes.length-1..0]
380       chld = element.get(0).childNodes[j]
381       if chld.nodeType == document.TEXT_NODE
382         len = chld.textContent.length
383         wordb = 0
384         i = 0
385         while i < len
386           space = ignore.exec(chld.textContent.substr(i))
387           if space?
388             if wordb < i
389               insertWrapped(chld.textContent.substr(wordb, i-wordb), chld)
390
391             $(document.createTextNode(space[0])).insertBefore(chld)
392             i += space[0].length
393             wordb = i
394           else
395             i = i + 1
396         if wordb < len - 1
397           insertWrapped(chld.textContent.substr(wordb, len - 1 - wordb), chld)
398         $(chld).remove()
399
400
401 class Przyporzadkuj extends Exercise
402   is_multiple: ->
403     for qp in $(".question-piece", @element)
404       if $(qp).attr('data-solution').split(/[ ,]+/).length > 1
405         return true
406     return false
407
408   constructor: (element) ->
409     super element
410
411     @multiple = @is_multiple()
412
413     @dragging @multiple, true
414
415   draggable_equal: (d1, d2) ->
416     return d1.data("no") == d2.data("no")
417
418   draggable_accept: ($draggable, $droppable) ->
419     dropped = $droppable.closest("ul, ol").find(".draggable")
420     return (super $draggable, $droppable) && dropped.length == 0
421
422   check_question: (question) ->
423     # subjects placed in predicates
424     minimum = $(question).data("minimum")
425     count = 0
426     bad_count = 0
427     all = 0
428     if not minimum
429       self = this
430       $(".subject .question-piece", question).each (i, el) ->
431         v = self.get_value_optional_list el, 'solution'
432         mandatory = v[0]
433         all += mandatory.length
434
435     for pred in $(".predicate [data-predicate]", question)
436       pn = $(pred).attr('data-predicate')
437       #if minimum?
438       #  all += minimum
439
440       for qp in $(".question-piece", pred)
441         v = @get_value_optional_list qp, 'solution'
442         mandatory = v[0]
443         optional = v[1]
444
445         if mandatory.indexOf(pn) >= 0 or (minimum and optional.indexOf(pn) >= 0)
446           count += 1
447           @piece_correct qp
448         else
449           bad_count += 1
450           @piece_incorrect qp
451
452     return [count, bad_count, all]
453
454   solve_question: (question) ->
455     minimum = $(question).data("min")
456
457     for qp in $(".subject .question-piece", question)
458       v = @get_value_optional_list qp, 'solution'
459       mandatory = v[0]
460       optional = v[1]
461
462       if minimum
463         draggables = mandatory.count(optional)[0...minimum]
464       else
465         draggables = mandatory
466       for m in draggables
467         $pr = $(".predicate [data-predicate=" + m + "]", question)
468         $ph = $pr.find ".placeholder:visible"
469         @draggable_move $(qp), $ph.eq(0), @multiple
470
471   get_answer: (question) ->
472     answer = {}
473     $(".predicate [data-predicate]", question).each (i, subjects) =>
474       predicate = $(subjects).attr('data-predicate')
475       answer[predicate] = []
476       $('.question-piece', subjects).each (i, qpiece) =>
477         $qpiece = $(qpiece)
478         answer[predicate].push($qpiece.attr('data-id'))
479     return answer
480
481
482 class PrawdaFalsz extends Exercise
483   constructor: (element) ->
484     super element
485
486     for qp in $(".question-piece", @element)
487       $(".true", qp).click (ev) =>
488         ev.preventDefault()
489         @retry()
490         $(ev.target).closest(".question-piece").data("value", "true")
491         $(ev.target).addClass('chosen').siblings('a').removeClass('chosen')
492       $(".false", qp).click (ev) =>
493         ev.preventDefault()
494         @retry()
495         $(ev.target).closest(".question-piece").data("value", "false")
496         $(ev.target).addClass('chosen').siblings('a').removeClass('chosen')
497
498
499   check_question: ->
500     all = 0
501     good = 0
502     bad = 0
503     for qp in $(".question-piece", @element)
504       if $(qp).data("solution").toString() == $(qp).data("value")
505         good += 1
506         @piece_correct qp
507       else
508         bad += 1
509         @piece_incorrect qp
510
511       all += 1
512
513     return [good, bad, all]
514
515   show_solutions: ->
516     @reset()
517     for qp in $(".question-piece", @element)
518       if $(qp).data('solution') == true
519         $(".true", qp).click()
520       else
521         $(".false", qp).click()
522
523   get_answer: (question) ->
524     answer = []
525     $(".question-piece", @element).each (i, qpiece) =>
526       answer.push($(qpiece).data('value') || '-')
527     return answer
528
529 ##########
530
531 exercise = (ele) ->
532   es =
533     wybor: Wybor
534     uporzadkuj: Uporzadkuj
535     luki: Luki
536     zastap: Zastap
537     przyporzadkuj: Przyporzadkuj
538     prawdafalsz: PrawdaFalsz
539
540
541   cls = es[$(ele).attr('data-type')]
542   new cls(ele)
543
544
545 window.edumed =
546   'EduModule': EduModule
547
548
549
550
551 $(document).ready () ->
552   new EduModule($("#book-text"))
553
554   $(".exercise").each (i, el) ->
555     exercise(this)