document list
[redakcja.git] / apps / catalogue / models.py
index ff3d434..69d0a0d 100644 (file)
@@ -3,12 +3,16 @@
 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
+from django.contrib.auth.models import User
 from django.core.urlresolvers import reverse
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
+from django.db.utils import IntegrityError
+
+from slughifi import slughifi
 
 from dvcs import models as dvcs_models
-from catalogue.xml_tools import compile_text
+from catalogue.xml_tools import compile_text, split_xml
 
 import logging
 logger = logging.getLogger("fnp.catalogue")
@@ -23,7 +27,6 @@ class Book(models.Model):
 
     parent = models.ForeignKey('self', null=True, blank=True, verbose_name=_('parent'), related_name="children")
     parent_number = models.IntegerField(_('parent number'), null=True, blank=True, db_index=True)
-    last_published = models.DateTimeField(null=True, editable=False, db_index=True)
 
     class NoTextError(BaseException):
         pass
@@ -32,6 +35,7 @@ class Book(models.Model):
         ordering = ['parent_number', 'title']
         verbose_name = _('book')
         verbose_name_plural = _('books')
+        permissions = [('can_pubmark', 'Can mark for publishing')]
 
     def __unicode__(self):
         return self.title
@@ -39,6 +43,41 @@ class Book(models.Model):
     def get_absolute_url(self):
         return reverse("catalogue_book", args=[self.slug])
 
+    @classmethod
+    def import_xml_text(cls, text=u'', creator=None, previous_book=None,
+                *args, **kwargs):
+
+        texts = split_xml(text)
+        if previous_book:
+            instance = previous_book
+        else:
+            instance = cls(*args, **kwargs)
+            instance.save()
+
+        # if there are more parts, set the rest to empty strings
+        book_len = len(instance)
+        for i in range(book_len - len(texts)):
+            texts.append(u'pusta część %d' % (i + 1), u'')
+
+        i = 0
+        for i, (title, text) in enumerate(texts):
+            if not title:
+                title = u'część %d' % (i + 1)
+
+            slug = slughifi(title)
+
+            if i < book_len:
+                chunk = instance[i]
+                chunk.slug = slug
+                chunk.comment = title
+                chunk.save()
+            else:
+                chunk = instance.add(slug, title, creator, adjust_slug=True)
+
+            chunk.commit(text, author=creator)
+
+        return instance
+
     @classmethod
     def create(cls, creator=None, text=u'', *args, **kwargs):
         """
@@ -47,7 +86,7 @@ class Book(models.Model):
         """
         instance = cls(*args, **kwargs)
         instance.save()
-        instance[0].commit(author=creator, text=text)
+        instance[0].commit(text, author=creator)
         return instance
 
     def __iter__(self):
@@ -56,28 +95,62 @@ class Book(models.Model):
     def __getitem__(self, chunk):
         return self.chunk_set.all()[chunk]
 
-    def materialize(self, publishable=True):
-        """ 
-            Get full text of the document compiled from chunks.
-            Takes the current versions of all texts
-            or versions most recently tagged for publishing.
+    def __len__(self):
+        return self.chunk_set.count()
+
+    def __nonzero__(self):
+        """
+            Necessary so that __len__ isn't used for bool evaluation.
+        """
+        return True
+
+    def get_current_changes(self, publishable=True):
+        """
+            Returns a list containing one Change for every Chunk in the Book.
+            Takes the most recent revision (publishable, if set).
+            Throws an error, if a proper revision is unavailable for a Chunk.
         """
         if publishable:
             changes = [chunk.publishable() for chunk in self]
         else:
-            changes = [chunk.head for chunk in self]
+            changes = [chunk.head for chunk in self if chunk.head is not None]
         if None in changes:
             raise self.NoTextError('Some chunks have no available text.')
+        return changes
+
+    def materialize(self, publishable=False, changes=None):
+        """ 
+            Get full text of the document compiled from chunks.
+            Takes the current versions of all texts
+            or versions most recently tagged for publishing,
+            or a specified iterable changes.
+        """
+        if changes is None:
+            changes = self.get_current_changes(publishable)
         return compile_text(change.materialize() for change in changes)
 
     def publishable(self):
-        if not len(self):
+        if not self.chunk_set.exists():
             return False
         for chunk in self:
             if not chunk.publishable():
                 return False
         return True
 
+    def publish(self, user):
+        """
+            Publishes a book on behalf of a (local) user.
+        """
+        from apiclient import api_call
+
+        changes = self.get_current_changes(publishable=True)
+        book_xml = book.materialize(changes=changes)
+        #api_call(user, "books", {"book_xml": book_xml})
+        # record the publish
+        br = BookPublishRecord.objects.create(book=self, user=user)
+        for c in changes:
+            ChunkPublishRecord.objects.create(book_record=br, change=c)
+
     def make_chunk_slug(self, proposed):
         """ 
             Finds a chunk slug not yet used in the book.
