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