give subjects back
[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, tag_open_close
9 from librarian import functions
10 import re
11 import random
12
13
14 class EduModule(Xmill):
15     def __init__(self, options=None):
16         super(EduModule, self).__init__(options)
17         self.activity_counter = 0
18         self.exercise_counter = 0
19
20         # text filters
21         def swap_endlines(txt):
22             if self.options['strofa']:
23                 txt = txt.replace("/\n", "<br/>\n")
24             return txt
25         self.register_text_filter(functions.substitute_entities)
26         self.register_text_filter(swap_endlines)
27
28     @tagged('div', 'stanza')
29     def handle_strofa(self, element):
30         self.options = {'strofa': True}
31         return "", ""
32
33     def handle_powiesc(self, element):
34         return u"""
35 <div class="module" id="book-text">
36 <!-- <span class="teacher-toggle">
37   <input type="checkbox" name="teacher-toggle" id="teacher-toggle"/>
38   <label for="teacher-toggle">Pokaż treść dla nauczyciela</label>
39  </span>-->
40
41 """, u"</div>"
42
43     handle_autor_utworu = tag("span", "author")
44     handle_dzielo_nadrzedne = tag("span", "collection")
45     handle_podtytul = tag("span", "subtitle")
46     handle_naglowek_akt = handle_naglowek_czesc = handle_srodtytul = tag("h2")
47     handle_naglowek_scena = tag('h2')
48     handle_naglowek_osoba = handle_naglowek_podrozdzial = tag('h3')
49     handle_akap = handle_akap_dialog = handle_akap_cd = tag('p', 'paragraph')
50
51     handle_wyroznienie = tag('em')
52     handle_tytul_dziela = tag('em', 'title')
53     handle_slowo_obce = tag('em', 'foreign')
54
55     def naglowek_to_anchor(self, naglowek):
56         return re.sub(r" +", " ", naglowek.text.strip())
57
58     def handle_nazwa_utworu(self, element):
59         toc = []
60         for naglowek in element.getparent().findall('.//naglowek_rozdzial'):
61             a = etree.Element("a")
62             a.attrib["href"] = "#" + self.naglowek_to_anchor(naglowek)
63             a.text = naglowek.text
64             atxt = etree.tostring(a, encoding=unicode)
65             toc.append("<li>%s</li>" % atxt)
66         toc = "<ul class='toc'>%s</ul>" % "".join(toc)
67         add_header = "Lekcja: " if self.options['wldoc'].book_info.type in ('course', 'synthetic') else ''
68         return "<h1 class='title'>%s" % add_header, "</h1>" + toc
69
70     @tagged("h2")
71     def handle_naglowek_rozdzial(self, element):
72         return "", "".join(tag_open_close("a", name=self.naglowek_to_anchor(element)))
73
74     def handle_uwaga(self, _e):
75         return None
76
77     def handle_aktywnosc(self, element):
78         self.activity_counter += 1
79         self.options = {
80             'activity': True,
81             'activity_counter': self.activity_counter,
82             }
83         submill = EduModule(dict(self.options.items() + {'sub_gen': True}.items()))
84
85         opis = submill.generate(element.xpath('opis')[0])
86
87         n = element.xpath('wskazowki')
88         if n: wskazowki = submill.generate(n[0])
89
90         else: wskazowki = ''
91         n = element.xpath('pomoce')
92
93         if n: pomoce = submill.generate(n[0])
94         else: pomoce = ''
95
96         forma = ''.join(element.xpath('forma/text()'))
97
98         czas = ''.join(element.xpath('czas/text()'))
99
100         counter = self.activity_counter
101
102         return u"""
103 <div class="activity">
104  <div class="text">
105   <span class="act_counter">%(counter)d.</span>
106   %(opis)s""" % locals(), \
107 u"""%(wskazowki)s
108  </div>
109  <aside class="info">
110   <section class="infobox time"><h1>Czas</h1><p>%(czas)s min</p></section>
111   <section class="infobox kind"><h1>Metoda</h1><p>%(forma)s</p></section>
112   %(pomoce)s
113  </aside>
114  <div class="clearboth"></div>
115 </div>
116 """ % locals()
117
118     handle_opis = ifoption(sub_gen=True)(tag('div', 'description'))
119     handle_wskazowki = ifoption(sub_gen=True)(tag('div', ('hints', 'teacher')))
120
121     @ifoption(sub_gen=True)
122     @tagged('section', 'infobox materials')
123     def handle_pomoce(self, _):
124         return """<h1>Pomoce</h1>""", ""
125
126     def handle_czas(self, *_):
127         return
128
129     def handle_forma(self, *_):
130         return
131
132     def handle_cwiczenie(self, element):
133         exercise_handlers = {
134             'wybor': Wybor,
135             'uporzadkuj': Uporzadkuj,
136             'luki': Luki,
137             'zastap': Zastap,
138             'przyporzadkuj': Przyporzadkuj,
139             'prawdafalsz': PrawdaFalsz
140             }
141
142         typ = element.attrib['typ']
143         self.exercise_counter += 1
144         self.options = {'exercise_counter': self.exercise_counter}
145         handler = exercise_handlers[typ](self.options)
146         return handler.generate(element)
147
148     # Lists
149     def handle_lista(self, element, attrs={}):
150         ltype = element.attrib.get('typ', 'punkt')
151         if ltype == 'slowniczek':
152             surl = element.attrib.get('src', None)
153             if surl is None:
154                 # print '** missing src on <slowniczek>, setting default'
155                 surl = 'http://edukacjamedialna.edu.pl/slowniczek'
156             sxml = None
157             if surl:
158                 sxml = etree.fromstring(self.options['provider'].by_uri(surl).get_string())
159             self.options = {'slowniczek': True, 'slowniczek_xml': sxml }
160             return '<div class="slowniczek">', '</div>'
161
162         listtag = {'num': 'ol',
163                'punkt': 'ul',
164                'alfa': 'ul',
165                'czytelnia': 'ul'}[ltype]
166
167         classes = attrs.get('class', '')
168         if classes: del attrs['class']
169
170         attrs_s = ' '.join(['%s="%s"' % kv for kv in attrs.items()])
171         if attrs_s: attrs_s = ' ' + attrs_s
172
173         return '<%s class="lista %s %s"%s>' % (listtag, ltype, classes, attrs_s), '</%s>' % listtag
174
175     def handle_punkt(self, element):
176         if self.options['slowniczek']:
177             return '<dl>', '</dl>'
178         else:
179             return '<li>', '</li>'
180
181     def handle_definiendum(self, element):
182         nxt = element.getnext()
183         definiens_s = ''
184
185         if not element.text:
186             print "!! Empty <definiendum/>"
187             return None
188
189         # let's pull definiens from another document
190         if self.options['slowniczek_xml'] is not None and (nxt is None or nxt.tag != 'definiens'):
191             sxml = self.options['slowniczek_xml']
192             defloc = sxml.xpath("//definiendum[text()='%s']" % element.text)
193             if defloc:
194                 definiens = defloc[0].getnext()
195                 if definiens.tag == 'definiens':
196                     subgen = EduModule(self.options)
197                     definiens_s = subgen.generate(definiens)
198             else:
199                 print '!! Missing definiendum in source:', element.text
200
201         return u"<dt>", u"</dt>" + definiens_s
202
203     def handle_definiens(self, element):
204         return u"<dd>", u"</dd>"
205
206     def handle_podpis(self, element):
207         return u"""<div class="caption">""", u"</div>"
208
209     def handle_tabela(self, element):
210         has_frames = int(element.attrib.get("ramki", "0"))
211         if has_frames: frames_c = "framed"
212         else: frames_c = ""
213         return u"""<table class="%s">""" % frames_c, u"</table>"
214
215     def handle_wiersz(self, element):
216         return u"<tr>", u"</tr>"
217
218     def handle_kol(self, element):
219         return u"<td>", u"</td>"
220
221     def handle_rdf__RDF(self, _):
222         # ustal w opcjach  rzeczy :D
223         return
224
225     def handle_link(self, element):
226         if 'url' in element.attrib:
227             return tag('a', href=element.attrib['url'])(self, element)
228         elif 'material' in element.attrib:
229             material_err = u' [BRAKUJĄCY MATERIAŁ]'
230             slug = element.attrib['material']
231             make_url = lambda f: self.options['urlmapper'] \
232               .url_for_material(slug, f)
233
234             if 'format' in element.attrib:
235                 formats = re.split(r"[, ]+",
236                                element.attrib['format'])
237             else:
238                 formats = [None]
239
240             formats = self.options['urlmapper'].materials(slug)
241
242             try:
243                 def_href = make_url(formats[0][0])
244                 def_err = u""
245             except (IndexError, self.options['urlmapper'].MaterialNotFound):
246                 def_err = material_err
247                 def_href = u""
248             fmt_links = []
249             for f in formats[1:]:
250                 try:
251                     fmt_links.append(u'<a href="%s">%s</a>' % (make_url(f[0]), f[0].upper()))
252                 except self.options['urlmapper'].MaterialNotFound:
253                     fmt_links.append(u'<a>%s%s</a>' % (f[0].upper(), material_err))
254             more_links = u' (%s)' % u', '.join(fmt_links) if fmt_links else u''
255
256             return u"<a href='%s'>" % def_href, u'%s</a>%s' % (def_err, more_links)
257
258
259 class Exercise(EduModule):
260     def __init__(self, *args, **kw):
261         self.question_counter = 0
262         super(Exercise, self).__init__(*args, **kw)
263
264     handle_opis = tag('div', 'description')
265
266     def handle_rozw_kom(self, element):
267         return u"""<div style="display:none" class="comment">""", u"""</div>"""
268
269     def handle_cwiczenie(self, element):
270         self.options = {'exercise': element.attrib['typ']}
271         self.question_counter = 0
272         self.piece_counter = 0
273
274         pre = u"""
275 <div class="exercise %(typ)s" data-type="%(typ)s">
276 <form action="#" method="POST">
277 <h3>Zadanie %(exercies_counter)d</h3>
278 <div class="buttons">
279 <span class="message"></span>
280 <input type="button" class="check" value="sprawdź"/>
281 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
282 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
283 <input type="button" class="reset" value="reset"/>
284 </div>
285
286 """ % {'exercies_counter': self.options['exercise_counter'], 'typ': element.attrib['typ']}
287         post = u"""
288 <div class="buttons">
289 <span class="message"></span>
290 <input type="button" class="check" value="sprawdź"/>
291 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
292 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
293 <input type="button" class="reset" value="reset"/>
294 </div>
295 </form>
296 </div>
297 """
298         # Add a single <pytanie> tag if it's not there
299         if not element.xpath(".//pytanie"):
300             qpre, qpost = self.handle_pytanie(element)
301             pre = pre + qpre
302             post = qpost + post
303         return pre, post
304
305     def handle_pytanie(self, element):
306         """This will handle <cwiczenie> element, when there is no <pytanie>
307         """
308         add_class = ""
309         self.question_counter += 1
310         self.piece_counter = 0
311         solution = element.attrib.get('rozw', None)
312         if solution: solution_s = ' data-solution="%s"' % solution
313         else: solution_s = ''
314
315         handles = element.attrib.get('uchwyty', None)
316         if handles:
317             add_class += ' handles handles-%s' % handles
318             self.options = {'handles': handles}
319
320         minimum = element.attrib.get('min', None)
321         if minimum: minimum_s = ' data-minimum="%d"' % int(minimum)
322         else: minimum_s = ''
323
324         return '<div class="question%s" data-no="%d" %s>' %\
325             (add_class, self.question_counter, solution_s + minimum_s), \
326             "</div>"
327
328
329 class Wybor(Exercise):
330     def handle_cwiczenie(self, element):
331         pre, post = super(Wybor, self).handle_cwiczenie(element)
332         is_single_choice = True
333         pytania = element.xpath(".//pytanie")
334         if not pytania:
335             pytania = [element]
336         for p in pytania:
337             solutions = re.split(r"[, ]+", p.attrib['rozw'])
338             if len(solutions) != 1:
339                 is_single_choice = False
340                 break
341             choices = p.xpath(".//*[@nazwa]")
342             uniq = set()
343             for n in choices: uniq.add(n.attrib['nazwa'])
344             if len(choices) != len(uniq):
345                 is_single_choice = False
346                 break
347
348         self.options = {'single': is_single_choice}
349         return pre, post
350
351     def handle_punkt(self, element):
352         if self.options['exercise'] and element.attrib.get('nazwa', None):
353             qc = self.question_counter
354             self.piece_counter += 1
355             no = self.piece_counter
356             eid = "q%(qc)d_%(no)d" % locals()
357             aname = element.attrib.get('nazwa', None)
358             if self.options['single']:
359                 return u"""
360 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
361 <input type="radio" name="q%(qc)d" id="%(eid)s" value="%(aname)s" />
362 <label for="%(eid)s">
363                 """ % locals(), u"</label></li>"
364             else:
365                 return u"""
366 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
367 <input type="checkbox" name="%(eid)s" id="%(eid)s" />
368 <label for="%(eid)s">
369 """ % locals(), u"</label></li>"
370
371         else:
372             return super(Wybor, self).handle_punkt(element)
373
374
375 class Uporzadkuj(Exercise):
376     def handle_pytanie(self, element):
377         """
378 Overrides the returned content default handle_pytanie
379         """
380         # we ignore the result, returning our own
381         super(Uporzadkuj, self).handle_pytanie(element)
382         order_items = element.xpath(".//punkt/@rozw")
383
384         return u"""<div class="question" data-original="%s" data-no="%s">""" % \
385             (','.join(order_items), self.question_counter), \
386             u"""</div>"""
387
388     def handle_punkt(self, element):
389         return """<li class="question-piece" data-pos="%(rozw)s"/>""" \
390             % element.attrib,\
391             "</li>"
392
393
394 class Luki(Exercise):
395     def find_pieces(self, question):
396         return question.xpath(".//luka")
397
398     def solution_html(self, piece):
399         sub = EduModule()
400         return sub.generate(piece)
401         # print piece.text
402         # return piece.text + ''.join(
403         #     [etree.tostring(n, encoding=unicode)
404         #      for n in piece])
405
406     def handle_pytanie(self, element):
407         qpre, qpost = super(Luki, self).handle_pytanie(element)
408
409         luki = list(enumerate(self.find_pieces(element)))
410         luki_html = ""
411         i = 0
412         random.shuffle(luki)
413         for (i, luka) in luki:
414             i += 1
415             luka_html = self.solution_html(luka)
416             luki_html += u'<span class="draggable question-piece" data-no="%d">%s</span>' % (i, luka_html)
417         self.words_html = '<div class="words">%s</div>' % luki_html
418
419         return qpre, qpost
420
421     def handle_opis(self, element):
422         return '', self.words_html
423
424     def handle_luka(self, element):
425         self.piece_counter += 1
426         return '<span class="placeholder" data-solution="%d"></span>' % self.piece_counter
427
428
429 class Zastap(Luki):
430     def find_pieces(self, question):
431         return question.xpath(".//zastap")
432
433     def solution_html(self, piece):
434         return piece.attrib['rozw']
435
436     def handle_zastap(self, element):
437         self.piece_counter += 1
438         return '<span class="placeholder zastap question-piece" data-solution="%d">' \
439             % self.piece_counter, '</span>'
440
441
442 class Przyporzadkuj(Exercise):
443     def handle_pytanie(self, element):
444         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
445         minimum = element.attrib.get("min", None)
446         if minimum:
447             self.options = {"min": int(minimum)}
448         return pre, post
449
450     def handle_lista(self, lista):
451         if 'nazwa' in lista.attrib:
452             attrs = {
453                 'data-name': lista.attrib['nazwa'],
454                 'class': 'predicate'
455             }
456             self.options = {'predicate': True}
457         elif 'cel' in lista.attrib:
458             attrs = {
459                 'data-target': lista.attrib['cel'],
460                 'class': 'subject'
461             }
462             self.options = {'subject': True, 'handles': 'uchwyty' in lista.attrib}
463         else:
464             attrs = {}
465         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
466         return pre, post + '<br class="clr"/>'
467
468     def handle_punkt(self, element):
469         if self.options['subject']:
470             self.piece_counter += 1
471             if self.options['handles']:
472                 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>'
473             else:
474                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable">' % (element.attrib['rozw'], self.piece_counter), '</li>'
475
476         elif self.options['predicate']:
477             if self.options['min']:
478                 placeholders = u'<li class="placeholder"/>' * self.options['min']
479             else:
480                 placeholders = u'<li class="placeholder multiple"/>'
481             return '<li data-predicate="%(nazwa)s">' % element.attrib, '<ul class="subjects">' + placeholders + '</ul></li>'
482
483         else:
484             return super(Przyporzadkuj, self).handle_punkt(element)
485
486
487 class PrawdaFalsz(Exercise):
488     def handle_punkt(self, element):
489         if 'rozw' in element.attrib:
490             return u'''<li data-solution="%s" class="question-piece">
491             <span class="buttons">
492             <a href="#" data-value="true" class="true">Prawda</a>
493             <a href="#" data-value="false" class="false">Fałsz</a>
494         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
495         else:
496             return super(PrawdaFalsz, self).handle_punkt(element)
497
498
499 class EduModuleFormat(Format):
500     PRIMARY_MATERIAL_FORMATS = ('pdf', 'odt')
501
502     class MaterialNotFound(BaseException):
503         pass
504
505     def __init__(self, wldoc, **kwargs):
506         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
507
508     def build(self):
509         # Sort materials by slug.
510         self.materials_by_slug = {}
511         for name, att in self.wldoc.source.attachments.items():
512             parts = name.rsplit('.', 1)
513             if len(parts) == 1:
514                 continue
515             slug, ext = parts
516             if slug not in self.materials_by_slug:
517                 self.materials_by_slug[slug] = {}
518             self.materials_by_slug[slug][ext] = att
519
520         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self, 'wldoc': self.wldoc})
521
522         html = edumod.generate(self.wldoc.edoc.getroot())
523
524         return IOFile.from_string(html.encode('utf-8'))
525
526     def materials(self, slug):
527         """Returns a list of pairs: (ext, iofile)."""
528         order = dict(reversed(k) for k in enumerate(self.PRIMARY_MATERIAL_FORMATS))
529         mats = self.materials_by_slug.get(slug, {}).items()
530         if not mats:
531             print "!! Material missing: '%s'" % slug
532         return sorted(mats, key=lambda (x, y): order.get(x, x))
533
534     def url_for_material(self, slug, fmt):
535         return "%s.%s" % (slug, fmt)
536
537
538 def transform(wldoc, stylesheet='edumed', options=None, flags=None):
539     """Transforms the WL document to XHTML.
540
541     If output_filename is None, returns an XML,
542     otherwise returns True if file has been written,False if it hasn't.
543     File won't be written if it has no content.
544     """
545     edumodfor = EduModuleFormat(wldoc)
546     return edumodfor.build()