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