basic flickr support
[redakcja.git] / apps / catalogue / models / book.py
index 1a141fc..ca5ccc8 100755 (executable)
@@ -4,18 +4,16 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.contrib.sites.models import Site
-from django.db import models
+from django.db import models, transaction
 from django.template.loader import render_to_string
 from django.utils.translation import ugettext_lazy as _
 from slughifi import slughifi
-from librarian import NoDublinCore, ParseError, ValidationError
-from librarian.dcparser import BookInfo
 
 import apiclient
 from catalogue.helpers import cached_in_field
 from catalogue.models import BookPublishRecord, ChunkPublishRecord
 from catalogue.signals import post_publish
-from catalogue.tasks import refresh_instance
+from catalogue.tasks import refresh_instance, book_content_updated
 from catalogue.xml_tools import compile_text, split_xml
 
 
@@ -24,6 +22,7 @@ class Book(models.Model):
 
     title = models.CharField(_('title'), max_length=255, db_index=True)
     slug = models.SlugField(_('slug'), max_length=128, unique=True, db_index=True)
+    public = models.BooleanField(_('public'), default=True, db_index=True)
     gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True)
 
     #wl_slug = models.CharField(_('title'), max_length=255, null=True, db_index=True, editable=False)
@@ -35,16 +34,16 @@ class Book(models.Model):
     _single = models.NullBooleanField(editable=False, db_index=True)
     _new_publishable = models.NullBooleanField(editable=False)
     _published = models.NullBooleanField(editable=False)
-
-    # Managers
-    objects = models.Manager()
+    _on_track = models.IntegerField(null=True, blank=True, db_index=True, editable=False)
+    dc_slug = models.CharField(max_length=128, null=True, blank=True,
+            editable=False, db_index=True)
 
     class NoTextError(BaseException):
         pass
 
     class Meta:
         app_label = 'catalogue'
-        ordering = ['title']
+        ordering = ['title', 'slug']
         verbose_name = _('book')
         verbose_name_plural = _('books')
 
@@ -74,11 +73,20 @@ class Book(models.Model):
     def get_absolute_url(self):
         return ("catalogue_book", [self.slug])
 
+    def correct_about(self):
+        return "http://%s%s" % (
+            Site.objects.get_current().domain,
+            self.get_absolute_url()
+        )
 
     # Creating & manipulating
     # =======================
 
+    def accessible(self, request):
+        return self.public or request.user.is_authenticated()
+
     @classmethod
+    @transaction.commit_on_success
     def create(cls, creator, text, *args, **kwargs):
         b = cls.objects.create(*args, **kwargs)
         b.chunk_set.all().update(creator=creator)
@@ -90,6 +98,7 @@ class Book(models.Model):
         return self.chunk_set.reverse()[0].split(*args, **kwargs)
 
     @classmethod
+    @transaction.commit_on_success
     def import_xml_text(cls, text=u'', previous_book=None,
                 commit_args=None, **kwargs):
         """Imports a book from XML, splitting it into chunks as necessary."""
@@ -136,6 +145,7 @@ class Book(models.Model):
             i += 1
         return new_slug
 
+    @transaction.commit_on_success
     def append(self, other, slugs=None, titles=None):
         """Add all chunks of another book to self."""
         assert self != other
@@ -183,6 +193,34 @@ class Book(models.Model):
         assert not other.chunk_set.exists()
         other.delete()
 
+    @transaction.commit_on_success
+    def prepend_history(self, other):
+        """Prepend history from all the other book's chunks to own."""
+        assert self != other
+
+        for i in range(len(self), len(other)):
+            title = u"pusta część %d" % i
+            chunk = self.add(slughifi(title), title)
+            chunk.commit('')
+
+        for i in range(len(other)):
+            self[i].prepend_history(other[0])
+
+        assert not other.chunk_set.exists()
+        other.delete()
+
+    def split(self):
+        """Splits all the chunks into separate books."""
+        self.title
+        for chunk in self:
+            book = Book.objects.create(title=chunk.title, slug=chunk.slug,
+                    public=self.public, gallery=self.gallery)
+            book[0].delete()
+            chunk.book = book
+            chunk.number = 1
+            chunk.save()
+        assert not self.chunk_set.exists()
+        self.delete()
 
     # State & cache
     # =============
@@ -201,6 +239,9 @@ class Book(models.Model):
             raise AssertionError(_('Not all chunks have publishable revisions.'))
         book_xml = self.materialize(changes=changes)
 
+        from librarian.dcparser import BookInfo
+        from librarian import NoDublinCore, ParseError, ValidationError
+
         try:
             bi = BookInfo.from_string(book_xml.encode('utf-8'))
         except ParseError, e:
@@ -210,7 +251,7 @@ class Book(models.Model):
         except ValidationError, e:
             raise AssertionError(_('Invalid Dublin Core') + ': ' + str(e))
 
-        valid_about = "http://%s%s" % (Site.objects.get_current().domain, self.get_absolute_url())
+        valid_about = self.correct_about()
         assert bi.about == valid_about, _("rdf:about is not") + " " + valid_about
 
     def hidden(self):
@@ -239,6 +280,16 @@ class Book(models.Model):
         return self.publish_log.exists()
     published = cached_in_field('_published')(is_published)
 
+    def get_on_track(self):
+        if self.published:
+            return -1
+        stages = [ch.stage.ordering if ch.stage is not None else 0
+                    for ch in self]
+        if not len(stages):
+            return 0
+        return min(stages)
+    on_track = cached_in_field('_on_track')(get_on_track)
+
     def is_single(self):
         return len(self) == 1
     single = cached_in_field('_single')(is_single)
@@ -247,11 +298,38 @@ class Book(models.Model):
     def short_html(self):
         return render_to_string('catalogue/book_list/book.html', {'book': self})
 
+    def book_info(self, publishable=True):
+        try:
+            book_xml = self.materialize(publishable=publishable)
+        except self.NoTextError:
+            pass
+        else:
+            from librarian.dcparser import BookInfo
+            from librarian import NoDublinCore, ParseError, ValidationError
+            try:
+                return BookInfo.from_string(book_xml.encode('utf-8'))
+            except (self.NoTextError, ParseError, NoDublinCore, ValidationError):
+                return None
+
+    def refresh_dc_cache(self):
+        update = {
+            'dc_slug': None,
+        }
+
+        info = self.book_info()
+        if info is not None:
+            update['dc_slug'] = info.url.slug
+        Book.objects.filter(pk=self.pk).update(**update)
+
     def touch(self):
+        # this should only really be done when text or publishable status changes
+        book_content_updated.delay(self)
+
         update = {
             "_new_publishable": self.is_new_publishable(),
             "_published": self.is_published(),
             "_single": self.is_single(),
+            "_on_track": self.get_on_track(),
             "_short_html": None,
         }
         Book.objects.filter(pk=self.pk).update(**update)
@@ -292,6 +370,15 @@ class Book(models.Model):
             changes = self.get_current_changes(publishable)
         return compile_text(change.materialize() for change in changes)
 
+    def wldocument(self, publishable=True, changes=None, parse_dublincore=True):
+        from catalogue.ebook_utils import RedakcjaDocProvider
+        from librarian.parser import WLDocument
+
+        return WLDocument.from_string(
+                self.materialize(publishable=publishable, changes=changes),
+                provider=RedakcjaDocProvider(publishable=publishable),
+                parse_dublincore=parse_dublincore)
+
     def publish(self, user):
         """
             Publishes a book on behalf of a (local) user.