bed9798a807305581fa166a899a2897ed4ecc7ee
[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(dict(self.options.items() + {'sub_gen': True}.items()))
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(sub_gen=True)(tag('div', 'description'))
82     handle_wskazowki = ifoption(sub_gen=True)(tag('div', ('hints', 'teacher')))
83
84     @ifoption(sub_gen=True)
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.options['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             assert element.text != ''
147             defloc = sxml.xpath("//definiendum[text()='%s']" % element.text)
148             if defloc:
149                 definiens = defloc[0].getnext()
150                 if definiens.tag == 'definiens':
151                     subgen = EduModule(self.options)
152                     definiens_s = subgen.generate(definiens)
153
154         return u"<dt>", u"</dt>" + definiens_s
155
156     def handle_definiens(self, element):
157         return u"<dd>", u"</dd>"
158
159
160     def handle_podpis(self, element):
161         return u"""<div class="caption">""", u"</div>"
162
163     def handle_tabela(self, element):
164         has_frames = int(element.attrib.get("ramki", "0"))
165         if has_frames: frames_c = "framed"
166         else: frames_c = ""
167         return u"""<table class="%s">""" % frames_c, u"</table>"
168
169     def handle_wiersz(self, element):
170         return u"<tr>", u"</tr>"
171
172     def handle_kol(self, element):
173         return u"<td>", u"</td>"
174
175     def handle_rdf__RDF(self, _):
176         # ustal w opcjach  rzeczy :D
177         return
178
179     def handle_link(self, element):
180         if 'material' in element.attrib:
181             formats = re.split(r"[, ]+", element.attrib['format'])
182             fmt_links = []
183             for f in formats:
184                 fmt_links.append(u'<a href="%s">%s</a>' % (self.options['urlmapper'].url_for_material(element.attrib['material'], f), f.upper()))
185
186             return u"", u' (%s)' % u' '.join(fmt_links)
187
188
189 class Exercise(EduModule):
190     def __init__(self, *args, **kw):
191         self.question_counter = 0
192         super(Exercise, self).__init__(*args, **kw)
193
194     def handle_rozw_kom(self, element):
195         return u"""<div style="display:none" class="comment">""", u"""</div>"""
196
197     def handle_cwiczenie(self, element):
198         self.options = {'exercise': element.attrib['typ']}
199         self.question_counter = 0
200         self.piece_counter = 0
201
202         pre = u"""
203 <div class="exercise %(typ)s" data-type="%(typ)s">
204 <form action="#" method="POST">
205 """ % element.attrib
206         post = u"""
207 <div class="buttons">
208 <span class="message"></span>
209 <input type="button" class="check" value="sprawdź"/>
210 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
211 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
212 <input type="button" class="reset" value="reset"/>
213 </div>
214 </form>
215 </div>
216 """
217         # Add a single <pytanie> tag if it's not there
218         if not element.xpath(".//pytanie"):
219             qpre, qpost = self.handle_pytanie(element)
220             pre = pre + qpre
221             post = qpost + post
222         return pre, post
223
224     def handle_pytanie(self, element):
225         """This will handle <cwiczenie> element, when there is no <pytanie>
226         """
227         add_class = ""
228         self.question_counter += 1
229         self.piece_counter = 0
230         solution = element.attrib.get('rozw', None)
231         if solution: solution_s = ' data-solution="%s"' % solution
232         else: solution_s = ''
233
234         handles = element.attrib.get('uchwyty', None)
235         if handles:
236             add_class += ' handles handles-%s' % handles
237             self.options = {'handles': handles}
238
239         minimum = element.attrib.get('min', None)
240         if minimum: minimum_s = ' data-minimum="%d"' % int(minimum)
241         else: minimum_s = ''
242
243         return '<div class="question%s" data-no="%d" %s>' %\
244             (add_class, self.question_counter, solution_s + minimum_s), \
245             "</div>"
246
247
248 class Wybor(Exercise):
249     def handle_cwiczenie(self, element):
250         pre, post = super(Wybor, self).handle_cwiczenie(element)
251         is_single_choice = True
252         for p in element.xpath(".//pytanie"):
253             solutions = re.split(r"[, ]+", p.attrib['rozw'])
254             if len(solutions) != 1:
255                 is_single_choice = False
256                 break
257         self.options = {'single': is_single_choice}
258         return pre, post
259
260     def handle_punkt(self, element):
261         if self.options['exercise'] and element.attrib.get('nazwa', None):
262             qc = self.question_counter
263             self.piece_counter += 1
264             no = self.piece_counter
265             eid = "q%(qc)d_%(no)d" % locals()
266             aname = element.attrib.get('nazwa', None)
267             if self.options['single']:
268                 return u"""
269 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
270 <input type="radio" name="q%(qc)d" id="%(eid)s" value="%(aname)s" />
271 <label for="%(eid)s">
272                 """ % locals(), u"</label></li>"
273             else:
274                 return u"""
275 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
276 <input type="checkbox" name="%(eid)s" id="%(eid)s" />
277 <label for="%(eid)s">
278 """ % locals(), u"</label></li>"
279
280         else:
281             return super(Wybor, self).handle_punkt(element)
282
283
284 class Uporzadkuj(Exercise):
285     def handle_pytanie(self, element):
286         """
287 Overrides the returned content default handle_pytanie
288         """
289         # we ignore the result, returning our own
290         super(Uporzadkuj, self).handle_pytanie(element)
291         order_items = element.xpath(".//punkt/@rozw")
292
293         return u"""<div class="question" data-original="%s" data-no="%s">""" % \
294             (','.join(order_items), self.question_counter), \
295             u"""</div>"""
296
297     def handle_punkt(self, element):
298         return """<li class="question-piece" data-pos="%(rozw)s"/>""" \
299             % element.attrib,\
300             "</li>"
301
302
303 class Luki(Exercise):
304     def find_pieces(self, question):
305         return question.xpath("//luka")
306
307     def solution_html(self, piece):
308         return piece.text + ''.join(
309             [etree.tostring(n, encoding=unicode)
310              for n in piece])
311
312     def handle_pytanie(self, element):
313         qpre, qpost = super(Luki, self).handle_pytanie(element)
314
315         luki = list(enumerate(self.find_pieces(element)))
316         luki_html = ""
317         i = 0
318         random.shuffle(luki)
319         for (i, luka) in luki:
320             i += 1
321             luka_html = self.solution_html(luka)
322             luki_html += u'<span class="draggable question-piece" data-no="%d">%s</span>' % (i, luka_html)
323         self.words_html = '<div class="words">%s</div>' % luki_html
324
325         return qpre, qpost
326
327     def handle_opis(self, element):
328         pre, post = super(Luki, self).handle_opis(element)
329         return pre, self.words_html + post
330
331     def handle_luka(self, element):
332         self.piece_counter += 1
333         return '<span class="placeholder" data-solution="%d"></span>' % self.piece_counter
334
335
336 class Zastap(Luki):
337     def find_pieces(self, question):
338         return question.xpath("//zastap")
339
340     def solution_html(self, piece):
341         return piece.attrib['rozw']
342
343     def handle_zastap(self, element):
344         self.piece_counter += 1
345         return '<span class="placeholder zastap question-piece" data-solution="%d">' \
346             % self.piece_counter, '</span>'
347
348
349 class Przyporzadkuj(Exercise):
350     def handle_pytanie(self, element):
351         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
352         minimum = element.attrib.get("min", None)
353         if minimum:
354             self.options = {"min": int(minimum)}
355         return pre, post
356
357     def handle_lista(self, lista):
358         if 'nazwa' in lista.attrib:
359             attrs = {
360                 'data-name': lista.attrib['nazwa'],
361                 'class': 'predicate'
362             }
363             self.options = {'predicate': True}
364         elif 'cel' in lista.attrib:
365             attrs = {
366                 'data-target': lista.attrib['cel'],
367                 'class': 'subject'
368             }
369             self.options = {'subject': True, 'handles': 'uchwyty' in lista.attrib}
370         else:
371             attrs = {}
372         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
373         return pre, post + '<br class="clr"/>'
374
375     def handle_punkt(self, element):
376         if self.options['subject']:
377             self.piece_counter += 1
378             if self.options['handles']:
379                 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>'
380             else:
381                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable">' % (element.attrib['rozw'], self.piece_counter), '</li>'
382
383         elif self.options['predicate']:
384             if self.options['min']:
385                 placeholders = u'<li class="placeholder"/>' * self.options['min']
386             else:
387                 placeholders = u'<li class="placeholder multiple"/>'
388             return '<li data-predicate="%(nazwa)s">' % element.attrib, '<ul class="subjects">' + placeholders + '</ul></li>'
389
390         else:
391             return super(Przyporzadkuj, self).handle_punkt(element)
392
393
394 class PrawdaFalsz(Exercise):
395     def handle_punkt(self, element):
396         if 'rozw' in element.attrib:
397             return u'''<li data-solution="%s" class="question-piece">
398             <span class="buttons">
399             <a href="#" data-value="true" class="true">Prawda</a>
400             <a href="#" data-value="false" class="false">Fałsz</a>
401         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
402         else:
403             return super(PrawdaFalsz, self).handle_punkt(element)
404
405
406 class EduModuleFormat(Format):
407     def __init__(self, wldoc, **kwargs):
408         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
409
410     def build(self):
411         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self})
412
413         html = edumod.generate(self.wldoc.edoc.getroot())
414
415         return IOFile.from_string(html.encode('utf-8'))
416
417     def url_for_material(self, slug, fmt=None):
418         # No briliant idea for an API here.
419         if fmt:
420             return "%s.%s" % (slug, fmt)
421         return slug
422
423
424 def transform(wldoc, stylesheet='edumed', options=None, flags=None):
425     """Transforms the WL document to XHTML.
426
427     If output_filename is None, returns an XML,
428     otherwise returns True if file has been written,False if it hasn't.
429     File won't be written if it has no content.
430     """
431     edumodfor = EduModuleFormat(wldoc)
432     return edumodfor.build()