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