fixes for autotagger
[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]*[0-9a-z]{1,2}[.]\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 def find_block(content, title_re, begin=-1, end=-1):
262     title_re = re.compile(title_re, re.I | re.UNICODE)
263     print "looking for %s" % title_re.pattern
264     if title_re.pattern[0:6] == 'pomoce':
265         import pdb; pdb.set_trace()
266
267     rb = -1
268     if begin < 0: begin = 0
269     if end < 0: end = len(content)
270
271     for i in range(begin, end):
272         elem = content[i]
273         if isinstance(elem, Paragraph):
274             if title_re.match(elem.line):
275                 rb = i
276                 continue
277         if isinstance(elem, Section):
278             if title_re.match(elem.title):
279                 rb = i
280                 continue
281         if rb >= 0:
282             if isinstance(elem, List):
283                 continue
284             if isinstance(elem, Paragraph) and elem.line:
285                 continue
286             break
287     if rb >= 0:
288         return rb, i
289
290
291 def remove_block(content, title_re, removed=None):
292     rb, re = find_block(content, title_re)
293
294     if removed is not None and isinstance(removed, list):
295         removed += content[rb:re][:]
296     content[rb:re] = []
297     return content
298
299
300 def mark_activities(content):
301     i = 0
302     tl = len(content)
303     is_przebieg = re.compile(r"[\s]*przebieg zaj..[\s]*", re.I)
304
305     is_next_section = re.compile(r"^[IVX]+[.]? ")
306     is_activity = re.compile(r"^[0-9]+[.]? (.+)")
307
308     is_activity_tools = re.compile(r"^pomoce:[\s]*(.+)")
309     is_activity_work = re.compile(r"^forma pracy:[\s]*(.+)")
310     is_activity_time = re.compile(r"^czas:[\s]*([\d]+).*")
311     activity_props = {
312         'pomoce': is_activity_tools,
313         'forma': is_activity_work,
314         'czas': is_activity_time
315         }
316     activities = []
317
318     in_activities = False
319     ab = -1
320     ae = -1
321     while True:
322         e = content[i]
323         if isinstance(e, Paragraph):
324             if not in_activities and \
325                 is_przebieg.match(e.line):
326                 in_activities = True
327
328             if in_activities and \
329                 is_next_section.match(e.line):
330                 in_activities = False
331             if in_activities:
332                 m = is_activity.match(e.line)
333                 if m:
334                     e.line = m.groups()[0]
335                     ab = i
336                 if is_activity_time.match(e.line):
337                     ae = i + 1
338                     activities.append((ab, ae))
339         i += 1
340         if i >= tl: break
341
342     activities.reverse()
343     for ab, ae in activities:
344         act_len = ae - ab
345         info_start = ae
346
347         act_els = []
348         act_els.append(Container("opis", content[ab]))
349         for i in range(ab, ae):
350             e = content[i]
351             if isinstance(e, Paragraph):
352                 for prop, pattern in activity_props.items():
353                     m = pattern.match(e.line)
354                     if m:
355                         act_els.append(Container(prop, m.groups()[0]))
356                         if info_start > i: info_start = i
357         act_els.insert(1, Container('wskazowki',
358                                     *content[ab + 1:info_start]))
359         content[ab:ae] = [Container('aktywnosc', *act_els)]
360     return content
361
362
363 def mark_dictionary(content):
364     db = -1
365     de = -1
366     i = 0
367     is_dictionary = re.compile(r"[\s]*s.owniczek[\s]*", re.I)
368     is_dictentry = re.compile(r"([^-]+) - (.+)")
369     slowniczek = content[0].spawn(List)
370     slowniczek.type = 'slowniczek'
371     while i < len(content):
372         e = content[i]
373         if isinstance(e, Section):
374             if is_dictionary.match(e.title):
375                 db = i + 1
376             elif db >= 1:
377                 de = i
378                 content[db:de] = [slowniczek]
379                 break
380         elif db >= 0:
381             if isinstance(e, Paragraph):
382                 m = is_dictentry.match(e.line)
383                 if m:
384                     slowniczek.append([Container('definiendum', m.groups()[0]),
385                                        Container('definiens', m.groups()[1])])
386
387                 else:
388                     slowniczek.append(e)
389         i += 1
390
391     return content
392
393
394 def move_evaluation(content):
395     evaluation = []
396
397     content = remove_block(content, r"ewaluacja[+ PA\[\].]*", evaluation)
398     if evaluation:
399         #        print "found evaluation %s" % (evaluation,)
400         evaluation[0].is_podrozdzial = True
401         # evaluation place
402         opcje_dodatkowe = find_block(content, r"opcje dodatkowe\s*")
403         if opcje_dodatkowe:
404             #            print "putting evaluation just before opcje dodatkowe @ %s" % (opcje_dodatkowe, )
405             content[opcje_dodatkowe[0]:opcje_dodatkowe[0]] = evaluation
406         else:
407             materialy = find_block(content, r"materia.y[+ AP\[\].]*")
408             if materialy:
409                 #                print "putting evaluation just before materialy @ %s" % (materialy, )
410                 content[materialy[0]:materialy[0]] = evaluation
411             else:
412                 print "er.. no idea where to place evaluation"
413     return content
414
415
416 def toxml(content, pretty_print=False):
417     # some transformations
418     content = mark_activities(content)
419     content = mark_dictionary(content)
420     content = remove_block(content, r"wykorzyst(yw)?ane metody[+ PA\[\].]*")
421     content = remove_block(content, r"(pomoce|potrzebne materia.y)[+ PA\[\]]*")
422     content = move_evaluation(content)
423
424     info = content.pop(0)
425
426     state = info.state
427     meta = state['meta']
428     slug = slughifi(meta.get(u'Tytuł modułu', ''))
429     holder = {}
430     holder['xml'] = u""
431
432     def p(t):
433         holder['xml'] += u"%s\n" % t
434
435     def dc(k, v):
436         p(u'<dc:%s xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">%s</dc:%s>' % (k, v, k))
437
438     def t(tag, ct):
439         p(u'<%s>%s</%s>' % (tag, ct, tag))
440
441     def a(ct):
442         if ct:
443             t(u'akap', ct)
444
445     p("<utwor>")
446     p(u'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">')
447     p(u'<rdf:Description rdf:about="http://redakcja.cyfrowaprzyszlosc.pl/documents/">')
448     authors = map(unicode.strip, meta[u'Autorzy'].split(u','))
449     for author in authors:
450         names = author.split(u' ')
451         lastname = names.pop()
452         names.insert(0, lastname + ",")
453         author = u' '.join(names)
454         dc(u'creator', author)
455     dc(u'title', meta.get(u'Tytuł modułu', u''))
456     dc(u'relation.isPartOf', meta.get(u'Dział', u''))
457     dc(u'publisher', u'Fundacja Nowoczesna Polska')
458     dc(u'subject.competence', meta.get(u'Wybrana kompetencja z Katalogu', u''))
459     dc(u'subject.curriculum', meta.get(u'Odniesienie do podstawy programowej', u''))
460     for keyword in meta.get(u'Słowa kluczowe', u'').split(u','):
461         keyword = keyword.strip()
462         dc(u'subject', keyword)
463     dc(u'description', dc_fixed['description'])
464     dc(u'description.material', dc_fixed['description.material'])
465     dc(u'relation', dc_fixed['relation'])
466     dc(u'identifier.url', u'http://cyfrowaprzyszlosc.pl/%s' % slug)
467     dc(u'rights', dc_fixed['rights'])
468     dc(u'rights.license', u'http://creativecommons.org/licenses/by-sa/3.0/')
469     dc(u'format', u'xml')
470     dc(u'type', u'text')
471     dc(u'date', u'2012-11-09')  # TODO
472     dc(u'audience', meta.get(u'Poziom edukacyjny', u''))
473     dc(u'language', u'pol')
474     p(u'</rdf:Description>')
475     p(u'</rdf:RDF>')
476
477     p(u'<powiesc>')
478     t(u'nazwa_utworu', meta.get(u'Tytuł modułu', u''))
479     #    p(u'<nota>')
480     a(u'<!-- Numer porządkowy: %s -->' % meta.get(u'Numer porządkowy', u''))
481     #    p(u'</nota>')
482
483     p(unicode(info.title))
484     for elm in content:
485         if isinstance(elm, unicode) or isinstance(elm, str):
486             a(elm)
487             continue
488         p(unicode(elm))
489
490     p(u'</powiesc>')
491     p(u'</utwor>')
492
493     if pretty_print:
494         from lxml import etree
495         from StringIO import StringIO
496         xml = etree.parse(StringIO(holder['xml']))
497         holder['xml'] = etree.tostring(xml, pretty_print=pretty_print, encoding=unicode)
498
499     return holder['xml']
500
501
502 # TODO / TBD
503 # ogarnąć podrozdziały
504 #  Przebieg zajęć
505 #  opcje dodatkowe
506 # usunąć 'opis zawartości'
507 # akapit łączony?