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