fix parent PDF creation, add test
[wolnelektury.git] / apps / lesmianator / models.py
1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 #
5 import cPickle
6 from datetime import datetime
7 from random import randint
8 from StringIO import StringIO
9
10 from django.core.files.base import ContentFile
11 from django.db import models
12 from django.db.models import permalink
13 from django.utils.translation import ugettext_lazy as _
14 from django.core.urlresolvers import reverse
15 from django.db.models.signals import m2m_changed
16 from django.contrib.auth.models import User
17 from django.contrib.contenttypes.models import ContentType
18 from django.contrib.contenttypes import generic
19 from django.conf import settings
20
21 from librarian import text
22 from catalogue.fields import JSONField
23 from catalogue.models import Book, Tag
24
25
26 class Poem(models.Model):
27     slug = models.SlugField(_('slug'), max_length=120, db_index=True)
28     text = models.TextField(_('text'))
29     created_by = models.ForeignKey(User, null=True)
30     created_from = JSONField(_('extra information'), null=True, blank=True)
31     created_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False)
32     seen_at = models.DateTimeField(_('last view date'), auto_now_add=True, editable=False)
33     view_count = models.IntegerField(_('view count'), default=1)
34
35     try:
36         f = open(settings.LESMIANATOR_PICKLE)
37         global_dictionary = cPickle.load(f)
38         f.close()
39     except:
40         global_dictionary = {}
41
42     def visit(self):
43         self.view_count += 1
44         self.seen_at = datetime.now()
45         self.save()
46
47     def __unicode__(self):
48         return "%s (%s...)" % (self.slug, self.text[:20])
49
50     @staticmethod
51     def choose_letter(word, continuations):
52         if word not in continuations:
53             return u'\n'
54
55         choices = sum((continuations[word][letter]
56                        for letter in continuations[word]))
57         r = randint(0, choices - 1)
58
59         for letter in continuations[word]:
60             r -= continuations[word][letter]
61             if r < 0:
62                 return letter
63
64     @classmethod
65     def write(cls, continuations=None, length=3, min_lines=2, maxlen=1000):
66         if continuations is None:
67             continuations = cls.global_dictionary
68         if not continuations:
69             return ''
70
71         letters = []
72         word = u''
73
74         finished_stanza_verses = 0
75         current_stanza_verses = 0
76         verse_start = True
77
78         char_count = 0
79
80         # do `min_lines' non-empty verses and then stop,
81         # but let Lesmianator finish his last stanza.
82         while finished_stanza_verses < min_lines and char_count < maxlen:
83             letter = cls.choose_letter(word, continuations)
84             letters.append(letter)
85             word = word[-length + 1:] + letter
86             char_count += 1
87
88             if letter == u'\n':
89                 if verse_start:
90                     finished_stanza_verses += current_stanza_verses
91                     current_stanza_verses = 0
92                 else:
93                     current_stanza_verses += 1
94                     verse_start = True
95             else:
96                 verse_start = False
97
98         return ''.join(letters).strip()
99
100     def get_absolute_url(self):
101         return reverse('get_poem', kwargs={'poem': self.slug})
102
103
104 class Continuations(models.Model):
105     pickle = models.FileField(_('Continuations file'), upload_to='lesmianator')
106     content_type = models.ForeignKey(ContentType)
107     object_id = models.PositiveIntegerField()
108     content_object = generic.GenericForeignKey('content_type', 'object_id')
109
110     class Meta:
111         unique_together = (('content_type', 'object_id'), )
112
113     def __unicode__(self):
114         return "Continuations for: %s" % unicode(self.content_object)
115
116     @staticmethod
117     def join_conts(a, b):
118         for pre in b:
119             a.setdefault(pre, {})
120             for post in b[pre]:
121                 a[pre].setdefault(post, 0)
122                 a[pre][post] += b[pre][post]
123         return a
124
125     @classmethod
126     def for_book(cls, book, length=3):
127         # count from this book only
128         output = StringIO()
129         f = open(book.xml_file.path)
130         text.transform(f, output, False, ('raw-text',))
131         f.close()
132         conts = {}
133         last_word = ''
134         for letter in output.getvalue().decode('utf-8').strip().lower():
135             mydict = conts.setdefault(last_word, {})
136             mydict.setdefault(letter, 0)
137             mydict[letter] += 1
138             last_word = last_word[-length+1:] + letter
139         # add children
140         return reduce(cls.join_conts, 
141                       (cls.get(child) for child in book.children.all()),
142                       conts)
143
144     @classmethod
145     def for_set(cls, tag):
146         # book contains its descendants, we don't want them twice
147         books = Book.tagged.with_any((tag,))
148         l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in books])
149         descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)]
150         if descendants_keys:
151             books = books.exclude(pk__in=descendants_keys)
152
153         cont_tabs = (cls.get(b) for b in books)
154         return reduce(cls.join_conts, cont_tabs)
155
156     @classmethod
157     def get(cls, sth):
158         object_type = ContentType.objects.get_for_model(sth)
159         should_keys = set([sth.id])
160         if isinstance(sth, Tag):
161             should_keys = set(b.pk for b in Book.tagged.with_any((sth,)))
162         try:
163             obj = cls.objects.get(content_type=object_type, object_id=sth.id)
164             if not obj.pickle:
165                 raise cls.DoesNotExist
166             f = open(obj.pickle.path)
167             keys, conts = cPickle.load(f)
168             f.close()
169             if set(keys) != should_keys:
170                 raise cls.DoesNotExist
171             return conts
172         except cls.DoesNotExist:
173             if isinstance(sth, Book):
174                 conts = cls.for_book(sth)
175             elif isinstance(sth, Tag):
176                 conts = cls.for_set(sth)
177             else:
178                 raise NotImplemented('Lesmianator continuations: only Book and Tag supported')
179
180             c, created = cls.objects.get_or_create(content_type=object_type, object_id=sth.id)
181             c.pickle.save(sth.slug+'.p', ContentFile(cPickle.dumps((should_keys, conts))))
182             c.save()
183             return conts
184
185