publish log + some fixes
[redakcja.git] / apps / catalogue / models.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
5 #
6 from django.contrib.auth.models import User
7 from django.core.urlresolvers import reverse
8 from django.db import models
9 from django.utils.translation import ugettext_lazy as _
10
11 from dvcs import models as dvcs_models
12 from catalogue.xml_tools import compile_text
13
14 import logging
15 logger = logging.getLogger("fnp.catalogue")
16
17
18 class Book(models.Model):
19     """ A document edited on the wiki """
20
21     title = models.CharField(_('title'), max_length=255, db_index=True)
22     slug = models.SlugField(_('slug'), max_length=128, unique=True, db_index=True)
23     gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True)
24
25     parent = models.ForeignKey('self', null=True, blank=True, verbose_name=_('parent'), related_name="children")
26     parent_number = models.IntegerField(_('parent number'), null=True, blank=True, db_index=True)
27
28     class NoTextError(BaseException):
29         pass
30
31     class Meta:
32         ordering = ['parent_number', 'title']
33         verbose_name = _('book')
34         verbose_name_plural = _('books')
35         permissions = [('can_pubmark', 'Can mark for publishing')]
36
37     def __unicode__(self):
38         return self.title
39
40     def get_absolute_url(self):
41         return reverse("catalogue_book", args=[self.slug])
42
43     @classmethod
44     def create(cls, creator=None, text=u'', *args, **kwargs):
45         """
46             >>> Book.create(slug='x', text='abc').materialize()
47             'abc'
48         """
49         instance = cls(*args, **kwargs)
50         instance.save()
51         instance[0].commit(author=creator, text=text)
52         return instance
53
54     def __iter__(self):
55         return iter(self.chunk_set.all())
56
57     def __getitem__(self, chunk):
58         return self.chunk_set.all()[chunk]
59
60     def __len__(self):
61         return self.chunk_set.count()
62
63     def __nonzero__(self):
64         """
65             Necessary so that __len__ isn't used for bool evaluation.
66         """
67         return True
68
69     def get_current_changes(self, publishable=True):
70         """
71             Returns a list containing one Change for every Chunk in the Book.
72             Takes the most recent revision (publishable, if set).
73             Throws an error, if a proper revision is unavailable for a Chunk.
74         """
75         if publishable:
76             changes = [chunk.publishable() for chunk in self]
77         else:
78             changes = [chunk.head for chunk in self]
79         if None in changes:
80             raise self.NoTextError('Some chunks have no available text.')
81         return changes
82
83     def materialize(self, publishable=False, changes=None):
84         """ 
85             Get full text of the document compiled from chunks.
86             Takes the current versions of all texts
87             or versions most recently tagged for publishing,
88             or a specified iterable changes.
89         """
90         if changes is None:
91             changes = self.get_current_changes(publishable)
92         return compile_text(change.materialize() for change in changes)
93
94     def publishable(self):
95         if not self.chunk_set.exists():
96             return False
97         for chunk in self:
98             if not chunk.publishable():
99                 return False
100         return True
101
102     def publish(self, user):
103         """
104             Publishes a book on behalf of a (local) user.
105         """
106         from apiclient import api_call
107
108         changes = self.get_current_changes(publishable=True)
109         book_xml = book.materialize(changes=changes)
110         #api_call(user, "books", {"book_xml": book_xml})
111         # record the publish
112         br = BookPublishRecord.objects.create(book=self, user=user)
113         for c in changes:
114             ChunkPublishRecord.objects.create(book_record=br, change=c)
115
116     def make_chunk_slug(self, proposed):
117         """ 
118             Finds a chunk slug not yet used in the book.
119         """
120         slugs = set(c.slug for c in self)
121         i = 1
122         new_slug = proposed
123         while new_slug in slugs:
124             new_slug = "%s-%d" % (proposed, i)
125             i += 1
126         return new_slug
127
128     def append(self, other):
129         number = self[len(self) - 1].number + 1
130         single = len(other) == 1
131         for chunk in other:
132             # move chunk to new book
133             chunk.book = self
134             chunk.number = number
135
136             # try some title guessing
137             if other.title.startswith(self.title):
138                 other_title_part = other.title[len(self.title):].lstrip(' /')
139             else:
140                 other_title_part = other.title
141
142             if single:
143                 # special treatment for appending one-parters:
144                 # just use the guessed title and original book slug
145                 chunk.comment = other_title_part
146                 if other.slug.startswith(self.slug):
147                     chunk_slug = other.slug[len(self.slug):].lstrip('-_')
148                 else:
149                     chunk_slug = other.slug
150                 chunk.slug = self.make_chunk_slug(chunk_slug)
151             else:
152                 chunk.comment = "%s, %s" % (other_title_part, chunk.comment)
153                 chunk.slug = self.make_chunk_slug(chunk.slug)
154             chunk.save()
155             number += 1
156         other.delete()
157
158     @staticmethod
159     def listener_create(sender, instance, created, **kwargs):
160         if created:
161             instance.chunk_set.create(number=1, slug='1')
162
163 models.signals.post_save.connect(Book.listener_create, sender=Book)
164
165
166 class Chunk(dvcs_models.Document):
167     """ An editable chunk of text. Every Book text is divided into chunks. """
168
169     book = models.ForeignKey(Book, editable=False)
170     number = models.IntegerField()
171     slug = models.SlugField()
172     comment = models.CharField(max_length=255, blank=True)
173
174     class Meta:
175         unique_together = [['book', 'number'], ['book', 'slug']]
176         ordering = ['number']
177
178     def __unicode__(self):
179         return "%d-%d: %s" % (self.book_id, self.number, self.comment)
180
181     def get_absolute_url(self):
182         return reverse("wiki_editor", args=[self.book.slug, self.slug])
183
184     @classmethod
185     def get(cls, slug, chunk=None):
186         if chunk is None:
187             return cls.objects.get(book__slug=slug, number=1)
188         else:
189             return cls.objects.get(book__slug=slug, slug=chunk)
190
191     def pretty_name(self, book_length=None):
192         title = self.book.title
193         if self.comment:
194             title += ", %s" % self.comment
195         if book_length > 1:
196             title += " (%d/%d)" % (self.number, book_length)
197         return title
198
199     def split(self, slug, comment='', creator=None):
200         """ Create an empty chunk after this one """
201         self.book.chunk_set.filter(number__gt=self.number).update(
202                 number=models.F('number')+1)
203         new_chunk = self.book.chunk_set.create(number=self.number+1,
204                 creator=creator, slug=slug, comment=comment)
205         return new_chunk
206
207     @staticmethod
208     def listener_saved(sender, instance, created, **kwargs):
209         if instance.book:
210             # save book so that its _list_html is reset
211             instance.book.save()
212
213 models.signals.post_save.connect(Chunk.listener_saved, sender=Chunk)
214
215
216 class BookPublishRecord(models.Model):
217     """
218         A record left after publishing a Book.
219     """
220
221     book = models.ForeignKey(Book)
222     timestamp = models.DateTimeField(auto_now_add=True)
223     user = models.ForeignKey(User)
224
225     class Meta:
226         ordering = ['-timestamp']
227
228
229 class ChunkPublishRecord(models.Model):
230     """
231         BookPublishRecord details for each Chunk.
232     """
233
234     book_record = models.ForeignKey(BookPublishRecord)
235     change = models.ForeignKey(Chunk.change_model)