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