excercise counters
[librarian.git] / librarian / pyhtml.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
5 #
6 from lxml import etree
7 from librarian import IOFile, RDFNS, DCNS, Format
8 from xmlutils import Xmill, tag, tagged, ifoption, tag_open_close
9 from librarian import functions
10 import re
11 import random
12
13
14 class EduModule(Xmill):
15     def __init__(self, options=None):
16         super(EduModule, self).__init__(options)
17         self.activity_counter = 0
18         self.exercise_counter = 0
19
20         # text filters
21         def swap_endlines(txt):
22             if self.options['strofa']:
23                 txt = txt.replace("/\n", "<br/>\n")
24             return txt
25         self.register_text_filter(functions.substitute_entities)
26         self.register_text_filter(swap_endlines)
27
28     @tagged('div', 'stanza')
29     def handle_strofa(self, element):
30         self.options = {'strofa': True}
31         return "", ""
32
33     def handle_powiesc(self, element):
34         return u"""
35 <div class="module" id="book-text">
36 <!-- <span class="teacher-toggle">
37   <input type="checkbox" name="teacher-toggle" id="teacher-toggle"/>
38   <label for="teacher-toggle">Pokaż treść dla nauczyciela</label>
39  </span>-->
40
41 """, u"</div>"
42
43     handle_autor_utworu = tag("span", "author")
44     handle_dzielo_nadrzedne = tag("span", "collection")
45     handle_podtytul = tag("span", "subtitle")
46     handle_naglowek_akt = handle_naglowek_czesc = handle_srodtytul = tag("h2")
47     handle_naglowek_scena = tag('h2')
48     handle_naglowek_osoba = handle_naglowek_podrozdzial = tag('h3')
49     handle_akap = handle_akap_dialog = handle_akap_cd = tag('p', 'paragraph')
50
51     handle_wyroznienie = tag('em')
52     handle_tytul_dziela = tag('em', 'title')
53     handle_slowo_obce = tag('em', 'foreign')
54
55     def handle_nazwa_utworu(self, element):
56         toc = []
57         for naglowek in element.getparent().findall('.//naglowek_rozdzial'):
58             a = etree.Element("a")
59             a.attrib["href"] = "#" + naglowek.text
60             a.text = naglowek.text
61             atxt = etree.tostring(a, encoding=unicode)
62             toc.append("<li>%s</li>" % atxt)
63         toc = "<ul class='toc'>%s</ul>" % "".join(toc)
64         add_header = "Lekcja: " if self.options['wldoc'].book_info.type in ('course', 'synthetic') else ''
65         return "<h1 class='title'>%s" % add_header, "</h1>" + toc
66
67     @tagged("h2")
68     def handle_naglowek_rozdzial(self, element):
69         return "", "".join(tag_open_close("a", name=element.text))
70
71     def handle_uwaga(self, _e):
72         return None
73
74     def handle_aktywnosc(self, element):
75         self.activity_counter += 1
76         self.options = {
77             'activity': True,
78             'activity_counter': self.activity_counter,
79             }
80         submill = EduModule(dict(self.options.items() + {'sub_gen': True}.items()))
81
82         opis = submill.generate(element.xpath('opis')[0])
83
84         n = element.xpath('wskazowki')
85         if n: wskazowki = submill.generate(n[0])
86
87         else: wskazowki = ''
88         n = element.xpath('pomoce')
89
90         if n: pomoce = submill.generate(n[0])
91         else: pomoce = ''
92
93         forma = ''.join(element.xpath('forma/text()'))
94
95         czas = ''.join(element.xpath('czas/text()'))
96
97         counter = self.activity_counter
98
99         return u"""
100 <div class="activity">
101  <div class="text">
102   <span class="act_counter">%(counter)d.</span>
103   %(opis)s""" % locals(), \
104 u"""%(wskazowki)s
105  </div>
106  <aside class="info">
107   <section class="infobox time"><h1>Czas</h1><p>%(czas)s min</p></section>
108   <section class="infobox kind"><h1>Metoda</h1><p>%(forma)s</p></section>
109   %(pomoce)s
110  </aside>
111  <div class="clearboth"></div>
112 </div>
113 """ % locals()
114
115     handle_opis = ifoption(sub_gen=True)(tag('div', 'description'))
116     handle_wskazowki = ifoption(sub_gen=True)(tag('div', ('hints', 'teacher')))
117
118     @ifoption(sub_gen=True)
119     @tagged('section', 'infobox materials')
120     def handle_pomoce(self, _):
121         return """<h1>Pomoce</h1>""", ""
122
123     def handle_czas(self, *_):
124         return
125
126     def handle_forma(self, *_):
127         return
128
129     def handle_cwiczenie(self, element):
130         exercise_handlers = {
131             'wybor': Wybor,
132             'uporzadkuj': Uporzadkuj,
133             'luki': Luki,
134             'zastap': Zastap,
135             'przyporzadkuj': Przyporzadkuj,
136             'prawdafalsz': PrawdaFalsz
137             }
138
139         typ = element.attrib['typ']
140         self.exercise_counter += 1
141         print self.exercise_counter
142         self.options = {'exercise_counter': self.exercise_counter}
143         handler = exercise_handlers[typ](self.options)
144         return handler.generate(element)
145
146     # Lists
147     def handle_lista(self, element, attrs={}):
148         ltype = element.attrib.get('typ', 'punkt')
149         if ltype == 'slowniczek':
150             surl = element.attrib.get('src', None)
151             if surl is None:
152                 print '** missing src on <slowniczek>, setting default'
153                 surl = 'http://edukacjamedialna.edu.pl/slowniczek'
154             sxml = None
155             if surl:
156                 sxml = etree.fromstring(self.options['provider'].by_uri(surl).get_string())
157             self.options = {'slowniczek': True, 'slowniczek_xml': sxml }
158             return '<div class="slowniczek">', '</div>'
159
160         listtag = {'num': 'ol',
161                'punkt': 'ul',
162                'alfa': 'ul',
163                'czytelnia': 'ul'}[ltype]
164
165         classes = attrs.get('class', '')
166         if classes: del attrs['class']
167
168         attrs_s = ' '.join(['%s="%s"' % kv for kv in attrs.items()])
169         if attrs_s: attrs_s = ' ' + attrs_s
170
171         return '<%s class="lista %s %s"%s>' % (listtag, ltype, classes, attrs_s), '</%s>' % listtag
172
173     def handle_punkt(self, element):
174         if self.options['slowniczek']:
175             return '<dl>', '</dl>'
176         else:
177             return '<li>', '</li>'
178
179     def handle_definiendum(self, element):
180         nxt = element.getnext()
181         definiens_s = ''
182
183         if not element.text:
184             print "!! Empty <definiendum/>"
185             return None
186
187         # let's pull definiens from another document
188         if self.options['slowniczek_xml'] is not None and (nxt is None or nxt.tag != 'definiens'):
189             sxml = self.options['slowniczek_xml']
190             defloc = sxml.xpath("//definiendum[text()='%s']" % element.text)
191             if defloc:
192                 definiens = defloc[0].getnext()
193                 if definiens.tag == 'definiens':
194                     subgen = EduModule(self.options)
195                     definiens_s = subgen.generate(definiens)
196             else:
197                 print '!! Missing definiendum in source:', element.text
198
199         return u"<dt>", u"</dt>" + definiens_s
200
201     def handle_definiens(self, element):
202         return u"<dd>", u"</dd>"
203
204     def handle_podpis(self, element):
205         return u"""<div class="caption">""", u"</div>"
206
207     def handle_tabela(self, element):
208         has_frames = int(element.attrib.get("ramki", "0"))
209         if has_frames: frames_c = "framed"
210         else: frames_c = ""
211         return u"""<table class="%s">""" % frames_c, u"</table>"
212
213     def handle_wiersz(self, element):
214         return u"<tr>", u"</tr>"
215
216     def handle_kol(self, element):
217         return u"<td>", u"</td>"
218
219     def handle_rdf__RDF(self, _):
220         # ustal w opcjach  rzeczy :D
221         return
222
223     def handle_link(self, element):
224         if 'url' in element.attrib:
225             return tag('a', href=element.attrib['url'])(self, element)
226         elif 'material' in element.attrib:
227             material_err = u' [BRAKUJĄCY MATERIAŁ]'
228             make_url = lambda f: self.options['urlmapper'] \
229               .url_for_material(element.attrib['material'], f)
230
231             if 'format' in element.attrib:
232                 formats = re.split(r"[, ]+",
233                                element.attrib['format'])
234             else:
235                 formats = [None]
236
237             try:
238                 def_href = make_url(formats[0])
239                 def_err = u""
240             except self.options['urlmapper'].MaterialNotFound:
241                 def_err = material_err
242                 def_href = u""
243             fmt_links = []
244             for f in formats[1:]:
245                 try:
246                     fmt_links.append(u'<a href="%s">%s</a>' % (make_url(f), f.upper()))
247                 except self.options['urlmapper'].MaterialNotFound:
248                     fmt_links.append(u'<a>%s%s</a>' % (f.upper(), material_err))
249             more_links = u' (%s)' % u', '.join(fmt_links) if fmt_links else u''
250
251             return u"<a href='%s'>" % def_href, u'%s</a>%s' % (def_err, more_links)
252
253
254 class Exercise(EduModule):
255     def __init__(self, *args, **kw):
256         self.question_counter = 0
257         super(Exercise, self).__init__(*args, **kw)
258
259     handle_opis = tag('div', 'description')
260
261     def handle_rozw_kom(self, element):
262         return u"""<div style="display:none" class="comment">""", u"""</div>"""
263
264     def handle_cwiczenie(self, element):
265         self.options = {'exercise': element.attrib['typ']}
266         self.question_counter = 0
267         self.piece_counter = 0
268
269         pre = u"""
270 <div class="exercise %(typ)s" data-type="%(typ)s">
271 <form action="#" method="POST">
272 <h3>Zadanie %(exercies_counter)d</h3>
273 <div class="buttons">
274 <span class="message"></span>
275 <input type="button" class="check" value="sprawdź"/>
276 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
277 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
278 <input type="button" class="reset" value="reset"/>
279 </div>
280
281 """ % {'exercies_counter': self.options['exercise_counter'], 'typ': element.attrib['typ']}
282         post = u"""
283 <div class="buttons">
284 <span class="message"></span>
285 <input type="button" class="check" value="sprawdź"/>
286 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
287 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
288 <input type="button" class="reset" value="reset"/>
289 </div>
290 </form>
291 </div>
292 """
293         # Add a single <pytanie> tag if it's not there
294         if not element.xpath(".//pytanie"):
295             qpre, qpost = self.handle_pytanie(element)
296             pre = pre + qpre
297             post = qpost + post
298         return pre, post
299
300     def handle_pytanie(self, element):
301         """This will handle <cwiczenie> element, when there is no <pytanie>
302         """
303         add_class = ""
304         self.question_counter += 1
305         self.piece_counter = 0
306         solution = element.attrib.get('rozw', None)
307         if solution: solution_s = ' data-solution="%s"' % solution
308         else: solution_s = ''
309
310         handles = element.attrib.get('uchwyty', None)
311         if handles:
312             add_class += ' handles handles-%s' % handles
313             self.options = {'handles': handles}
314
315         minimum = element.attrib.get('min', None)
316         if minimum: minimum_s = ' data-minimum="%d"' % int(minimum)
317         else: minimum_s = ''
318
319         return '<div class="question%s" data-no="%d" %s>' %\
320             (add_class, self.question_counter, solution_s + minimum_s), \
321             "</div>"
322
323
324 class Wybor(Exercise):
325     def handle_cwiczenie(self, element):
326         pre, post = super(Wybor, self).handle_cwiczenie(element)
327         is_single_choice = True
328         pytania = element.xpath(".//pytanie")
329         if not pytania:
330             pytania = [element]
331         for p in pytania:
332             solutions = re.split(r"[, ]+", p.attrib['rozw'])
333             if len(solutions) != 1:
334                 is_single_choice = False
335                 break
336             choices = p.xpath(".//*[@nazwa]")
337             uniq = set()
338             for n in choices: uniq.add(n.attrib['nazwa'])
339             if len(choices) != len(uniq):
340                 is_single_choice = False
341                 break
342
343         self.options = {'single': is_single_choice}
344         return pre, post
345
346     def handle_punkt(self, element):
347         if self.options['exercise'] and element.attrib.get('nazwa', None):
348             qc = self.question_counter
349             self.piece_counter += 1
350             no = self.piece_counter
351             eid = "q%(qc)d_%(no)d" % locals()
352             aname = element.attrib.get('nazwa', None)
353             if self.options['single']:
354                 return u"""
355 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
356 <input type="radio" name="q%(qc)d" id="%(eid)s" value="%(aname)s" />
357 <label for="%(eid)s">
358                 """ % locals(), u"</label></li>"
359             else:
360                 return u"""
361 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
362 <input type="checkbox" name="%(eid)s" id="%(eid)s" />
363 <label for="%(eid)s">
364 """ % locals(), u"</label></li>"
365
366         else:
367             return super(Wybor, self).handle_punkt(element)
368
369
370 class Uporzadkuj(Exercise):
371     def handle_pytanie(self, element):
372         """
373 Overrides the returned content default handle_pytanie
374         """
375         # we ignore the result, returning our own
376         super(Uporzadkuj, self).handle_pytanie(element)
377         order_items = element.xpath(".//punkt/@rozw")
378
379         return u"""<div class="question" data-original="%s" data-no="%s">""" % \
380             (','.join(order_items), self.question_counter), \
381             u"""</div>"""
382
383     def handle_punkt(self, element):
384         return """<li class="question-piece" data-pos="%(rozw)s"/>""" \
385             % element.attrib,\
386             "</li>"
387
388
389 class Luki(Exercise):
390     def find_pieces(self, question):
391         return question.xpath(".//luka")
392
393     def solution_html(self, piece):
394         sub = EduModule()
395         return sub.generate(piece)
396         # print piece.text
397         # return piece.text + ''.join(
398         #     [etree.tostring(n, encoding=unicode)
399         #      for n in piece])
400
401     def handle_pytanie(self, element):
402         qpre, qpost = super(Luki, self).handle_pytanie(element)
403
404         luki = list(enumerate(self.find_pieces(element)))
405         luki_html = ""
406         i = 0
407         random.shuffle(luki)
408         for (i, luka) in luki:
409             i += 1
410             luka_html = self.solution_html(luka)
411             luki_html += u'<span class="draggable question-piece" data-no="%d">%s</span>' % (i, luka_html)
412         self.words_html = '<div class="words">%s</div>' % luki_html
413
414         return qpre, qpost
415
416     def handle_opis(self, element):
417         return '', self.words_html
418
419     def handle_luka(self, element):
420         self.piece_counter += 1
421         return '<span class="placeholder" data-solution="%d"></span>' % self.piece_counter
422
423
424 class Zastap(Luki):
425     def find_pieces(self, question):
426         return question.xpath(".//zastap")
427
428     def solution_html(self, piece):
429         return piece.attrib['rozw']
430
431     def handle_zastap(self, element):
432         self.piece_counter += 1
433         return '<span class="placeholder zastap question-piece" data-solution="%d">' \
434             % self.piece_counter, '</span>'
435
436
437 class Przyporzadkuj(Exercise):
438     def handle_pytanie(self, element):
439         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
440         minimum = element.attrib.get("min", None)
441         if minimum:
442             self.options = {"min": int(minimum)}
443         return pre, post
444
445     def handle_lista(self, lista):
446         if 'nazwa' in lista.attrib:
447             attrs = {
448                 'data-name': lista.attrib['nazwa'],
449                 'class': 'predicate'
450             }
451             self.options = {'predicate': True}
452         elif 'cel' in lista.attrib:
453             attrs = {
454                 'data-target': lista.attrib['cel'],
455                 'class': 'subject'
456             }
457             self.options = {'subject': True, 'handles': 'uchwyty' in lista.attrib}
458         else:
459             attrs = {}
460         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
461         return pre, post + '<br class="clr"/>'
462
463     def handle_punkt(self, element):
464         if self.options['subject']:
465             self.piece_counter += 1
466             if self.options['handles']:
467                 return '<li><span data-solution="%s" data-no="%s" class="question-piece draggable handle add-li">%s</span>' % (element.attrib['rozw'], self.piece_counter, self.piece_counter), '</li>'
468             else:
469                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable">' % (element.attrib['rozw'], self.piece_counter), '</li>'
470
471         elif self.options['predicate']:
472             if self.options['min']:
473                 placeholders = u'<li class="placeholder"/>' * self.options['min']
474             else:
475                 placeholders = u'<li class="placeholder multiple"/>'
476             return '<li data-predicate="%(nazwa)s">' % element.attrib, '<ul>' + placeholders + '</ul></li>'
477
478         else:
479             return super(Przyporzadkuj, self).handle_punkt(element)
480
481
482 class PrawdaFalsz(Exercise):
483     def handle_punkt(self, element):
484         if 'rozw' in element.attrib:
485             return u'''<li data-solution="%s" class="question-piece">
486             <span class="buttons">
487             <a href="#" data-value="true" class="true">Prawda</a>
488             <a href="#" data-value="false" class="false">Fałsz</a>
489         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
490         else:
491             return super(PrawdaFalsz, self).handle_punkt(element)
492
493
494 class EduModuleFormat(Format):
495     DEFAULT_MATERIAL_FORMAT = 'odt'
496
497     class MaterialNotFound(BaseException):
498         pass
499
500     def __init__(self, wldoc, **kwargs):
501         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
502
503     def build(self):
504         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self, 'wldoc': self.wldoc})
505
506         html = edumod.generate(self.wldoc.edoc.getroot())
507
508         return IOFile.from_string(html.encode('utf-8'))
509
510     def url_for_material(self, slug, fmt=None):
511         if fmt is None:
512             fmt = self.DEFAULT_MATERIAL_FORMAT
513         # No briliant idea for an API here.
514         if fmt:
515             return "%s.%s" % (slug, fmt)
516         return slug
517
518
519 def transform(wldoc, stylesheet='edumed', options=None, flags=None):
520     """Transforms the WL document to XHTML.
521
522     If output_filename is None, returns an XML,
523     otherwise returns True if file has been written,False if it hasn't.
524     File won't be written if it has no content.
525     """
526     edumodfor = EduModuleFormat(wldoc)
527     return edumodfor.build()