+ new_fragment.tags = set(meta_tags + themes + [book_tag] + ancestor_tags)
+ self.save()
+ self.html_built.send(sender=self)
+ return True
+ return False
+
+ # Thin wrappers for builder tasks
+ def build_pdf(self, *args, **kwargs):
+ """(Re)builds PDF."""
+ return tasks.build_pdf.delay(self.pk, *args, **kwargs)
+ def build_epub(self, *args, **kwargs):
+ """(Re)builds EPUB."""
+ return tasks.build_epub.delay(self.pk, *args, **kwargs)
+ def build_mobi(self, *args, **kwargs):
+ """(Re)builds MOBI."""
+ return tasks.build_mobi.delay(self.pk, *args, **kwargs)
+ def build_txt(self, *args, **kwargs):
+ """(Re)builds TXT."""
+ return tasks.build_txt.delay(self.pk, *args, **kwargs)
+
+ @staticmethod
+ def zip_format(format_):
+ def pretty_file_name(book):
+ return "%s/%s.%s" % (
+ b.extra_info['author'],
+ b.slug,
+ format_)
+
+ field_name = "%s_file" % format_
+ books = Book.objects.filter(parent=None).exclude(**{field_name: ""})
+ paths = [(pretty_file_name(b), getattr(b, field_name).path)
+ for b in books.iterator()]
+ return create_zip(paths,
+ getattr(settings, "ALL_%s_ZIP" % format_.upper()))
+
+ def zip_audiobooks(self, format_):
+ bm = BookMedia.objects.filter(book=self, type=format_)
+ paths = map(lambda bm: (None, bm.file.path), bm)
+ return create_zip(paths, "%s_%s" % (self.slug, format_))
+
+ def search_index(self, book_info=None, reuse_index=False, index_tags=True):
+ import search
+ if reuse_index:
+ idx = search.ReusableIndex()
+ else:
+ idx = search.Index()
+
+ idx.open()
+ try:
+ idx.index_book(self, book_info)
+ if index_tags:
+ idx.index_tags()
+ finally:
+ idx.close()
+
+ @classmethod
+ def from_xml_file(cls, xml_file, **kwargs):
+ from django.core.files import File
+ from librarian import dcparser
+
+ # use librarian to parse meta-data
+ book_info = dcparser.parse(xml_file)
+
+ if not isinstance(xml_file, File):
+ xml_file = File(open(xml_file))
+
+ try:
+ return cls.from_text_and_meta(xml_file, book_info, **kwargs)
+ finally:
+ xml_file.close()
+
+ @classmethod
+ def from_text_and_meta(cls, raw_file, book_info, overwrite=False,
+ build_epub=True, build_txt=True, build_pdf=True, build_mobi=True,
+ search_index=True, search_index_tags=True, search_index_reuse=False):
+
+ # check for parts before we do anything
+ children = []
+ if hasattr(book_info, 'parts'):
+ for part_url in book_info.parts:
+ try:
+ children.append(Book.objects.get(slug=part_url.slug))
+ except Book.DoesNotExist:
+ raise Book.DoesNotExist(_('Book "%s" does not exist.') %
+ part_url.slug)
+
+
+ # Read book metadata
+ book_slug = book_info.url.slug
+ if re.search(r'[^a-z0-9-]', book_slug):
+ raise ValueError('Invalid characters in slug')
+ book, created = Book.objects.get_or_create(slug=book_slug)
+
+ if created:
+ book_shelves = []
+ else:
+ if not overwrite:
+ raise Book.AlreadyExists(_('Book %s already exists') % (
+ book_slug))
+ # Save shelves for this book
+ book_shelves = list(book.tags.filter(category='set'))
+
+ book.language = book_info.language
+ book.title = book_info.title
+ if book_info.variant_of:
+ book.common_slug = book_info.variant_of.slug
+ else:
+ book.common_slug = book.slug
+ book.extra_info = book_info.to_dict()
+ book.save()
+
+ meta_tags = Tag.tags_from_info(book_info)
+
+ book.tags = set(meta_tags + book_shelves)
+
+ book_tag = book.book_tag()
+
+ for n, child_book in enumerate(children):
+ child_book.parent = book
+ child_book.parent_number = n
+ child_book.save()
+
+ # Save XML and HTML files
+ book.xml_file.save('%s.xml' % book.slug, raw_file, save=False)
+
+ # delete old fragments when overwriting
+ book.fragments.all().delete()
+
+ if book.build_html():
+ if not settings.NO_BUILD_TXT and build_txt:
+ book.build_txt()
+
+ book.build_cover(book_info)
+
+ if not settings.NO_BUILD_EPUB and build_epub:
+ book.build_epub()
+
+ if not settings.NO_BUILD_PDF and build_pdf:
+ book.build_pdf()
+
+ if not settings.NO_BUILD_MOBI and build_mobi:
+ book.build_mobi()
+
+ if not settings.NO_SEARCH_INDEX and search_index:
+ book.search_index(index_tags=search_index_tags, reuse_index=search_index_reuse)
+ #index_book.delay(book.id, book_info)
+
+ book_descendants = list(book.children.all())
+ descendants_tags = set()
+ # add l-tag to descendants and their fragments
+ while len(book_descendants) > 0:
+ child_book = book_descendants.pop(0)
+ descendants_tags.update(child_book.tags)
+ child_book.tags = list(child_book.tags) + [book_tag]
+ child_book.save()
+ for fragment in child_book.fragments.all().iterator():
+ fragment.tags = set(list(fragment.tags) + [book_tag])
+ book_descendants += list(child_book.children.all())
+
+ for tag in descendants_tags:
+ tasks.touch_tag(tag)
+
+ book.save()
+
+ # refresh cache
+ book.reset_tag_counter()
+ book.reset_theme_counter()
+
+ cls.published.send(sender=book)
+ return book
+
+ def related_info(self):
+ """Keeps info about related objects (tags, media) in cache field."""
+ if self._related_info is not None:
+ return self._related_info
+ else:
+ rel = {'tags': {}, 'media': {}}
+
+ tags = self.tags.filter(category__in=(
+ 'author', 'kind', 'genre', 'epoch'))
+ tags = split_tags(tags)
+ for category in tags:
+ rel['tags'][category] = [
+ (t.name, t.slug) for t in tags[category]]
+
+ for media_format in BookMedia.formats:
+ rel['media'][media_format] = self.has_media(media_format)
+
+ book = self
+ parents = []
+ while book.parent:
+ parents.append((book.parent.title, book.parent.slug))
+ book = book.parent
+ parents = parents[::-1]
+ if parents:
+ rel['parents'] = parents
+
+ if self.pk:
+ type(self).objects.filter(pk=self.pk).update(_related_info=rel)
+ return rel
+
+ def related_themes(self):
+ theme_counter = self.theme_counter
+ book_themes = list(Tag.objects.filter(pk__in=theme_counter.keys()))
+ for tag in book_themes:
+ tag.count = theme_counter[tag.pk]
+ return book_themes
+
+ def reset_tag_counter(self):
+ if self.id is None:
+ return
+
+ cache_key = "Book.tag_counter/%d" % self.id
+ permanent_cache.delete(cache_key)
+ if self.parent:
+ self.parent.reset_tag_counter()
+
+ @property
+ def tag_counter(self):
+ if self.id:
+ cache_key = "Book.tag_counter/%d" % self.id
+ tags = permanent_cache.get(cache_key)
+ else:
+ tags = None
+
+ if tags is None:
+ tags = {}
+ for child in self.children.all().order_by().iterator():
+ for tag_pk, value in child.tag_counter.iteritems():
+ tags[tag_pk] = tags.get(tag_pk, 0) + value
+ for tag in self.tags.exclude(category__in=('book', 'theme', 'set')).order_by().iterator():
+ tags[tag.pk] = 1
+
+ if self.id:
+ permanent_cache.set(cache_key, tags)
+ return tags
+
+ def reset_theme_counter(self):
+ if self.id is None:
+ return
+
+ cache_key = "Book.theme_counter/%d" % self.id
+ permanent_cache.delete(cache_key)
+ if self.parent:
+ self.parent.reset_theme_counter()
+
+ @property
+ def theme_counter(self):
+ if self.id:
+ cache_key = "Book.theme_counter/%d" % self.id
+ tags = permanent_cache.get(cache_key)
+ else:
+ tags = None
+
+ if tags is None:
+ tags = {}
+ for fragment in Fragment.tagged.with_any([self.book_tag()]).order_by().iterator():
+ for tag in fragment.tags.filter(category='theme').order_by().iterator():
+ tags[tag.pk] = tags.get(tag.pk, 0) + 1
+
+ if self.id:
+ permanent_cache.set(cache_key, tags)
+ return tags
+
+ def pretty_title(self, html_links=False):
+ book = self
+ names = list(book.tags.filter(category='author'))
+
+ books = []
+ while book:
+ books.append(book)
+ book = book.parent
+ names.extend(reversed(books))
+
+ if html_links:
+ names = ['<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name) for tag in names]
+ else:
+ names = [tag.name for tag in names]
+
+ return ', '.join(names)
+
+ @classmethod
+ def tagged_top_level(cls, tags):
+ """ Returns top-level books tagged with `tags`.
+
+ It only returns those books which don't have ancestors which are
+ also tagged with those tags.
+
+ """
+ # get relevant books and their tags
+ objects = cls.tagged.with_all(tags)
+ # eliminate descendants
+ l_tags = Tag.objects.filter(category='book',
+ slug__in=[book.book_tag_slug() for book in objects.iterator()])
+ descendants_keys = [book.pk for book in cls.tagged.with_any(l_tags).iterator()]
+ if descendants_keys:
+ objects = objects.exclude(pk__in=descendants_keys)
+
+ return objects
+
+ @classmethod
+ def book_list(cls, filter=None):
+ """Generates a hierarchical listing of all books.
+
+ Books are optionally filtered with a test function.
+
+ """
+
+ books_by_parent = {}
+ books = cls.objects.all().order_by('parent_number', 'sort_key').only(
+ 'title', 'parent', 'slug')
+ if filter:
+ books = books.filter(filter).distinct()