Cleaning: timezone issues, deprecated urls.py imports, missing notes.
[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.timezone import utc
14 from django.utils.translation import ugettext_lazy as _
15 from django.core.urlresolvers import reverse
16 from django.db.models.signals import m2m_changed
17 from django.contrib.auth.models import User
18 from django.contrib.contenttypes.models import ContentType
19 from django.contrib.contenttypes import generic
20 from django.conf import settings
21
22 from jsonfield 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.utcnow().replace(tzinfo=utc)
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         wldoc = book.wldocument(parse_dublincore=False)
130         output = wldoc.as_text(('raw-text',)).get_string()
131         del wldoc
132
133         conts = {}
134         last_word = ''
135         for letter in output.decode('utf-8').strip().lower():
136             mydict = conts.setdefault(last_word, {})
137             mydict.setdefault(letter, 0)
138             mydict[letter] += 1
139             last_word = last_word[-length+1:] + letter
140         # add children
141         return reduce(cls.join_conts, 
142                       (cls.get(child) for child in book.children.all().iterator()),
143                       conts)
144
145     @classmethod
146     def for_set(cls, tag):
147         # book contains its descendants, we don't want them twice
148         books = Book.tagged.with_any((tag,))
149         l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in books.iterator()])
150         descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags).iterator()]
151         if descendants_keys:
152             books = books.exclude(pk__in=descendants_keys)
153
154         cont_tabs = (cls.get(b) for b in books.iterator())
155         return reduce(cls.join_conts, cont_tabs)
156
157     @classmethod
158     def get(cls, sth):
159         object_type = ContentType.objects.get_for_model(sth)
160         should_keys = set([sth.id])
161         if isinstance(sth, Tag):
162             should_keys = set(b.pk for b in Book.tagged.with_any((sth,)).iterator())
163         try:
164             obj = cls.objects.get(content_type=object_type, object_id=sth.id)
165             if not obj.pickle:
166                 raise cls.DoesNotExist
167             f = open(obj.pickle.path)
168             keys, conts = cPickle.load(f)
169             f.close()
170             if set(keys) != should_keys:
171                 raise cls.DoesNotExist
172             return conts
173         except cls.DoesNotExist:
174             if isinstance(sth, Book):
175                 conts = cls.for_book(sth)
176             elif isinstance(sth, Tag):
177                 conts = cls.for_set(sth)
178             else:
179                 raise NotImplemented('Lesmianator continuations: only Book and Tag supported')
180
181             c, created = cls.objects.get_or_create(content_type=object_type, object_id=sth.id)
182             c.pickle.save(sth.slug+'.p', ContentFile(cPickle.dumps((should_keys, conts))))
183             c.save()
184             return conts
185
186