minor name fix
[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         for p in element.xpath(".//pytanie"):
270             solutions = re.split(r"[, ]+", p.attrib['rozw'])
271             if len(solutions) != 1:
272                 is_single_choice = False
273                 break
274             choices = element.xpath(".//*[@nazwa]")
275             uniq = set()
276             for n in choices: uniq.add(n.attrib['nazwa'])
277             if len(choices) != len(uniq):
278                 is_single_choice = False
279                 break
280
281         self.options = {'single': is_single_choice}
282         return pre, post
283
284     def handle_punkt(self, element):
285         if self.options['exercise'] and element.attrib.get('nazwa', None):
286             qc = self.question_counter
287             self.piece_counter += 1
288             no = self.piece_counter
289             eid = "q%(qc)d_%(no)d" % locals()
290             aname = element.attrib.get('nazwa', None)
291             if self.options['single']:
292                 return u"""
293 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
294 <input type="radio" name="q%(qc)d" id="%(eid)s" value="%(aname)s" />
295 <label for="%(eid)s">
296                 """ % locals(), u"</label></li>"
297             else:
298                 return u"""
299 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
300 <input type="checkbox" name="%(eid)s" id="%(eid)s" />
301 <label for="%(eid)s">
302 """ % locals(), u"</label></li>"
303
304         else:
305             return super(Wybor, self).handle_punkt(element)
306
307
308 class Uporzadkuj(Exercise):
309     def handle_pytanie(self, element):
310         """
311 Overrides the returned content default handle_pytanie
312         """
313         # we ignore the result, returning our own
314         super(Uporzadkuj, self).handle_pytanie(element)
315         order_items = element.xpath(".//punkt/@rozw")
316
317         return u"""<div class="question" data-original="%s" data-no="%s">""" % \
318             (','.join(order_items), self.question_counter), \
319             u"""</div>"""
320
321     def handle_punkt(self, element):
322         return """<li class="question-piece" data-pos="%(rozw)s"/>""" \
323             % element.attrib,\
324             "</li>"
325
326
327 class Luki(Exercise):
328     def find_pieces(self, question):
329         return question.xpath(".//luka")
330
331     def solution_html(self, piece):
332         return piece.text + ''.join(
333             [etree.tostring(n, encoding=unicode)
334              for n in piece])
335
336     def handle_pytanie(self, element):
337         qpre, qpost = super(Luki, self).handle_pytanie(element)
338
339         luki = list(enumerate(self.find_pieces(element)))
340         luki_html = ""
341         i = 0
342         random.shuffle(luki)
343         for (i, luka) in luki:
344             i += 1
345             luka_html = self.solution_html(luka)
346             luki_html += u'<span class="draggable question-piece" data-no="%d">%s</span>' % (i, luka_html)
347         self.words_html = '<div class="words">%s</div>' % luki_html
348
349         return qpre, qpost
350
351     def handle_opis(self, element):
352         return '', self.words_html
353
354     def handle_luka(self, element):
355         self.piece_counter += 1
356         return '<span class="placeholder" data-solution="%d"></span>' % self.piece_counter
357
358
359 class Zastap(Luki):
360     def find_pieces(self, question):
361         return question.xpath(".//zastap")
362
363     def solution_html(self, piece):
364         return piece.attrib['rozw']
365
366     def handle_zastap(self, element):
367         self.piece_counter += 1
368         return '<span class="placeholder zastap question-piece" data-solution="%d">' \
369             % self.piece_counter, '</span>'
370
371
372 class Przyporzadkuj(Exercise):
373     def handle_pytanie(self, element):
374         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
375         minimum = element.attrib.get("min", None)
376         if minimum:
377             self.options = {"min": int(minimum)}
378         return pre, post
379
380     def handle_lista(self, lista):
381         if 'nazwa' in lista.attrib:
382             attrs = {
383                 'data-name': lista.attrib['nazwa'],
384                 'class': 'predicate'
385             }
386             self.options = {'predicate': True}
387         elif 'cel' in lista.attrib:
388             attrs = {
389                 'data-target': lista.attrib['cel'],
390                 'class': 'subject'
391             }
392             self.options = {'subject': True, 'handles': 'uchwyty' in lista.attrib}
393         else:
394             attrs = {}
395         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
396         return pre, post + '<br class="clr"/>'
397
398     def handle_punkt(self, element):
399         if self.options['subject']:
400             self.piece_counter += 1
401             if self.options['handles']:
402                 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>'
403             else:
404                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable">' % (element.attrib['rozw'], self.piece_counter), '</li>'
405
406         elif self.options['predicate']:
407             if self.options['min']:
408                 placeholders = u'<li class="placeholder"/>' * self.options['min']
409             else:
410                 placeholders = u'<li class="placeholder multiple"/>'
411             return '<li data-predicate="%(nazwa)s">' % element.attrib, '<ul class="subjects">' + placeholders + '</ul></li>'
412
413         else:
414             return super(Przyporzadkuj, self).handle_punkt(element)
415
416
417 class PrawdaFalsz(Exercise):
418     def handle_punkt(self, element):
419         if 'rozw' in element.attrib:
420             return u'''<li data-solution="%s" class="question-piece">
421             <span class="buttons">
422             <a href="#" data-value="true" class="true">Prawda</a>
423             <a href="#" data-value="false" class="false">Fałsz</a>
424         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
425         else:
426             return super(PrawdaFalsz, self).handle_punkt(element)
427
428
429 class EduModuleFormat(Format):
430     def __init__(self, wldoc, **kwargs):
431         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
432
433     def build(self):
434         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self})
435
436         html = edumod.generate(self.wldoc.edoc.getroot())
437
438         return IOFile.from_string(html.encode('utf-8'))
439
440     def url_for_material(self, slug, fmt=None):
441         # No briliant idea for an API here.
442         if fmt:
443             return "%s.%s" % (slug, fmt)
444         return slug
445
446
447 def transform(wldoc, stylesheet='edumed', options=None, flags=None):
448     """Transforms the WL document to XHTML.
449
450     If output_filename is None, returns an XML,
451     otherwise returns True if file has been written,False if it hasn't.
452     File won't be written if it has no content.
453     """
454     edumodfor = EduModuleFormat(wldoc)
455     return edumodfor.build()