Merge branch 'edumed' of github.com:fnp/redakcja into edumed
[redakcja.git] / apps / catalogue / management / edumed.py
1 # EduMed auto-tagger
2 # -*- coding: utf-8 -*-
3 import re
4 from slughifi import slughifi
5
6
7 class Tagger(object):
8     def __init__(self, state, lines):
9         self.state = state
10         self.lines = lines
11
12     def spawn(self, cls):
13         return cls(self.state, self.lines)
14
15     def line(self, position):
16         return self.lines[position]
17
18     ignore = [re.compile(r"^[\[][PA][\]] - [^ ]+$")]
19     empty_line = re.compile(r"^\s+$")
20
21     def skip_empty(self, position):
22         while self.line(position) == "" or \
23             self.empty_line.match(self.line(position)) or \
24             filter(lambda r: r.match(self.line(position)),
25                              self.ignore[:]):
26             position += 1
27         return position
28
29     def tag(self, position):
30         """
31 Return None -- means that we can't tag it in any way
32         """
33         return None
34
35     def wrap(self, tagname, content):
36         return u"<%s>%s</%s>" % (tagname, content, tagname)
37
38     @staticmethod
39     def anymatches(regex):
40         return lambda x: regex.match(x)
41
42
43 class Section(Tagger):
44     looks_like = re.compile(r"^[IVX]+[.]\s+(.*)$")
45
46     def __init__(self, *a):
47         super(Section, self).__init__(*a)
48         self.is_podrozdzial = False
49
50     def tag(self, pos):
51         pos2 = self.skip_empty(pos)
52         pos = pos2
53         m = self.looks_like.match(self.line(pos))
54         if m:
55             self.title = m.groups()[0]
56             return pos + 1
57
58     def __unicode__(self):
59         return self.wrap(self.is_podrozdzial and "naglowek_podrozdzial" or "naglowek_rozdzial",
60                          self.title)
61
62
63 class Meta(Tagger):
64     looks_like = re.compile(r"([^:]+): (.*)", re.UNICODE)
65
66     def tag(self, pos):
67         pos = self.skip_empty(pos)
68         m = self.looks_like.match(self.line(pos))
69         if m:
70             k = m.groups()[0]
71             v = m.groups()[1]
72             m = self.state.get('meta', {})
73             m[k] = v
74             self.state['meta'] = m
75             return pos + 1
76
77
78 class Informacje(Tagger):
79     def tag(self, pos):
80         self.title = self.spawn(Section)
81         self.meta = []
82         pos = self.title.tag(pos)
83         if pos is None: return
84
85             # collect meta
86         while True:
87             pos = self.skip_empty(pos)
88             meta = self.spawn(Meta)
89             pos2 = meta.tag(pos)
90             if pos2 is None: break
91             self.meta.append(meta)
92             pos = pos2
93
94         return pos
95
96
97 class List(Tagger):
98     point = re.compile(r"^[\s]*[-*·]{1,2}(.*)")
99     num = re.compile(r"^[\s]*[a-z][.]\s+(.*)")
100
101     def __init__(self, *args):
102
103         super(List, self).__init__(*args)
104         self.items = []
105         self.type = 'punkt'
106
107     def tag(self, pos):
108         while True:
109             l = self.line(pos)
110             m = self.point.match(l)
111             if not m:
112                 m = self.num.match(l)
113                 if m: self.type = 'num'
114             if l and m:
115                 self.items.append(m.groups()[0].lstrip())
116                 pos += 1
117             else:
118                 break
119         if self.items:
120             return pos
121
122     def append(self, tagger):
123         self.items.append(tagger)
124
125     def __unicode__(self):
126         s = '<lista typ="%s">' % self.type
127         for i in self.items:
128             if isinstance(i, list):
129                 x = "\n".join(map(lambda elem: unicode(elem), i))
130             else:
131                 x = unicode(i)
132             s += "\n<punkt>%s</punkt>" % x
133         s += "\n</lista>\n"
134         return s
135
136
137 class Paragraph(Tagger):
138     remove_this = [
139         re.compile(r"[\s]*opis zawarto.ci[\s]*", re.I),
140         re.compile(r"^[\s]*$")
141         ]
142     podrozdzial = [
143         re.compile(r"[\s]*(przebieg zaj..|opcje dodatkowe)[\s]*", re.I),
144         ]
145
146     def tag(self, pos):
147         self.line = self.lines[pos]
148         self.ignore = False
149         self.is_podrozdzial = False
150
151         for x in self.remove_this:
152             if x.match(self.line):
153                 self.ignore = True
154
155         for x in self.podrozdzial:
156             if x.match(self.line):
157                 self.is_podrozdzial = True
158
159         return pos + 1
160
161     def __unicode__(self):
162         if not self.ignore:
163             if self.is_podrozdzial:
164                 tag = 'naglowek_podrozdzial'
165             else:
166                 tag = 'akap'
167             return u"<%s>%s</%s>" % (tag, self.line, tag)
168         else:
169             return u''
170
171
172 class Container:
173     def __init__(self, tag_name, *elems):
174         self.tag_name = tag_name
175         self.elems = elems
176
177     def __unicode__(self):
178         s = u"<%s>" % self.tag_name
179         add_nl = False
180         for e in self.elems:
181             if isinstance(e, (str, unicode)):
182                 s += unicode(e)
183             else:
184                 s += "\n  " + unicode(e)
185                 add_nl = True
186
187         if add_nl: s += "\n"
188         s += u"</%s>" % self.tag_name
189         return s
190
191
192 def eatany(pos, *taggers):
193     try:
194         for t in list(taggers):
195             p = t.tag(pos)
196             if p:
197                 return (t, p)
198     except IndexError:
199         pass
200     return (None, pos)
201
202
203 def eatseq(pos, *taggers):
204     good = []
205     taggers = list(taggers[:])
206     try:
207         while len(taggers):
208             p = taggers[0].tag(pos)
209             if p is None:
210                 return (tuple(good), pos)
211             good.append(taggers.pop(0))
212             # print "%d -> %d" % (pos, p)
213             pos = p
214
215     except IndexError:
216         print "Got index error for pos=%d" % pos
217     return (tuple(good), pos)
218
219
220 def tagger(text, pretty_print=False):
221     """
222 tagger(text) function name and signature is a contract.
223 returns auto-tagged text
224     """
225     if not isinstance(text, unicode):
226         text = unicode(text.decode('utf-8'))
227     lines = text.split("\n")
228     pos = 0
229     content = []
230     state = {}
231     info = Informacje(state, lines)
232
233     ((info,), pos) = eatseq(pos, info)
234
235     # print "[i] %d. %s" % (pos, lines[pos])
236
237     content.append(info)
238
239     while True:
240         x, pos = eatany(pos, info.spawn(Section),
241                         info.spawn(List), info.spawn(Paragraph))
242
243         if x is not None:
244             content.append(x)
245         else:
246             content.append(lines[pos])
247             pos += 1
248             if pos >= len(lines):
249                 break
250
251     return toxml(content, pretty_print=pretty_print)
252
253 dc_fixed = {
254     'description': u'Publikacja zrealizowana w ramach projektu Cyfrowa Przyszłość (http://cyfrowaprzyszlosc.pl).',
255     'relation': u'moduły powiązane linki',
256     'description.material': u'linki do załączników',
257     'rights': u'Creative Commons Uznanie autorstwa - Na tych samych warunkach 3.0',
258     }
259
260
261 class NotFound(Exception):
262     pass
263
264
265 def find_block(content, title_re, begin=-1, end=-1):
266     title_re = re.compile(title_re, re.I | re.UNICODE)
267
268     rb = -1
269     if begin < 0: begin = 0
270     if end < 0: end = len(content)
271
272     for i in range(begin, end):
273         elem = content[i]
274         if isinstance(elem, Paragraph):
275             if title_re.match(elem.line):
276                 rb = i
277                 continue
278         if isinstance(elem, Section):
279             if title_re.match(elem.title):
280                 rb = i
281                 continue
282         if rb >= 0:
283             if isinstance(elem, List):
284                 continue
285             if isinstance(elem, Paragraph) and elem.line:
286                 continue
287             break
288     if rb >= 0:
289         return rb, i
290     raise NotFound()
291
292
293 def remove_block(content, title_re, removed=None):
294     rb, re = find_block(content, title_re)
295     if removed is not None and isinstance(removed, list):
296         removed += content[rb:re][:]
297     content[rb:re] = []
298     return content
299
300
301 def mark_activities(content):
302     i = 0
303     tl = len(content)
304     is_przebieg = re.compile(r"[\s]*przebieg zaj..[\s]*", re.I)
305
306     is_next_section = re.compile(r"^[IVX]+[.]? ")
307     is_activity = re.compile(r"^[0-9]+[.] (.+)")
308
309     is_activity_tools = re.compile(r"^pomoce:[\s]*(.+)")
310     is_activity_work = re.compile(r"^forma pracy:[\s]*(.+)")
311     is_activity_time = re.compile(r"^czas:[\s]*([\d]+).*")
312     activity_props = {
313         'pomoce': is_activity_tools,
314         'forma': is_activity_work,
315         'czas': is_activity_time
316         }
317     activities = []
318
319     in_activities = False
320     ab = -1
321     ae = -1
322     while True:
323         e = content[i]
324         if isinstance(e, Section):
325             if in_activities and \
326                 is_next_section.match(e.title):
327                 in_activities = False
328             
329         if isinstance(e, Paragraph):
330             if not in_activities and \
331                 is_przebieg.match(e.line):
332                 in_activities = True
333
334             if in_activities:
335                 m = is_activity.match(e.line)
336                 if m:
337                     e.line = m.groups()[0]
338                     ab = i
339                 if is_activity_time.match(e.line):
340                     ae = i + 1
341                     activities.append((ab, ae))
342         i += 1
343         if i >= tl: break
344
345     activities.reverse()
346     for ab, ae in activities:
347         act_len = ae - ab
348         info_start = ae
349
350         act_els = []
351         act_els.append(Container("opis", content[ab]))
352         for i in range(ab, ae):
353             e = content[i]
354             if isinstance(e, Paragraph):
355                 for prop, pattern in activity_props.items():
356                     m = pattern.match(e.line)
357                     if m:
358                         act_els.append(Container(prop, m.groups()[0]))
359                         if info_start > i: info_start = i
360         act_els.insert(1, Container('wskazowki',
361                                     *content[ab + 1:info_start]))
362         content[ab:ae] = [Container('aktywnosc', *act_els)]
363     return content
364
365
366 def mark_dictionary(content):
367     db = -1
368     de = -1
369     i = 0
370     is_dictionary = re.compile(r"[\s]*s.owniczek[\s]*", re.I)
371     is_dictentry = re.compile(r"([^-]+) - (.+)")
372     slowniczek = content[0].spawn(List)
373     slowniczek.type = 'slowniczek'
374     while i < len(content):
375         e = content[i]
376         if isinstance(e, Section):
377             if is_dictionary.match(e.title):
378                 db = i + 1
379             elif db >= 1:
380                 de = i
381                 content[db:de] = [slowniczek]
382                 break
383         elif db >= 0:
384             if isinstance(e, Paragraph):
385                 m = is_dictentry.match(e.line)
386                 if m:
387                     slowniczek.append([Container('definiendum', m.groups()[0]),
388                                        Container('definiens', m.groups()[1])])
389
390                 else:
391                     slowniczek.append(e)
392         i += 1
393
394     return content
395
396
397 def move_evaluation(content):
398     evaluation = []
399
400     content = remove_block(content, r"ewaluacja[+ PA\[\].]*", evaluation)
401     if evaluation:
402         #        print "found evaluation %s" % (evaluation,)
403         evaluation[0].is_podrozdzial = True
404         # evaluation place
405         opcje_dodatkowe = find_block(content, r"opcje dodatkowe\s*")
406         if opcje_dodatkowe:
407             #            print "putting evaluation just before opcje dodatkowe @ %s" % (opcje_dodatkowe, )
408             content[opcje_dodatkowe[0]:opcje_dodatkowe[0]] = evaluation
409         else:
410             materialy = find_block(content, r"materia.y[+ AP\[\].]*")
411             if materialy:
412                 #                print "putting evaluation just before materialy @ %s" % (materialy, )
413                 content[materialy[0]:materialy[0]] = evaluation
414             else:
415                 print "er.. no idea where to place evaluation"
416     return content
417
418
419 def toxml(content, pretty_print=False):
420     # some transformations
421     content = mark_activities(content)
422     content = mark_dictionary(content)
423     try:
424         content = remove_block(content, r"wykorzyst(yw)?ane metody[+ PA\[\].]*")
425     except NotFound:
426         pass
427     try:
428         content = remove_block(content, r"(pomoce|potrzebne materia.y)[+ PA\[\]]*")
429     except NotFound:
430         pass
431     content = move_evaluation(content)
432
433     info = content.pop(0)
434
435     state = info.state
436     meta = state['meta']
437     slug = slughifi(meta.get(u'Tytuł modułu', ''))
438     holder = {}
439     holder['xml'] = u""
440
441     def p(t):
442         holder['xml'] += u"%s\n" % t
443
444     def dc(k, v):
445         p(u'<dc:%s xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">%s</dc:%s>' % (k, v, k))
446
447     def t(tag, ct):
448         p(u'<%s>%s</%s>' % (tag, ct, tag))
449
450     def a(ct):
451         if ct:
452             t(u'akap', ct)
453
454     p("<utwor>")
455     p(u'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">')
456     p(u'<rdf:Description rdf:about="http://redakcja.cyfrowaprzyszlosc.pl/documents/">')
457     authors = map(unicode.strip, meta[u'Autorzy'].split(u','))
458     for author in authors:
459         names = author.split(u' ')
460         lastname = names.pop()
461         names.insert(0, lastname + ",")
462         author = u' '.join(names)
463         dc(u'creator', author)
464     dc(u'title', meta.get(u'Tytuł modułu', u''))
465     dc(u'relation.isPartOf', meta.get(u'Dział', u''))
466     dc(u'publisher', u'Fundacja Nowoczesna Polska')
467     dc(u'subject.competence', meta.get(u'Wybrana kompetencja z Katalogu', u''))
468     dc(u'subject.curriculum', meta.get(u'Odniesienie do podstawy programowej', u''))
469     for keyword in meta.get(u'Słowa kluczowe', u'').split(u','):
470         keyword = keyword.strip()
471         dc(u'subject', keyword)
472     dc(u'description', dc_fixed['description'])
473     dc(u'description.material', dc_fixed['description.material'])
474     dc(u'relation', dc_fixed['relation'])
475     dc(u'identifier.url', u'http://cyfrowaprzyszlosc.pl/%s' % slug)
476     dc(u'rights', dc_fixed['rights'])
477     dc(u'rights.license', u'http://creativecommons.org/licenses/by-sa/3.0/')
478     dc(u'format', u'xml')
479     dc(u'type', u'text')
480     dc(u'date', u'2012-11-09')  # TODO
481     dc(u'audience', meta.get(u'Poziom edukacyjny', u''))
482     dc(u'language', u'pol')
483     p(u'</rdf:Description>')
484     p(u'</rdf:RDF>')
485
486     p(u'<powiesc>')
487     t(u'nazwa_utworu', meta.get(u'Tytuł modułu', u''))
488     #    p(u'<nota>')
489     a(u'<!-- Numer porządkowy: %s -->' % meta.get(u'Numer porządkowy', u''))
490     #    p(u'</nota>')
491
492     p(unicode(info.title))
493     for elm in content:
494         if isinstance(elm, unicode) or isinstance(elm, str):
495             a(elm)
496             continue
497         p(unicode(elm))
498
499     p(u'</powiesc>')
500     p(u'</utwor>')
501
502     if pretty_print:
503         from lxml import etree
504         from StringIO import StringIO
505         xml = etree.parse(StringIO(holder['xml']))
506         holder['xml'] = etree.tostring(xml, pretty_print=pretty_print, encoding=unicode)
507
508     return holder['xml']
509
510
511 # TODO / TBD
512 # ogarnąć podrozdziały
513 #  Przebieg zajęć
514 #  opcje dodatkowe
515 # usunąć 'opis zawartości'
516 # akapit łączony?