@@ -86,40 +159,59 @@ class Book(models.Model):
         i = 1
         new_slug = proposed
         while new_slug in slugs:
-            new_slug = "%s-%d" % (proposed, i)
+            new_slug = "%s_%d" % (proposed, i)
             i += 1
         return new_slug
 
-    def append(self, other):
+    def append(self, other, slugs=None, titles=None):
+        """Add all chunks of another book to self."""
         number = self[len(self) - 1].number + 1
-        single = len(other) == 1
-        for chunk in other:
+        len_other = len(other)
+        single = len_other == 1
+
+        if slugs is not None:
+            assert len(slugs) == len_other
+        if titles is not None:
+            assert len(titles) == len_other
+            if slugs is None:
+                slugs = [slughifi(t) for t in titles]
+
+        for i, chunk in enumerate(other):
             # move chunk to new book
             chunk.book = self
             chunk.number = number
 
-            # try some title guessing
-            if other.title.startswith(self.title):
-                other_title_part = other.title[len(self.title):].lstrip(' /')
-            else:
-                other_title_part = other.title
-
-            if single:
-                # special treatment for appending one-parters:
-                # just use the guessed title and original book slug
-                chunk.comment = other_title_part
-                if other.slug.startswith(self.slug):
-                    chunk_slug = other.slug[len(self.slug):].lstrip('-_')
+            if titles is None:
+                # try some title guessing
+                if other.title.startswith(self.title):
+                    other_title_part = other.title[len(self.title):].lstrip(' /')
+                else:
+                    other_title_part = other.title
+
+                if single:
+                    # special treatment for appending one-parters:
+                    # just use the guessed title and original book slug
+                    chunk.comment = other_title_part
+                    if other.slug.startswith(self.slug):
+                        chunk_slug = other.slug[len(self.slug):].lstrip('-_')
+                    else:
+                        chunk_slug = other.slug
+                    chunk.slug = self.make_chunk_slug(chunk_slug)
                 else:
-                    chunk_slug = other.slug
-                chunk.slug = self.make_chunk_slug(chunk_slug)
+                    chunk.comment = "%s, %s" % (other_title_part, chunk.comment)
             else:
-                chunk.comment = "%s, %s" % (other_title_part, chunk.comment)
-                chunk.slug = self.make_chunk_slug(chunk.slug)
+                chunk.slug = slugs[i]
+                chunk.comment = titles[i]
+
+            chunk.slug = self.make_chunk_slug(chunk.slug)
             chunk.save()
             number += 1
         other.delete()
 
+    def add(self, *args, **kwargs):
+        """Add a new chunk at the end."""
+        return self.chunk_set.reverse()[0].split(*args, **kwargs)
+
     @staticmethod
     def listener_create(sender, instance, created, **kwargs):
         if created:
@@ -161,12 +253,18 @@ class Chunk(dvcs_models.Document):
             title += " (%d/%d)" % (self.number, book_length)
         return title
 
-    def split(self, slug, comment='', creator=None):
+    def split(self, slug, comment='', creator=None, adjust_slug=False):
         """ Create an empty chunk after this one """
         self.book.chunk_set.filter(number__gt=self.number).update(
                 number=models.F('number')+1)
-        new_chunk = self.book.chunk_set.create(number=self.number+1,
-                creator=creator, slug=slug, comment=comment)
+        new_chunk = None
+        while not new_chunk:
+            new_slug = self.book.make_chunk_slug(slug)
+            try:
+                new_chunk = self.book.chunk_set.create(number=self.number+1,
+                    creator=creator, slug=new_slug, comment=comment)
+            except IntegrityError:
+                pass
         return new_chunk
 
     @staticmethod
@@ -176,3 +274,25 @@ class Chunk(dvcs_models.Document):
             instance.book.save()
 
 models.signals.post_save.connect(Chunk.listener_saved, sender=Chunk)
+
+
+class BookPublishRecord(models.Model):
+    """
+        A record left after publishing a Book.
+    """
+
+    book = models.ForeignKey(Book)
+    timestamp = models.DateTimeField(auto_now_add=True)
+    user = models.ForeignKey(User)
+
+    class Meta:
+        ordering = ['-timestamp']
+
+
+class ChunkPublishRecord(models.Model):
+    """
+        BookPublishRecord details for each Chunk.
+    """
+
+    book_record = models.ForeignKey(BookPublishRecord)
+    change = models.ForeignKey(Chunk.change_model)