a6f668643e556cd9360b1e43462041fe691ce350
[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         sub = EduModule()
361         return sub.generate(piece)
362         # print piece.text
363         # return piece.text + ''.join(
364         #     [etree.tostring(n, encoding=unicode)
365         #      for n in piece])
366
367     def handle_pytanie(self, element):
368         qpre, qpost = super(Luki, self).handle_pytanie(element)
369
370         luki = list(enumerate(self.find_pieces(element)))
371         luki_html = ""
372         i = 0
373         random.shuffle(luki)
374         for (i, luka) in luki:
375             i += 1
376             luka_html = self.solution_html(luka)
377             luki_html += u'<span class="draggable question-piece" data-no="%d">%s</span>' % (i, luka_html)
378         self.words_html = '<div class="words">%s</div>' % luki_html
379
380         return qpre, qpost
381
382     def handle_opis(self, element):
383         return '', self.words_html
384
385     def handle_luka(self, element):
386         self.piece_counter += 1
387         return '<span class="placeholder" data-solution="%d"></span>' % self.piece_counter
388
389
390 class Zastap(Luki):
391     def find_pieces(self, question):
392         return question.xpath(".//zastap")
393
394     def solution_html(self, piece):
395         return piece.attrib['rozw']
396
397     def handle_zastap(self, element):
398         self.piece_counter += 1
399         return '<span class="placeholder zastap question-piece" data-solution="%d">' \
400             % self.piece_counter, '</span>'
401
402
403 class Przyporzadkuj(Exercise):
404     def handle_pytanie(self, element):
405         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
406         minimum = element.attrib.get("min", None)
407         if minimum:
408             self.options = {"min": int(minimum)}
409         return pre, post
410
411     def handle_lista(self, lista):
412         if 'nazwa' in lista.attrib:
413             attrs = {
414                 'data-name': lista.attrib['nazwa'],
415                 'class': 'predicate'
416             }
417             self.options = {'predicate': True}
418         elif 'cel' in lista.attrib:
419             attrs = {
420                 'data-target': lista.attrib['cel'],
421                 'class': 'subject'
422             }
423             self.options = {'subject': True, 'handles': 'uchwyty' in lista.attrib}
424         else:
425             attrs = {}
426         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
427         return pre, post + '<br class="clr"/>'
428
429     def handle_punkt(self, element):
430         if self.options['subject']:
431             self.piece_counter += 1
432             if self.options['handles']:
433                 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>'
434             else:
435                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable">' % (element.attrib['rozw'], self.piece_counter), '</li>'
436
437         elif self.options['predicate']:
438             if self.options['min']:
439                 placeholders = u'<li class="placeholder"/>' * self.options['min']
440             else:
441                 placeholders = u'<li class="placeholder multiple"/>'
442             return '<li data-predicate="%(nazwa)s">' % element.attrib, '<ul class="subjects">' + placeholders + '</ul></li>'
443
444         else:
445             return super(Przyporzadkuj, self).handle_punkt(element)
446
447
448 class PrawdaFalsz(Exercise):
449     def handle_punkt(self, element):
450         if 'rozw' in element.attrib:
451             return u'''<li data-solution="%s" class="question-piece">
452             <span class="buttons">
453             <a href="#" data-value="true" class="true">Prawda</a>
454             <a href="#" data-value="false" class="false">Fałsz</a>
455         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
456         else:
457             return super(PrawdaFalsz, self).handle_punkt(element)
458
459
460 class EduModuleFormat(Format):
461     DEFAULT_MATERIAL_FORMAT = 'odt'
462
463     class MaterialNotFound(BaseException):
464         pass
465
466     def __init__(self, wldoc, **kwargs):
467         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
468
469     def build(self):
470         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self})
471
472         html = edumod.generate(self.wldoc.edoc.getroot())
473
474         return IOFile.from_string(html.encode('utf-8'))
475
476     def url_for_material(self, slug, fmt=None):
477         if fmt is None:
478             fmt = self.DEFAULT_MATERIAL_FORMAT
479         # No briliant idea for an API here.
480         if fmt:
481             return "%s.%s" % (slug, fmt)
482         return slug
483
484
485 def transform(wldoc, stylesheet='edumed', options=None, flags=None):
486     """Transforms the WL document to XHTML.
487
488     If output_filename is None, returns an XML,
489     otherwise returns True if file has been written,False if it hasn't.
490     File won't be written if it has no content.
491     """
492     edumodfor = EduModuleFormat(wldoc)
493     return edumodfor.build()