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