minor fixes
[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": thumb_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         if not self.instruction_printed:
495             self.instruction_printed = True
496             return u'<span class="instruction">%s</span>' % self.INSTRUCTION[self.options['handles'] and 1 or 0]
497         else:
498             return ""
499
500     def handle_cwiczenie(self, element):
501         pre, post = super(Przyporzadkuj, self).handle_cwiczenie(element)
502         lista_with_handles = element.xpath(".//*[@uchwyty]")
503         if lista_with_handles:
504             self.options = {'handles': True}
505         return pre, post
506
507     def handle_pytanie(self, element):
508         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
509         minimum = element.attrib.get("min", None)
510         if minimum:
511             self.options = {"min": int(minimum)}
512         return pre, post
513
514     def handle_lista(self, lista):
515         if 'nazwa' in lista.attrib:
516             attrs = {
517                 'data-name': lista.attrib['nazwa'],
518                 'class': 'predicate'
519             }
520             self.options = {'predicate': True}
521         elif 'cel' in lista.attrib:
522             attrs = {
523                 'data-target': lista.attrib['cel'],
524                 'class': 'subject'
525             }
526             self.options = {'subject': True}
527         else:
528             attrs = {}
529         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
530         return pre, post + '<br class="clr"/>'
531
532     def handle_punkt(self, element):
533         if self.options['subject']:
534             self.piece_counter += 1
535             if self.options['handles']:
536                 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>'
537             else:
538                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable">' % (element.attrib['rozw'], self.piece_counter), '</li>'
539
540         elif self.options['predicate']:
541             if self.options['min']:
542                 placeholders = u'<li class="placeholder"/>' * self.options['min']
543             else:
544                 placeholders = u'<li class="placeholder multiple"/>'
545             return '<li data-predicate="%(nazwa)s">' % element.attrib, '<ul class="subjects">' + placeholders + '</ul></li>'
546
547         else:
548             return super(Przyporzadkuj, self).handle_punkt(element)
549
550
551 class PrawdaFalsz(Exercise):
552     def handle_punkt(self, element):
553         if 'rozw' in element.attrib:
554             return u'''<li data-solution="%s" class="question-piece">
555             <span class="buttons">
556             <a href="#" data-value="true" class="true">Prawda</a>
557             <a href="#" data-value="false" class="false">Fałsz</a>
558         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
559         else:
560             return super(PrawdaFalsz, self).handle_punkt(element)
561
562
563 class EduModuleFormat(Format):
564     PRIMARY_MATERIAL_FORMATS = ('pdf', 'odt')
565
566     class MaterialNotFound(BaseException):
567         pass
568
569     def __init__(self, wldoc, **kwargs):
570         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
571
572     def build(self):
573         # Sort materials by slug.
574         self.materials_by_slug = {}
575         for name, att in self.wldoc.source.attachments.items():
576             parts = name.rsplit('.', 1)
577             if len(parts) == 1:
578                 continue
579             slug, ext = parts
580             if slug not in self.materials_by_slug:
581                 self.materials_by_slug[slug] = {}
582             self.materials_by_slug[slug][ext] = att
583
584         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self, 'wldoc': self.wldoc})
585
586         html = edumod.generate(self.wldoc.edoc.getroot())
587
588         return IOFile.from_string(html.encode('utf-8'))
589
590     def materials(self, slug):
591         """Returns a list of pairs: (ext, iofile)."""
592         order = dict(reversed(k) for k in enumerate(self.PRIMARY_MATERIAL_FORMATS))
593         mats = self.materials_by_slug.get(slug, {}).items()
594         if not mats:
595             print "!! Material missing: '%s'" % slug
596         return sorted(mats, key=lambda (x, y): order.get(x, x))
597
598     def url_for_material(self, slug, fmt):
599         return "%s.%s" % (slug, fmt)
600
601     def url_for_image(self, slug, fmt, width=None):
602         return self.url_for_material(self, slug, fmt)
603
604
605 def transform(wldoc, stylesheet='edumed', options=None, flags=None):
606     """Transforms the WL document to XHTML.
607
608     If output_filename is None, returns an XML,
609     otherwise returns True if file has been written,False if it hasn't.
610     File won't be written if it has no content.
611     """
612     edumodfor = EduModuleFormat(wldoc)
613     return edumodfor.build()