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