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