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