X-Git-Url: https://git.mdrn.pl/redakcja.git/blobdiff_plain/2f9c60b76f3ab4e69d794a6bb14388a81ff29eb7..f48c55901118787ceafe9c0e1f34e780d9bfe7fe:/apps/catalogue/models.py?ds=sidebyside diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py index ff3d434e..69d0a0d3 100644 --- a/apps/catalogue/models.py +++ b/apps/catalogue/models.py @@ -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)