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