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