search
[wolnelektury.git] / src / lesmianator / models.py
1 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
3 #
4 from functools import reduce
5 import pickle
6 from pickle import PickleError
7 from datetime import datetime
8 from random import randint
9
10 from django.core.files.base import ContentFile
11 from django.db import models
12 from django.utils.timezone import utc
13 from django.contrib.auth.models import User
14 from django.contrib.contenttypes.models import ContentType
15 from django.contrib.contenttypes.fields import GenericForeignKey
16 from django.conf import settings
17 from django.urls import reverse
18
19 from catalogue.models import Book
20 from social.models import UserList
21
22
23 class Poem(models.Model):
24     slug = models.SlugField('slug', max_length=120, db_index=True)
25     text = models.TextField('tekst')
26     created_by = models.ForeignKey(User, models.SET_NULL, null=True)
27     created_from = models.TextField('dodatkowe informacje', null=True, blank=True)
28     created_at = models.DateTimeField('data utworzenia', auto_now_add=True, editable=False)
29     seen_at = models.DateTimeField('data ostatniego obejrzenia', auto_now_add=True, editable=False)
30     view_count = models.IntegerField('licznik obejrzeń', default=1)
31
32     try:
33         f = open(settings.LESMIANATOR_PICKLE, 'rb')
34         global_dictionary = pickle.load(f)
35         f.close()
36     except (IOError, AttributeError, PickleError):
37         global_dictionary = {}
38
39     def visit(self):
40         self.view_count += 1
41         self.seen_at = datetime.utcnow().replace(tzinfo=utc)
42         self.save()
43
44     def __str__(self):
45         return "%s (%s...)" % (self.slug, self.text[:20])
46
47     @staticmethod
48     def choose_letter(word, continuations):
49         if word not in continuations:
50             return '\n'
51
52         choices = sum((continuations[word][letter]
53                        for letter in continuations[word]))
54         r = randint(0, choices - 1)
55
56         for letter in continuations[word]:
57             r -= continuations[word][letter]
58             if r < 0:
59                 return letter
60
61     @classmethod
62     def write(cls, continuations=None, length=3, min_lines=2, maxlen=1000):
63         if continuations is None:
64             continuations = cls.global_dictionary
65         if not continuations:
66             return ''
67
68         letters = []
69         word = ''
70
71         finished_stanza_verses = 0
72         current_stanza_verses = 0
73         verse_start = True
74
75         char_count = 0
76
77         # do `min_lines' non-empty verses and then stop,
78         # but let Lesmianator finish his last stanza.
79         while finished_stanza_verses < min_lines and char_count < maxlen:
80             letter = cls.choose_letter(word, continuations)
81             letters.append(letter)
82             word = word[-length + 1:] + letter
83             char_count += 1
84
85             if letter == '\n':
86                 if verse_start:
87                     finished_stanza_verses += current_stanza_verses
88                     current_stanza_verses = 0
89                 else:
90                     current_stanza_verses += 1
91                     verse_start = True
92             else:
93                 verse_start = False
94
95         return ''.join(letters).strip()
96
97     def get_absolute_url(self):
98         return reverse('get_poem', kwargs={'poem': self.slug})
99
100
101 class Continuations(models.Model):
102     pickle = models.FileField('plik kontynuacji', upload_to='lesmianator')
103     content_type = models.ForeignKey(ContentType, models.CASCADE)
104     object_id = models.PositiveIntegerField()
105     content_object = GenericForeignKey('content_type', 'object_id')
106
107     class Meta:
108         unique_together = (('content_type', 'object_id'), )
109
110     def __str__(self):
111         return "Continuations for: %s" % str(self.content_object)
112
113     @staticmethod
114     def join_conts(a, b):
115         for pre in b:
116             a.setdefault(pre, {})
117             for post in b[pre]:
118                 a[pre].setdefault(post, 0)
119                 a[pre][post] += b[pre][post]
120         return a
121
122     @classmethod
123     def for_book(cls, book, length=3):
124         # count from this book only
125         wldoc = book.wldocument(parse_dublincore=False)
126         output = wldoc.as_text(('raw-text',)).get_bytes()
127         del wldoc
128
129         conts = {}
130         last_word = ''
131         for letter in output.decode('utf-8').strip().lower():
132             mydict = conts.setdefault(last_word, {})
133             mydict.setdefault(letter, 0)
134             mydict[letter] += 1
135             last_word = last_word[-length+1:] + letter
136         # add children
137         return reduce(cls.join_conts,
138                       (cls.get(child) for child in book.children.all().iterator()),
139                       conts)
140
141     @classmethod
142     def for_userlist(cls, ul):
143         cont_tabs = (cls.get(b) for b in ul.get_books())
144         return reduce(cls.join_conts, cont_tabs)
145
146     @classmethod
147     def get(cls, sth):
148         object_type = ContentType.objects.get_for_model(sth)
149         should_keys = {sth.id}
150         if isinstance(sth, UserList):
151             should_keys = set(b.pk for b in sth.get_books())
152         try:
153             obj = cls.objects.get(content_type=object_type, object_id=sth.id)
154             if not obj.pickle:
155                 raise cls.DoesNotExist
156             f = open(obj.pickle.path, 'rb')
157             keys, conts = pickle.load(f)
158             f.close()
159             if set(keys) != should_keys:
160                 raise cls.DoesNotExist
161             return conts
162         except cls.DoesNotExist:
163             if isinstance(sth, Book):
164                 conts = cls.for_book(sth)
165             elif isinstance(sth, UserList):
166                 conts = cls.for_userlist(sth)
167             else:
168                 raise NotImplementedError('Lesmianator continuations: only Book and Tag supported')
169
170             c, created = cls.objects.get_or_create(content_type=object_type, object_id=sth.id)
171             c.pickle.save(sth.slug+'.p', ContentFile(pickle.dumps((should_keys, conts))))
172             c.save()
173             return conts