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