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