fix cwiczenie wybor
[librarian.git] / librarian / pypdf.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 """PDF creation library.
7
8 Creates one big XML from the book and its children, converts it to LaTeX
9 with TeXML, then runs it by XeLaTeX.
10
11 """
12 from copy import deepcopy
13 import os.path
14 import shutil
15 import re
16 import random
17 from urllib2 import urlopen
18
19 from lxml import etree
20
21 from xmlutils import Xmill, ifoption, tag_open_close
22 from librarian import DCNS, get_resource, IOFile
23 from librarian import functions
24 from pdf import PDFFormat, substitute_hyphens, fix_hanging
25
26
27 def escape(really):
28     def deco(f):
29         def _wrap(*args, **kw):
30             value = f(*args, **kw)
31
32             prefix = (u'<TeXML escape="%d">' % (1 if really else 0))
33             postfix = u'</TeXML>'
34             if isinstance(value, list):
35                 import pdb
36                 pdb.set_trace()
37             if isinstance(value, tuple):
38                 return prefix + value[0], value[1] + postfix
39             else:
40                 return prefix + value + postfix
41         return _wrap
42     return deco
43
44
45 def cmd(name, parms=None):
46     def wrap(self, element=None):
47         pre, post = tag_open_close('cmd', name=name)
48
49         if parms:
50             for parm in parms:
51                 e = etree.Element("parm")
52                 e.text = parm
53                 pre += etree.tostring(e)
54         if element is not None:
55             pre += "<parm>"
56             post = "</parm>" + post
57             return pre, post
58         else:
59             return pre + post
60     return wrap
61
62
63 def mark_alien_characters(text):
64     text = re.sub(ur"([\u0400-\u04ff]+)", ur"<alien>\1</alien>", text)
65     return text
66
67
68 class EduModule(Xmill):
69     def __init__(self, options=None, state=None):
70         super(EduModule, self).__init__(options, state)
71         self.activity_counter = 0
72         self.activity_last = None
73         self.exercise_counter = 0
74
75         def swap_endlines(txt):
76             if self.options['strofa']:
77                 txt = txt.replace("/\n", '<ctrl ch="\\"/>')
78             return txt
79         self.register_text_filter(swap_endlines)
80         self.register_text_filter(functions.substitute_entities)
81         self.register_text_filter(mark_alien_characters)
82
83     def get_dc(self, element, dc_field, single=False):
84         values = map(lambda t: t.text, element.xpath("//dc:%s" % dc_field, namespaces={'dc': DCNS.uri}))
85         if single:
86             return values[0] if len(values) else ''
87         return values
88
89     def handle_rdf__RDF(self, _):
90         """skip metadata in generation"""
91         return
92
93     @escape(True)
94     def get_rightsinfo(self, element):
95         rights_lic = self.get_dc(element, 'rights.license', True)
96         return u'<cmd name="rightsinfostr">' + (u'<opt>%s</opt>' % rights_lic if rights_lic else '') + \
97             u'<parm>%s</parm>' % self.get_dc(element, 'rights', True) + \
98             u'</cmd>'
99
100     @escape(True)
101     def get_authors(self, element, which=None):
102         dc = self.options['wldoc'].book_info
103         if which is None:
104             authors = dc.authors_textbook + \
105                 dc.authors_scenario + \
106                 dc.authors_expert
107         else:
108             authors = getattr(dc, "authors_%s" % which)
109         return u', '.join(author.readable() for author in authors if author)
110
111     @escape(True)
112     def get_title(self, element):
113         return self.get_dc(element, 'title', True)
114
115     @escape(True)
116     def get_description(self, element):
117         desc = self.get_dc(element, 'description', single=True)
118         if not desc:
119             print '!! no description'
120         return desc
121
122     @escape(True)
123     def get_curriculum(self, element):
124         identifiers = self.get_dc(element, 'subject.curriculum')
125         if not identifiers:
126             return ''
127         try:
128             from curriculum.templatetags.curriculum_tags import curriculum
129             curr_elements = curriculum(identifiers)
130         except ImportError:
131             curr_elements = {'identifiers': identifiers}
132         items = ['Podstawa programowa:']
133         newline = '<ctrl ch="\\"/>\n'
134         if 'currset' in curr_elements:
135             for (course, level), types in curr_elements['currset'].iteritems():
136                 lines = [u'%s, %s poziom edukacyjny' % (course, level)]
137                 for type, currs in types.iteritems():
138                     lines.append(type)
139                     lines += [curr.title for curr in currs]
140                 items.append(newline.join(lines))
141         else:
142             items += identifiers
143         return '\n<cmd name="vspace"><parm>.6em</parm></cmd>\n'.join(
144             '<cmd name="akap"><parm>%s</parm></cmd>' % item for item in items)
145
146     def handle_utwor(self, element):
147         lines = [
148             u'''
149                 <TeXML xmlns="http://getfo.sourceforge.net/texml/ns1">
150                 <TeXML escape="0">
151                 \\documentclass[%s]{wl}
152                 \\usepackage{style}''' % self.options['customization_str'],
153             self.options['has_cover'] and '\usepackage{makecover}',
154             (self.options['morefloats'] == 'new' and '\usepackage[maxfloats=64]{morefloats}') or
155             (self.options['morefloats'] == 'old' and '\usepackage{morefloats}') or
156             (self.options['morefloats'] == 'none' and
157                 u'''\\IfFileExists{morefloats.sty}{
158                 \\usepackage{morefloats}
159                 }{}'''),
160             u'''\\def\\authors{%s}''' % self.get_authors(element),
161             u'''\\def\\authorsexpert{%s}''' % self.get_authors(element, 'expert'),
162             u'''\\def\\authorsscenario{%s}''' % self.get_authors(element, 'scenario'),
163             u'''\\def\\authorstextbook{%s}''' % self.get_authors(element, 'textbook'),
164             u'''\\def\\description{%s}''' % self.get_description(element),
165
166             u'''\\author{\\authors}''',
167             u'''\\title{%s}''' % self.get_title(element),
168             u'''\\def\\bookurl{%s}''' % self.options['wldoc'].book_info.url.canonical(),
169             u'''\\def\\rightsinfo{%s}''' % self.get_rightsinfo(element),
170             u'''\\def\\curriculum{%s}''' % self.get_curriculum(element),
171             u'</TeXML>'
172         ]
173
174         return u"".join(filter(None, lines)), u'</TeXML>'
175
176     @escape(True)
177     def handle_powiesc(self, element):
178         return u"""
179     <env name="document">
180     <cmd name="maketitle"/>
181     """, """<cmd name="editorialsection" /></env>"""
182
183     @escape(True)
184     def handle_texcommand(self, element):
185         cmd = functions.texcommand(element.tag)
186         return u'<TeXML escape="1"><cmd name="%s"><parm>' % cmd, u'</parm></cmd></TeXML>'
187
188     handle_akap = \
189         handle_akap_cd = \
190         handle_akap_dialog = \
191         handle_autor_utworu = \
192         handle_dedykacja = \
193         handle_didaskalia = \
194         handle_didask_tekst = \
195         handle_dlugi_cytat = \
196         handle_dzielo_nadrzedne = \
197         handle_lista_osoba = \
198         handle_mat = \
199         handle_miejsce_czas = \
200         handle_motto = \
201         handle_motto_podpis = \
202         handle_naglowek_akt = \
203         handle_naglowek_czesc = \
204         handle_naglowek_listy = \
205         handle_naglowek_osoba = \
206         handle_naglowek_scena = \
207         handle_nazwa_utworu = \
208         handle_nota = \
209         handle_osoba = \
210         handle_pa = \
211         handle_pe = \
212         handle_podtytul = \
213         handle_poezja_cyt = \
214         handle_pr = \
215         handle_pt = \
216         handle_sekcja_asterysk = \
217         handle_sekcja_swiatlo = \
218         handle_separator_linia = \
219         handle_slowo_obce = \
220         handle_srodtytul = \
221         handle_tytul_dziela = \
222         handle_wyroznienie = \
223         handle_dywiz = \
224         handle_texcommand
225
226     def handle_naglowek_rozdzial(self, element):
227         if not self.options['teacher']:
228             if element.text.startswith((u'Wiedza', u'Zadania', u'Słowniczek', u'Dla ucznia')):
229                 self.state['mute'] = False
230             else:
231                 self.state['mute'] = True
232                 return None
233         return self.handle_texcommand(element)
234     handle_naglowek_rozdzial.unmuter = True
235
236     def handle_naglowek_podrozdzial(self, element):
237         self.activity_counter = 0
238         if not self.options['teacher']:
239             if element.text.startswith(u'Dla ucznia'):
240                 self.state['mute'] = False
241                 return None
242             elif element.text.startswith(u'Dla nauczyciela'):
243                 self.state['mute'] = True
244                 return None
245             elif self.state['mute']:
246                 return None
247         return self.handle_texcommand(element)
248     handle_naglowek_podrozdzial.unmuter = True
249
250     def handle_uwaga(self, _e):
251         return None
252
253     def handle_extra(self, _e):
254         return None
255
256     def handle_nbsp(self, _e):
257         return '<spec cat="tilde" />'
258
259     _handle_strofa = cmd("strofa")
260
261     def handle_strofa(self, element):
262         self.options = {'strofa': True}
263         return self._handle_strofa(element)
264
265     def handle_aktywnosc(self, element):
266         self.activity_counter += 1
267         self.options = {
268             'activity': True,
269             'activity_counter': self.activity_counter,
270             'sub_gen': True,
271         }
272         submill = EduModule(self.options, self.state)
273
274         if element.xpath('opis'):
275             opis = submill.generate(element.xpath('opis')[0])
276         else:
277             opis = ''
278
279         n = element.xpath('wskazowki')
280         if n:
281             wskazowki = submill.generate(n[0])
282         else:
283             wskazowki = ''
284         n = element.xpath('pomoce')
285
286         if n:
287             pomoce = submill.generate(n[0])
288         else:
289             pomoce = ''
290
291         forma = ''.join(element.xpath('forma/text()'))
292
293         czas = ''.join(element.xpath('czas/text()'))
294
295         counter = self.activity_counter
296
297         if element.getnext().tag == 'aktywnosc' or (len(self.activity_last) and self.activity_last.getnext() == element):
298             counter_tex = """<cmd name="activitycounter"><parm>%(counter)d.</parm></cmd>""" % locals()
299         else:
300             counter_tex = ''
301
302         self.activity_last = element
303
304         return u"""
305 <cmd name="noindent" />
306 %(counter_tex)s
307 <cmd name="activityinfo"><parm>
308  <cmd name="activitytime"><parm>%(czas)s</parm></cmd>
309  <cmd name="activityform"><parm>%(forma)s</parm></cmd>
310  <cmd name="activitytools"><parm>%(pomoce)s</parm></cmd>
311 </parm></cmd>
312
313
314 %(opis)s
315
316 %(wskazowki)s
317 """ % locals()
318
319     handle_opis = ifoption(sub_gen=True)(lambda s, e: ('', ''))
320     handle_wskazowki = ifoption(sub_gen=True)(lambda s, e: ('', ''))
321
322     @ifoption(sub_gen=True)
323     def handle_pomoce(self, _):
324         return "Pomoce: ", ""
325
326     def handle_czas(self, *_):
327         return
328
329     def handle_forma(self, *_):
330         return
331
332     def handle_lista(self, element, attrs=None):
333         ltype = element.attrib.get('typ', 'punkt')
334         if not element.findall("punkt"):
335             if ltype == 'czytelnia':
336                 return 'W przygotowaniu.'
337             else:
338                 return None
339         if ltype == 'slowniczek':
340             surl = element.attrib.get('src', None)
341             if surl is None:
342                 # print '** missing src on <slowniczek>, setting default'
343                 surl = 'http://edukacjamedialna.edu.pl/lekcje/slowniczek/'
344             sxml = etree.fromstring(self.options['wldoc'].provider.by_uri(surl).get_string())
345             self.options = {'slowniczek': True, 'slowniczek_xml': sxml}
346
347         listcmd = {
348             'num': 'enumerate',
349             'punkt': 'itemize',
350             'alfa': 'itemize',
351             'slowniczek': 'itemize',
352             'czytelnia': 'itemize'
353         }[ltype]
354
355         return u'<env name="%s">' % listcmd, u'</env>'
356
357     def handle_punkt(self, element):
358         return '<cmd name="item"/>', ''
359
360     def handle_cwiczenie(self, element):
361         exercise_handlers = {
362             'wybor': Wybor,
363             'uporzadkuj': Uporzadkuj,
364             'luki': Luki,
365             'zastap': Zastap,
366             'przyporzadkuj': Przyporzadkuj,
367             'prawdafalsz': PrawdaFalsz
368         }
369
370         typ = element.attrib['typ']
371         self.exercise_counter += 1
372         if typ not in exercise_handlers:
373             return '(no handler)'
374         self.options = {'exercise_counter': self.exercise_counter}
375         handler = exercise_handlers[typ](self.options, self.state)
376         return handler.generate(element)
377
378     # XXX this is copied from pyhtml.py, except for return and
379     # should be refactored for no code duplication
380     def handle_definiendum(self, element):
381         nxt = element.getnext()
382         definiens_s = ''
383
384         # let's pull definiens from another document
385         if self.options['slowniczek_xml'] is not None and (nxt is None or nxt.tag != 'definiens'):
386             sxml = self.options['slowniczek_xml']
387             assert element.text != ''
388             if "'" in (element.text or ''):
389                 defloc = sxml.xpath("//definiendum[text()=\"%s\"]" % (element.text or '').strip())
390             else:
391                 defloc = sxml.xpath("//definiendum[text()='%s']" % (element.text or '').strip())
392             if defloc:
393                 definiens = defloc[0].getnext()
394                 if definiens.tag == 'definiens':
395                     subgen = EduModule(self.options, self.state)
396                     definiens_s = subgen.generate(definiens)
397
398         return u'<cmd name="textbf"><parm>', u"</parm></cmd>: " + definiens_s
399
400     def handle_definiens(self, element):
401         return u"", u""
402
403     def handle_podpis(self, element):
404         return u"""<env name="figure">""", u"</env>"
405
406     def handle_tabela(self, element):
407         max_col = 0
408         for w in element.xpath("wiersz"):
409             ks = w.xpath("kol")
410             if max_col < len(ks):
411                 max_col = len(ks)
412         self.options = {'columnts': max_col}
413         # styling:
414         #     has_frames = int(element.attrib.get("ramki", "0"))
415         #     if has_frames: frames_c = "framed"
416         #     else: frames_c = ""
417         #     return u"""<table class="%s">""" % frames_c, u"</table>"
418         return u'''
419 <cmd name="begin"><parm>tabular</parm><parm>%s</parm></cmd>
420     ''' % ('l' * max_col), u'''<cmd name="end"><parm>tabular</parm></cmd>'''
421
422     @escape(True)
423     def handle_wiersz(self, element):
424         return u"", u'<ctrl ch="\\"/>'
425
426     @escape(True)
427     def handle_kol(self, element):
428         if element.getnext() is not None:
429             return u"", u'<spec cat="align" />'
430         return u"", u""
431
432     def handle_link(self, element):
433         if element.attrib.get('url'):
434             url = element.attrib.get('url')
435             if url == element.text:
436                 return cmd('url')(self, element)
437             else:
438                 return cmd('href', parms=[element.attrib['url']])(self, element)
439         else:
440             return cmd('emph')(self, element)
441
442     def handle_obraz(self, element):
443         frmt = self.options['format']
444         name = element.attrib.get('nazwa', '').strip()
445         image = frmt.get_image(name.strip())
446         name = image.get_filename().rsplit('/', 1)[-1]
447         img_path = "obraz/%s" % name.replace("_", "")
448         frmt.attachments[img_path] = image
449         return cmd("obraz", parms=[img_path])(self)
450
451     def handle_video(self, element):
452         url = element.attrib.get('url')
453         if not url:
454             print '!! <video> missing url'
455             return
456         m = re.match(r'(?:https?://)?(?:www.)?youtube.com/watch\?(?:.*&)?v=([^&]+)(?:$|&)', url)
457         if not m:
458             print '!! unknown <video> url scheme:', url
459             return
460         name = m.group(1)
461         thumb = IOFile.from_string(urlopen("http://img.youtube.com/vi/%s/0.jpg" % name).read())
462         img_path = "video/%s.jpg" % name.replace("_", "")
463         self.options['format'].attachments[img_path] = thumb
464         canon_url = "https://www.youtube.com/watch?v=%s" % name
465         return cmd("video", parms=[img_path, canon_url])(self)
466
467
468 class Exercise(EduModule):
469     def __init__(self, *args, **kw):
470         self.question_counter = 0
471         super(Exercise, self).__init__(*args, **kw)
472         self.piece_counter = None
473
474     handle_rozw_kom = ifoption(teacher=True)(cmd('akap'))
475
476     def handle_cwiczenie(self, element):
477         self.options = {
478             'exercise': element.attrib['typ'],
479             'sub_gen': True,
480         }
481         self.question_counter = 0
482         self.piece_counter = 0
483
484         header = etree.Element("parm")
485         header_cmd = etree.Element("cmd", name="naglowekpodrozdzial")
486         header_cmd.append(header)
487         header.text = u"Zadanie %d." % self.options['exercise_counter']
488
489         pre = etree.tostring(header_cmd, encoding=unicode)
490         post = u""
491         # Add a single <pytanie> tag if it's not there
492         if not element.xpath(".//pytanie"):
493             qpre, qpost = self.handle_pytanie(element)
494             pre += qpre
495             post = qpost + post
496         return pre, post
497
498     def handle_pytanie(self, element):
499         """This will handle <cwiczenie> element, when there is no <pytanie>
500         """
501         self.question_counter += 1
502         self.piece_counter = 0
503         pre = post = u""
504         if self.options['teacher'] and element.attrib.get('rozw'):
505             post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
506         return pre, post
507
508     def handle_punkt(self, element):
509         pre, post = super(Exercise, self).handle_punkt(element)
510         if self.options['teacher'] and element.attrib.get('rozw'):
511             post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
512         return pre, post
513
514     def solution_header(self):
515         par = etree.Element("cmd", name="par")
516         parm = etree.Element("parm")
517         parm.text = u"Rozwiązanie:"
518         par.append(parm)
519         return etree.tostring(par)
520
521     def explicit_solution(self):
522         if self.options['solution']:
523             par = etree.Element("cmd", name="par")
524             parm = etree.Element("parm")
525             parm.text = self.options['solution']
526             par.append(parm)
527             return self.solution_header() + etree.tostring(par)
528
529
530 class Wybor(Exercise):
531     def handle_cwiczenie(self, element):
532         pre, post = super(Wybor, self).handle_cwiczenie(element)
533         is_single_choice = True
534         pytania = element.xpath(".//pytanie")
535         if not pytania:
536             pytania = [element]
537         for p in pytania:
538             solutions = p.xpath(".//punkt[@rozw='prawda']")
539             if len(solutions) != 1:
540                 is_single_choice = False
541                 break
542
543         self.options = {'single': is_single_choice}
544         return pre, post
545
546     def handle_punkt(self, element):
547         if self.options['exercise'] and element.attrib.get('rozw', None):
548             cmd = 'radio' if self.options['single'] else 'checkbox'
549             if self.options['teacher'] and element.attrib['rozw'] == 'prawda':
550                 cmd += 'checked'
551             return u'<cmd name="%s"/>' % cmd, ''
552         else:
553             return super(Wybor, self).handle_punkt(element)
554
555
556 class Uporzadkuj(Exercise):
557     def handle_pytanie(self, element):
558         order_items = element.xpath(".//punkt/@rozw")
559         return super(Uporzadkuj, self).handle_pytanie(element)
560
561
562 class Przyporzadkuj(Exercise):
563     def handle_lista(self, lista):
564         header = etree.Element("parm")
565         header_cmd = etree.Element("cmd", name="par")
566         header_cmd.append(header)
567         if 'nazwa' in lista.attrib:
568             header.text = u"Kategorie:"
569         elif 'cel' in lista.attrib:
570             header.text = u"Elementy do przyporządkowania:"
571         else:
572             header.text = u"Lista:"
573         pre, post = super(Przyporzadkuj, self).handle_lista(lista)
574         pre = etree.tostring(header_cmd, encoding=unicode) + pre
575         return pre, post
576
577
578 class Luki(Exercise):
579     def find_pieces(self, question):
580         return question.xpath(".//luka")
581
582     def solution(self, piece):
583         piece = deepcopy(piece)
584         piece.tail = None
585         sub = EduModule()
586         return sub.generate(piece)
587
588     def handle_pytanie(self, element):
589         qpre, qpost = super(Luki, self).handle_pytanie(element)
590
591         luki = self.find_pieces(element)
592         random.shuffle(luki)
593         self.words = u"<env name='itemize'>%s</env>" % (
594             "".join("<cmd name='item'/>%s" % self.solution(luka) for luka in luki)
595         )
596         return qpre, qpost
597
598     def handle_opis(self, element):
599         return '', self.words
600
601     def handle_luka(self, element):
602         luka = "_" * 10
603         if self.options['teacher']:
604             piece = deepcopy(element)
605             piece.tail = None
606             sub = EduModule()
607             text = sub.generate(piece)
608             luka += u" [rozwiązanie: %s]" % text
609         return luka
610
611
612 class Zastap(Luki):
613     def find_pieces(self, question):
614         return question.xpath(".//zastap")
615
616     def solution(self, piece):
617         return piece.attrib.get('rozw', '')
618
619     def list_header(self):
620         return u"Elementy do wstawienia"
621
622     def handle_zastap(self, element):
623         piece = deepcopy(element)
624         piece.tail = None
625         sub = EduModule()
626         text = sub.generate(piece)
627         if self.options['teacher'] and element.attrib.get('rozw'):
628             text += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
629         return text
630
631
632 class PrawdaFalsz(Exercise):
633     def handle_punkt(self, element):
634         pre, post = super(PrawdaFalsz, self).handle_punkt(element)
635         if 'rozw' in element.attrib:
636             post += u" [Prawda/Fałsz]"
637         return pre, post
638
639
640 def fix_lists(tree):
641     lists = tree.xpath(".//lista")
642     for l in lists:
643         if l.text:
644             p = l.getprevious()
645             if p is not None:
646                 if p.tail is None:
647                     p.tail = ''
648                 p.tail += l.text
649             else:
650                 p = l.getparent()
651                 if p.text is None:
652                     p.text = ''
653                 p.text += l.text
654             l.text = ''
655     return tree
656
657
658 class EduModulePDFFormat(PDFFormat):
659     style = get_resource('res/styles/edumed/pdf/edumed.sty')
660
661     def get_texml(self):
662         substitute_hyphens(self.wldoc.edoc)
663         fix_hanging(self.wldoc.edoc)
664
665         self.attachments = {}
666         edumod = EduModule({
667             "wldoc": self.wldoc,
668             "format": self,
669             "teacher": self.customization.get('teacher'),
670         })
671         texml = edumod.generate(fix_lists(self.wldoc.edoc.getroot())).encode('utf-8')
672
673         open("/tmp/texml.xml", "w").write(texml)
674         return texml
675
676     def get_tex_dir(self):
677         temp = super(EduModulePDFFormat, self).get_tex_dir()
678         shutil.copy(get_resource('res/styles/edumed/logo.png'), temp)
679         for name, iofile in self.attachments.items():
680             iofile.save_as(os.path.join(temp, name))
681         return temp
682
683     def get_image(self, name):
684         return self.wldoc.source.attachments[name]