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