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