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