de04fc009080476d0578bb6cef98d61178ad039b
[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     def handle_video(self, element):
259         url = element.attrib.get('url')
260         if not url:
261             print '!! <video> missing url'
262             return
263         m = re.match(r'https?://(?:www.)?youtube.com/watch\?(?:.*&)?v=([^&]+)(?:$|&)', url)
264         if not m:
265             print '!! unknown <video> url scheme:', url
266             return
267         return """<iframe width="630" height="384" src="http://www.youtube.com/embed/%s"
268             frameborder="0" allowfullscreen></iframe>""" % m.group(1), ""
269
270
271 class Exercise(EduModule):
272     INSTRUCTION = ""
273     def __init__(self, *args, **kw):
274         self.question_counter = 0
275         super(Exercise, self).__init__(*args, **kw)
276         self.instruction_printed = False
277
278     @tagged('div', 'description')
279     def handle_opis(self, element):
280         return "", self.get_instruction()
281
282     def handle_rozw_kom(self, element):
283         return u"""<div style="display:none" class="comment">""", u"""</div>"""
284
285     def handle_cwiczenie(self, element):
286         self.options = {'exercise': element.attrib['typ']}
287         self.question_counter = 0
288         self.piece_counter = 0
289
290         pre = u"""
291 <div class="exercise %(typ)s" data-type="%(typ)s">
292 <form action="#" method="POST">
293 <h3>Zadanie %(exercies_counter)d</h3>
294 <div class="buttons">
295 <span class="message"></span>
296 <input type="button" class="check" value="sprawdź"/>
297 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
298 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
299 <input type="button" class="reset" value="reset"/>
300 </div>
301
302 """ % {'exercies_counter': self.options['exercise_counter'], 'typ': element.attrib['typ']}
303         post = u"""
304 <div class="buttons">
305 <span class="message"></span>
306 <input type="button" class="check" value="sprawdź"/>
307 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
308 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
309 <input type="button" class="reset" value="reset"/>
310 </div>
311 </form>
312 </div>
313 """
314         # Add a single <pytanie> tag if it's not there
315         if not element.xpath(".//pytanie"):
316             qpre, qpost = self.handle_pytanie(element)
317             pre = pre + qpre
318             post = qpost + post
319         return pre, post
320
321     def handle_pytanie(self, element):
322         """This will handle <cwiczenie> element, when there is no <pytanie>
323         """
324         add_class = ""
325         self.question_counter += 1
326         self.piece_counter = 0
327         solution = element.attrib.get('rozw', None)
328         if solution: solution_s = ' data-solution="%s"' % solution
329         else: solution_s = ''
330
331         handles = element.attrib.get('uchwyty', None)
332         if handles:
333             add_class += ' handles handles-%s' % handles
334             self.options = {'handles': handles}
335
336         minimum = element.attrib.get('min', None)
337         if minimum: minimum_s = ' data-minimum="%d"' % int(minimum)
338         else: minimum_s = ''
339
340         return '<div class="question%s" data-no="%d" %s>' %\
341             (add_class, self.question_counter, solution_s + minimum_s), \
342             "</div>"
343
344     def get_instruction(self):
345         if not self.instruction_printed:
346             self.instruction_printed = True
347             return u'<span class="instruction">%s</span>' % self.INSTRUCTION
348         else:
349             return ""
350
351
352
353 class Wybor(Exercise):
354     INSTRUCTION = None
355     def handle_cwiczenie(self, element):
356         pre, post = super(Wybor, self).handle_cwiczenie(element)
357         is_single_choice = True
358         pytania = element.xpath(".//pytanie")
359         if not pytania:
360             pytania = [element]
361         for p in pytania:
362             solutions = re.split(r"[, ]+", p.attrib['rozw'])
363             if len(solutions) != 1:
364                 is_single_choice = False
365                 break
366             choices = p.xpath(".//*[@nazwa]")
367             uniq = set()
368             for n in choices: uniq.add(n.attrib['nazwa'])
369             if len(choices) != len(uniq):
370                 is_single_choice = False
371                 break
372
373         self.options = {'single': is_single_choice}
374         return pre, post
375
376     def handle_punkt(self, element):
377         if self.options['exercise'] and element.attrib.get('nazwa', None):
378             qc = self.question_counter
379             self.piece_counter += 1
380             no = self.piece_counter
381             eid = "q%(qc)d_%(no)d" % locals()
382             aname = element.attrib.get('nazwa', None)
383             if self.options['single']:
384                 return u"""
385 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
386 <input type="radio" name="q%(qc)d" id="%(eid)s" value="%(aname)s" />
387 <label for="%(eid)s">
388                 """ % locals(), u"</label></li>"
389             else:
390                 return u"""
391 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
392 <input type="checkbox" name="%(eid)s" id="%(eid)s" />
393 <label for="%(eid)s">
394 """ % locals(), u"</label></li>"
395
396         else:
397             return super(Wybor, self).handle_punkt(element)
398
399
400 class Uporzadkuj(Exercise):
401     INSTRUCTION = u"Kliknij wybraną odpowiedź i przeciągnij w nowe miejsce."
402
403     def handle_pytanie(self, element):
404         """
405 Overrides the returned content default handle_pytanie
406         """
407         # we ignore the result, returning our own
408         super(Uporzadkuj, self).handle_pytanie(element)
409         order_items = element.xpath(".//punkt/@rozw")
410
411         return u"""<div class="question" data-original="%s" data-no="%s">""" % \
412             (','.join(order_items), self.question_counter), \
413             u"""</div>"""
414
415     def handle_punkt(self, element):
416         return """<li class="question-piece" data-pos="%(rozw)s"/>""" \
417             % element.attrib,\
418             "</li>"
419
420
421 class Luki(Exercise):
422     INSTRUCTION = u"Przeciągnij odpowiedzi i upuść w wybranym polu."
423     def find_pieces(self, question):
424         return question.xpath(".//luka")
425
426     def solution_html(self, piece):
427         sub = EduModule()
428         return sub.generate(piece)
429         # print piece.text
430         # return piece.text + ''.join(
431         #     [etree.tostring(n, encoding=unicode)
432         #      for n in piece])
433
434     def handle_pytanie(self, element):
435         qpre, qpost = super(Luki, self).handle_pytanie(element)
436
437         luki = list(enumerate(self.find_pieces(element)))
438         luki_html = ""
439         i = 0
440         random.shuffle(luki)
441         for (i, luka) in luki:
442             i += 1
443             luka_html = self.solution_html(luka)
444             luki_html += u'<span class="draggable question-piece" data-no="%d">%s</span>' % (i, luka_html)
445         self.words_html = '<div class="words">%s</div>' % luki_html
446
447         return qpre, qpost
448
449     def handle_opis(self, element):
450         return '', self.words_html
451
452     def handle_luka(self, element):
453         self.piece_counter += 1
454         return '<span class="placeholder" data-solution="%d"></span>' % self.piece_counter
455
456
457 class Zastap(Luki):
458     INSTRUCTION = u"Przeciągnij odpowiedzi i upuść je na słowie lub wyrażeniu, które chcesz zastąpić."
459
460     def find_pieces(self, question):
461         return question.xpath(".//zastap")
462
463     def solution_html(self, piece):
464         return piece.attrib['rozw']
465
466     def handle_zastap(self, element):
467         self.piece_counter += 1
468         return '<span class="placeholder zastap question-piece" data-solution="%d">' \
469             % self.piece_counter, '</span>'
470
471
472 class Przyporzadkuj(Exercise):
473     INSTRUCTION = [u"Przeciągnij odpowiedzi i upuść w wybranym polu.",
474                    u"Kliknij numer odpowiedzi, przeciągnij i upuść w wybranym polu."]
475
476     def get_instruction(self):
477         print self.options['handles']
478         if not self.instruction_printed:
479             self.instruction_printed = True
480             return u'<span class="instruction">%s</span>' % self.INSTRUCTION[self.options['handles'] and 1 or 0]
481         else:
482             return ""
483
484     def handle_cwiczenie(self, element):
485         pre, post = super(Przyporzadkuj, self).handle_cwiczenie(element)
486         lista_with_handles = element.xpath(".//*[@uchwyty]")
487         if lista_with_handles:
488             self.options = {'handles': True}
489         return pre, post
490
491     def handle_pytanie(self, element):
492         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
493         minimum = element.attrib.get("min", None)
494         if minimum:
495             self.options = {"min": int(minimum)}
496         return pre, post
497
498     def handle_lista(self, lista):
499         if 'nazwa' in lista.attrib:
500             attrs = {
501                 'data-name': lista.attrib['nazwa'],
502                 'class': 'predicate'
503             }
504             self.options = {'predicate': True}
505         elif 'cel' in lista.attrib:
506             attrs = {
507                 'data-target': lista.attrib['cel'],
508                 'class': 'subject'
509             }
510             self.options = {'subject': True}
511         else:
512             attrs = {}
513         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
514         return pre, post + '<br class="clr"/>'
515
516     def handle_punkt(self, element):
517         if self.options['subject']:
518             self.piece_counter += 1
519             if self.options['handles']:
520                 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>'
521             else:
522                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable">' % (element.attrib['rozw'], self.piece_counter), '</li>'
523
524         elif self.options['predicate']:
525             if self.options['min']:
526                 placeholders = u'<li class="placeholder"/>' * self.options['min']
527             else:
528                 placeholders = u'<li class="placeholder multiple"/>'
529             return '<li data-predicate="%(nazwa)s">' % element.attrib, '<ul class="subjects">' + placeholders + '</ul></li>'
530
531         else:
532             return super(Przyporzadkuj, self).handle_punkt(element)
533
534
535 class PrawdaFalsz(Exercise):
536     def handle_punkt(self, element):
537         if 'rozw' in element.attrib:
538             return u'''<li data-solution="%s" class="question-piece">
539             <span class="buttons">
540             <a href="#" data-value="true" class="true">Prawda</a>
541             <a href="#" data-value="false" class="false">Fałsz</a>
542         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
543         else:
544             return super(PrawdaFalsz, self).handle_punkt(element)
545
546
547 class EduModuleFormat(Format):
548     PRIMARY_MATERIAL_FORMATS = ('pdf', 'odt')
549
550     class MaterialNotFound(BaseException):
551         pass
552
553     def __init__(self, wldoc, **kwargs):
554         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
555
556     def build(self):
557         # Sort materials by slug.
558         self.materials_by_slug = {}
559         for name, att in self.wldoc.source.attachments.items():
560             parts = name.rsplit('.', 1)
561             if len(parts) == 1:
562                 continue
563             slug, ext = parts
564             if slug not in self.materials_by_slug:
565                 self.materials_by_slug[slug] = {}
566             self.materials_by_slug[slug][ext] = att
567
568         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self, 'wldoc': self.wldoc})
569
570         html = edumod.generate(self.wldoc.edoc.getroot())
571
572         return IOFile.from_string(html.encode('utf-8'))
573
574     def materials(self, slug):
575         """Returns a list of pairs: (ext, iofile)."""
576         order = dict(reversed(k) for k in enumerate(self.PRIMARY_MATERIAL_FORMATS))
577         mats = self.materials_by_slug.get(slug, {}).items()
578         if not mats:
579             print "!! Material missing: '%s'" % slug
580         return sorted(mats, key=lambda (x, y): order.get(x, x))
581
582     def url_for_material(self, slug, fmt):
583         return "%s.%s" % (slug, fmt)
584
585
586 def transform(wldoc, stylesheet='edumed', options=None, flags=None):
587     """Transforms the WL document to XHTML.
588
589     If output_filename is None, returns an XML,
590     otherwise returns True if file has been written,False if it hasn't.
591     File won't be written if it has no content.
592     """
593     edumodfor = EduModuleFormat(wldoc)
594     return edumodfor.build()