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