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