curriculum in pdf footer
[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 = re.split(r"[, ]+", p.attrib.get('rozw', ''))
539             if len(solutions) != 1:
540                 is_single_choice = False
541                 break
542             choices = p.xpath(".//*[@nazwa]")
543             uniq = set()
544             for n in choices:
545                 uniq.add(n.attrib.get('nazwa', ''))
546             if len(choices) != len(uniq):
547                 is_single_choice = False
548                 break
549
550         self.options = {'single': is_single_choice}
551         return pre, post
552
553     def handle_punkt(self, element):
554         if self.options['exercise'] and element.attrib.get('nazwa', None):
555             cmd = 'radio' if self.options['single'] else 'checkbox'
556             return u'<cmd name="%s"/>' % cmd, ''
557         else:
558             return super(Wybor, self).handle_punkt(element)
559
560
561 class Uporzadkuj(Exercise):
562     def handle_pytanie(self, element):
563         order_items = element.xpath(".//punkt/@rozw")
564         return super(Uporzadkuj, self).handle_pytanie(element)
565
566
567 class Przyporzadkuj(Exercise):
568     def handle_lista(self, lista):
569         header = etree.Element("parm")
570         header_cmd = etree.Element("cmd", name="par")
571         header_cmd.append(header)
572         if 'nazwa' in lista.attrib:
573             header.text = u"Kategorie:"
574         elif 'cel' in lista.attrib:
575             header.text = u"Elementy do przyporządkowania:"
576         else:
577             header.text = u"Lista:"
578         pre, post = super(Przyporzadkuj, self).handle_lista(lista)
579         pre = etree.tostring(header_cmd, encoding=unicode) + pre
580         return pre, post
581
582
583 class Luki(Exercise):
584     def find_pieces(self, question):
585         return question.xpath(".//luka")
586
587     def solution(self, piece):
588         piece = deepcopy(piece)
589         piece.tail = None
590         sub = EduModule()
591         return sub.generate(piece)
592
593     def handle_pytanie(self, element):
594         qpre, qpost = super(Luki, self).handle_pytanie(element)
595
596         luki = self.find_pieces(element)
597         random.shuffle(luki)
598         self.words = u"<env name='itemize'>%s</env>" % (
599             "".join("<cmd name='item'/>%s" % self.solution(luka) for luka in luki)
600         )
601         return qpre, qpost
602
603     def handle_opis(self, element):
604         return '', self.words
605
606     def handle_luka(self, element):
607         luka = "_" * 10
608         if self.options['teacher']:
609             piece = deepcopy(element)
610             piece.tail = None
611             sub = EduModule()
612             text = sub.generate(piece)
613             luka += u" [rozwiązanie: %s]" % text
614         return luka
615
616
617 class Zastap(Luki):
618     def find_pieces(self, question):
619         return question.xpath(".//zastap")
620
621     def solution(self, piece):
622         return piece.attrib.get('rozw', '')
623
624     def list_header(self):
625         return u"Elementy do wstawienia"
626
627     def handle_zastap(self, element):
628         piece = deepcopy(element)
629         piece.tail = None
630         sub = EduModule()
631         text = sub.generate(piece)
632         if self.options['teacher'] and element.attrib.get('rozw'):
633             text += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
634         return text
635
636
637 class PrawdaFalsz(Exercise):
638     def handle_punkt(self, element):
639         pre, post = super(PrawdaFalsz, self).handle_punkt(element)
640         if 'rozw' in element.attrib:
641             post += u" [Prawda/Fałsz]"
642         return pre, post
643
644
645 def fix_lists(tree):
646     lists = tree.xpath(".//lista")
647     for l in lists:
648         if l.text:
649             p = l.getprevious()
650             if p is not None:
651                 if p.tail is None:
652                     p.tail = ''
653                 p.tail += l.text
654             else:
655                 p = l.getparent()
656                 if p.text is None:
657                     p.text = ''
658                 p.text += l.text
659             l.text = ''
660     return tree
661
662
663 class EduModulePDFFormat(PDFFormat):
664     style = get_resource('res/styles/edumed/pdf/edumed.sty')
665
666     def get_texml(self):
667         substitute_hyphens(self.wldoc.edoc)
668         fix_hanging(self.wldoc.edoc)
669
670         self.attachments = {}
671         edumod = EduModule({
672             "wldoc": self.wldoc,
673             "format": self,
674             "teacher": self.customization.get('teacher'),
675         })
676         texml = edumod.generate(fix_lists(self.wldoc.edoc.getroot())).encode('utf-8')
677
678         open("/tmp/texml.xml", "w").write(texml)
679         return texml
680
681     def get_tex_dir(self):
682         temp = super(EduModulePDFFormat, self).get_tex_dir()
683         shutil.copy(get_resource('res/styles/edumed/logo.png'), temp)
684         for name, iofile in self.attachments.items():
685             iofile.save_as(os.path.join(temp, name))
686         return temp
687
688     def get_image(self, name):
689         return self.wldoc.source.attachments[name]