From 6e75a30246598a40cabb8ca1498c3a4f6f146c91 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 27 Oct 2011 15:54:29 +0200 Subject: [PATCH] initial mobi format support (with a really awful icon) some format generalizations minor fixes --- apps/api/handlers.py | 13 +- apps/catalogue/forms.py | 1 + .../management/commands/importbooks.py | 9 +- apps/catalogue/management/commands/pack.py | 3 +- .../0016_auto__add_field_book_mobi_file.py | 131 +++++++++++++++++ apps/catalogue/models.py | 138 +++++++++--------- apps/catalogue/test_utils.py | 3 +- apps/catalogue/urls.py | 1 + apps/catalogue/utils.py | 4 +- apps/catalogue/views.py | 11 +- lib/librarian | 2 +- wolnelektury/settings.py | 2 + wolnelektury/static/img/mobi.png | Bin 0 -> 6668 bytes .../templates/catalogue/book_detail.html | 7 +- .../catalogue/tagged_object_list.html | 1 + 15 files changed, 237 insertions(+), 89 deletions(-) create mode 100644 apps/catalogue/migrations/0016_auto__add_field_book_mobi_file.py create mode 100644 wolnelektury/static/img/mobi.png diff --git a/apps/api/handlers.py b/apps/api/handlers.py index 41b5ac6f9..8f06deafc 100644 --- a/apps/api/handlers.py +++ b/apps/api/handlers.py @@ -93,8 +93,7 @@ class BookDetailHandler(BaseHandler): """ allowed_methods = ['GET'] - fields = ['title', 'parent', - 'xml', 'html', 'pdf', 'epub', 'txt', + fields = ['title', 'parent'] + Book.file_types + [ 'media', 'url'] + category_singular.keys() @piwik_track @@ -203,7 +202,7 @@ def _file_getter(format): else: return '' return get_file -for format in ('xml', 'txt', 'html', 'epub', 'pdf'): +for format in Book.file_types: setattr(BooksHandler, format, _file_getter(format)) @@ -353,16 +352,16 @@ class CatalogueHandler(BaseHandler): @staticmethod def book_dict(book, fields=None): - all_fields = ('url', 'title', 'description', + all_fields = ['url', 'title', 'description', 'gazeta_link', 'wiki_link', - 'xml', 'epub', 'txt', 'pdf', 'html', + ] + Book.file_types + [ 'mp3', 'ogg', 'daisy', 'parent', 'parent_number', 'tags', 'license', 'license_description', 'source_name', 'technical_editors', 'editors', 'author', 'sort_key', - ) + ] if fields: fields = (f for f in fields if f in all_fields) else: @@ -373,7 +372,7 @@ class CatalogueHandler(BaseHandler): obj = {} for field in fields: - if field in ('xml', 'epub', 'txt', 'pdf', 'html'): + if field in Book.file_types: f = getattr(book, field+'_file') if f: obj[field] = { diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py index 2bf974d44..04969c2e2 100644 --- a/apps/catalogue/forms.py +++ b/apps/catalogue/forms.py @@ -85,6 +85,7 @@ FORMATS = ( ('txt', 'TXT'), ('epub', 'EPUB'), ('daisy', 'DAISY'), + ('mobi', 'MOBI'), ) diff --git a/apps/catalogue/management/commands/importbooks.py b/apps/catalogue/management/commands/importbooks.py index f51214d72..ecd3fcc97 100644 --- a/apps/catalogue/management/commands/importbooks.py +++ b/apps/catalogue/management/commands/importbooks.py @@ -22,6 +22,8 @@ class Command(BaseCommand): help='Print status messages to stdout'), make_option('-E', '--no-build-epub', action='store_false', dest='build_epub', default=True, help='Don\'t build EPUB file'), + make_option('-M', '--no-build-mobi', action='store_false', dest='build_mobi', default=True, + help='Don\'t build MOBI file'), make_option('-T', '--no-build-txt', action='store_false', dest='build_txt', default=True, help='Don\'t build TXT file'), make_option('-P', '--no-build-pdf', action='store_false', dest='build_pdf', default=True, @@ -84,13 +86,18 @@ class Command(BaseCommand): book = Book.from_xml_file(file_path, overwrite=force, build_epub=options.get('build_epub'), build_txt=options.get('build_txt'), - build_pdf=options.get('build_pdf')) + build_pdf=options.get('build_pdf'), + build_mobi=options.get('build_mobi')) files_imported += 1 if os.path.isfile(file_base + '.pdf'): book.pdf_file.save('%s.pdf' % book.slug, File(file(file_base + '.pdf'))) if verbose: print "Importing %s.pdf" % file_base + if os.path.isfile(file_base + '.mobi'): + book.mobi_file.save('%s.mobi' % book.slug, File(file(file_base + '.mobi'))) + if verbose: + print "Importing %s.mobi" % file_base if os.path.isfile(file_base + '.epub'): book.epub_file.save('%s.epub' % book.slug, File(file(file_base + '.epub'))) if verbose: diff --git a/apps/catalogue/management/commands/pack.py b/apps/catalogue/management/commands/pack.py index a300aff0f..c75f092a9 100755 --- a/apps/catalogue/management/commands/pack.py +++ b/apps/catalogue/management/commands/pack.py @@ -24,7 +24,6 @@ class Command(BaseCommand): help='Exclude specific books by slug') ) help = 'Prepare data for Lesmianator.' - ftypes = ['xml', 'txt', 'html', 'epub', 'pdf'] args = '[%s] output_path.zip' % '|'.join(ftypes) def handle(self, ftype, path, **options): @@ -34,7 +33,7 @@ class Command(BaseCommand): include = options.get('include') exclude = options.get('exclude') - if ftype in self.ftypes: + if ftype in Book.file_types: field = "%s_file" % ftype else: print self.style.ERROR('Unknown file type.') diff --git a/apps/catalogue/migrations/0016_auto__add_field_book_mobi_file.py b/apps/catalogue/migrations/0016_auto__add_field_book_mobi_file.py new file mode 100644 index 000000000..87faf6bc2 --- /dev/null +++ b/apps/catalogue/migrations/0016_auto__add_field_book_mobi_file.py @@ -0,0 +1,131 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'Book.mobi_file' + db.add_column('catalogue_book', 'mobi_file', self.gf('django.db.models.fields.files.FileField')(default='', max_length=100, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Book.mobi_file' + db.delete_column('catalogue_book', 'mobi_file') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'catalogue.book': { + 'Meta': {'ordering': "('sort_key',)", 'object_name': 'Book'}, + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'epub_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mobi_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}), + 'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}) + }, + 'catalogue.bookmedia': { + 'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'}, + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'catalogue.filerecord': { + 'Meta': {'ordering': "('-time', '-slug', '-type')", 'object_name': 'FileRecord'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'sha1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20', 'db_index': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'}, + 'anchor': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fragments'", 'to': "orm['catalogue.Book']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'short_text': ('django.db.models.fields.TextField', [], {}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + 'catalogue.tag': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'}, + 'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'main_page': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}) + }, + 'catalogue.tagrelation': { + 'Meta': {'unique_together': "(('tag', 'content_type', 'object_id'),)", 'object_name': 'TagRelation', 'db_table': "'catalogue_tag_relation'"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['catalogue.Tag']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['catalogue'] diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py index e4d359500..565b759ce 100644 --- a/apps/catalogue/models.py +++ b/apps/catalogue/models.py @@ -22,7 +22,7 @@ from django.conf import settings from newtagging.models import TagBase, tags_updated from newtagging import managers from catalogue.fields import JSONField, OverwritingFileField -from catalogue.utils import ExistingFile, BookImportDocProvider, create_zip_task, remove_zip +from catalogue.utils import ExistingFile, ORMDocProvider, create_zip_task, remove_zip from librarian import dcparser, html, epub, NoDublinCore import mutagen @@ -296,11 +296,8 @@ class Book(models.Model): gazeta_link = models.CharField(blank=True, max_length=240) wiki_link = models.CharField(blank=True, max_length=240) # files generated during publication - xml_file = models.FileField(_('XML file'), upload_to=book_upload_path('xml'), blank=True) - html_file = models.FileField(_('HTML file'), upload_to=book_upload_path('html'), blank=True) - pdf_file = models.FileField(_('PDF file'), upload_to=book_upload_path('pdf'), blank=True) - epub_file = models.FileField(_('EPUB file'), upload_to=book_upload_path('epub'), blank=True) - txt_file = models.FileField(_('TXT file'), upload_to=book_upload_path('txt'), blank=True) + + file_types = ['epub', 'html', 'mobi', 'pdf', 'txt', 'xml'] parent = models.ForeignKey('self', blank=True, null=True, related_name='children') objects = models.Manager() @@ -351,49 +348,15 @@ class Book(models.Model): return book_tag def has_media(self, type): - if type == 'xml': - if self.xml_file: - return True - else: - return False - elif type == 'html': - if self.html_file: - return True - else: - return False - elif type == 'txt': - if self.txt_file: - return True - else: - return False - elif type == 'pdf': - if self.pdf_file: - return True - else: - return False - elif type == 'epub': - if self.epub_file: - return True - else: - return False + if type in Book.file_types: + return bool(getattr(self, "%s_file" % type)) else: - if self.media.filter(type=type).exists(): - return True - else: - return False + return self.media.filter(type=type).exists() def get_media(self, type): if self.has_media(type): - if type == "xml": - return self.xml_file - elif type == "html": - return self.html_file - elif type == "epub": - return self.epub_file - elif type == "txt": - return self.txt_file - elif type == "pdf": - return self.pdf_file + if type in Book.file_types: + return getattr(self, "%s_file" % type) else: return self.media.filter(type=type) else: @@ -433,11 +396,13 @@ class Book(models.Model): tags = [mark_safe(u'%s' % (tag.get_absolute_url(), tag.name)) for tag in tags] formats = [] - # files generated during publication + # files generated during publication if self.has_media("html"): formats.append(u'%s' % (reverse('book_text', kwargs={'slug': self.slug}), _('Read online'))) if self.has_media("pdf"): formats.append(u'PDF' % self.get_media('pdf').url) + if self.has_media("mobi"): + formats.append(u'MOBI' % self.get_media('mobi').url) if self.root_ancestor.has_media("epub"): formats.append(u'EPUB' % self.root_ancestor.get_media('epub').url) if self.has_media("txt"): @@ -473,26 +438,6 @@ class Book(models.Model): has_description.boolean = True # ugly ugly ugly - def has_pdf_file(self): - return bool(self.pdf_file) - has_pdf_file.short_description = 'PDF' - has_pdf_file.boolean = True - - def has_epub_file(self): - return bool(self.epub_file) - has_epub_file.short_description = 'EPUB' - has_epub_file.boolean = True - - def has_txt_file(self): - return bool(self.txt_file) - has_txt_file.short_description = 'HTML' - has_txt_file.boolean = True - - def has_html_file(self): - return bool(self.html_file) - has_html_file.short_description = 'HTML' - has_html_file.boolean = True - def has_odt_file(self): return bool(self.has_media("odt")) has_odt_file.short_description = 'ODT' @@ -524,10 +469,9 @@ class Book(models.Model): # remove zip with all pdf files remove_zip(settings.ALL_PDF_ZIP) - path, fname = os.path.realpath(self.xml_file.path).rsplit('/', 1) try: pdf_file = NamedTemporaryFile(delete=False) - pdf.transform(BookImportDocProvider(self), + pdf.transform(ORMDocProvider(self), file_path=str(self.xml_file.path), output_file=pdf_file, ) @@ -536,6 +480,28 @@ class Book(models.Model): finally: unlink(pdf_file.name) + def build_mobi(self): + """ (Re)builds the MOBI file. + + """ + from librarian import mobi + from tempfile import NamedTemporaryFile + import os + + # remove zip with all pdf files + remove_zip(settings.ALL_MOBI_ZIP) + + try: + mobi_file = NamedTemporaryFile(suffix='.mobi', delete=False) + mobi.transform(ORMDocProvider(self), verbose=1, + file_path=str(self.xml_file.path), + output_file=mobi_file.name, + ) + + self.mobi_file.save('%s.mobi' % self.slug, File(open(mobi_file.name))) + finally: + unlink(mobi_file.name) + def build_epub(self, remove_descendants=True): """ (Re)builds the epub file. If book has a parent, does nothing. @@ -554,7 +520,7 @@ class Book(models.Model): epub_file = StringIO() try: - epub.transform(BookImportDocProvider(self), self.slug, output_file=epub_file) + epub.transform(ORMDocProvider(self), self.slug, output_file=epub_file) self.epub_file.save('%s.epub' % self.slug, ContentFile(epub_file.getvalue())) FileRecord(slug=self.slug, type='epub', sha1=sha1(epub_file.getvalue()).hexdigest()).save() except NoDublinCore: @@ -660,6 +626,15 @@ class Book(models.Model): result = create_zip_task(paths, settings.ALL_PDF_ZIP) return result + @staticmethod + def zip_mobi(): + books = Book.objects.all() + + paths = filter(lambda x: x is not None, + map(lambda b: b.mobi_file and b.mobi_file.path or None, books)) + result = create_zip_task.delay(paths, settings.ALL_MOBI_ZIP) + return settings.MEDIA_URL + result.wait() + def zip_audiobooks(self): bm = BookMedia.objects.filter(book=self) paths = map(lambda bm: bm.file.path, bm) @@ -670,7 +645,6 @@ class Book(models.Model): result = create_zip_task(paths, self.slug) return result - @classmethod def from_xml_file(cls, xml_file, **kwargs): # use librarian to parse meta-data @@ -685,7 +659,8 @@ class Book(models.Model): 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): + def from_text_and_meta(cls, raw_file, book_info, overwrite=False, + build_epub=True, build_txt=True, build_pdf=True, build_mobi=True): import re # check for parts before we do anything @@ -761,6 +736,9 @@ class Book(models.Model): if not settings.NO_BUILD_PDF and build_pdf: book.root_ancestor.build_pdf() + if not settings.NO_BUILD_MOBI and build_mobi: + book.build_mobi() + book_descendants = list(book.children.all()) # add l-tag to descendants and their fragments # delete unnecessary EPUB files @@ -872,6 +850,24 @@ class Book(models.Model): return objects +def _has_factory(ftype): + has = lambda self: bool(getattr(self, "%s_file" % ftype)) + has.short_description = t.upper() + has.boolean = True + has.__name__ = "has_%s_file" % ftype + return has + + +# add the file fields +for t in Book.file_types: + field_name = "%s_file" % t + models.FileField(_("%s file" % t.upper()), + upload_to=book_upload_path(t), + blank=True).contribute_to_class(Book, field_name) + + setattr(Book, "has_%s_file" % t, _has_factory(t)) + + class Fragment(models.Model): text = models.TextField() short_text = models.TextField(editable=False) diff --git a/apps/catalogue/test_utils.py b/apps/catalogue/test_utils.py index 7905efbb3..48e9c90ad 100644 --- a/apps/catalogue/test_utils.py +++ b/apps/catalogue/test_utils.py @@ -10,7 +10,8 @@ class WLTestCase(TestCase): """ def setUp(self): self._MEDIA_ROOT, settings.MEDIA_ROOT = settings.MEDIA_ROOT, tempfile.mkdtemp(prefix='djangotest_') - settings.NO_BUILD_PDF = settings.NO_BUILD_EPUB = settings.NO_BUILD_TXT = True + settings.NO_BUILD_PDF = settings.NO_BUILD_MOBI = settings.NO_BUILD_EPUB = settings.NO_BUILD_TXT = True + settings.USE_CELERY = False def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT, True) diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py index 7fd265fdc..fbb1618f7 100644 --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@ -25,6 +25,7 @@ urlpatterns = patterns('catalogue.views', # zip url(r'^zip/pdf/.*\.zip$', 'download_zip', {'format': 'pdf', 'slug': None}, 'download_zip_pdf'), url(r'^zip/epub/.*\.zip$', 'download_zip', {'format': 'epub', 'slug': None}, 'download_zip_epub'), + url(r'^zip/mobi/.*\.zip$', 'download_zip', {'format': 'mobi', 'slug': None}, 'download_zip_mobi'), url(r'^zip/audiobook/(?P[a-zA-Z0-9-]+)\.zip', 'download_zip', {'format': 'audiobook'}, 'download_zip_audiobook'), # tools diff --git a/apps/catalogue/utils.py b/apps/catalogue/utils.py index 07458c9ac..3a8d1b027 100644 --- a/apps/catalogue/utils.py +++ b/apps/catalogue/utils.py @@ -55,8 +55,8 @@ class ExistingFile(UploadedFile): pass -class BookImportDocProvider(DocProvider): - """Used for joined EPUB and PDF files.""" +class ORMDocProvider(DocProvider): + """Used for getting books' children.""" def __init__(self, book): self.book = book diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index 90ef8c2c1..9168099ca 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -606,7 +606,7 @@ def download_shelf(request, slug): if form.is_valid(): formats = form.cleaned_data['formats'] if len(formats) == 0: - formats = ['pdf', 'epub', 'odt', 'txt'] + formats = ['pdf', 'epub', 'mobi', 'odt', 'txt'] # Create a ZIP archive temp = tempfile.TemporaryFile() @@ -617,6 +617,9 @@ def download_shelf(request, slug): if 'pdf' in formats and book.pdf_file: filename = book.pdf_file.path archive.write(filename, str('%s.pdf' % book.slug)) + if 'mobi' in formats and book.mobi_file: + filename = book.mobi_file.path + archive.write(filename, str('%s.mobi' % book.slug)) if book.root_ancestor not in already and 'epub' in formats and book.root_ancestor.epub_file: filename = book.root_ancestor.epub_file.path archive.write(filename, str('%s.epub' % book.root_ancestor.slug)) @@ -646,13 +649,15 @@ def shelf_book_formats(request, shelf): """ shelf = get_object_or_404(models.Tag, slug=shelf, category='set') - formats = {'pdf': False, 'epub': False, 'odt': False, 'txt': False} + formats = {'pdf': False, 'epub': False, 'mobi': False, 'odt': False, 'txt': False} for book in collect_books(models.Book.tagged.with_all(shelf)): if book.pdf_file: formats['pdf'] = True if book.root_ancestor.epub_file: formats['epub'] = True + if book.mobi_file: + formats['mobi'] = True if book.txt_file: formats['txt'] = True for format in ('odt',): @@ -780,6 +785,8 @@ def download_zip(request, format, slug): url = models.Book.zip_pdf() elif format == 'epub': url = models.Book.zip_epub() + elif format == 'mobi': + url = models.Book.zip_mobi() elif format == 'audiobook' and slug is not None: book = models.Book.objects.get(slug=slug) url = book.zip_audiobooks() diff --git a/lib/librarian b/lib/librarian index a6ee2dd83..f8442984a 160000 --- a/lib/librarian +++ b/lib/librarian @@ -1 +1 @@ -Subproject commit a6ee2dd83d3c4d5d2d3e8cb3401734ced2b12c22 +Subproject commit f8442984a021793164661a54ec84ae9caaf9862b diff --git a/wolnelektury/settings.py b/wolnelektury/settings.py index 826d1110d..0b9fcdfa0 100644 --- a/wolnelektury/settings.py +++ b/wolnelektury/settings.py @@ -227,9 +227,11 @@ MAX_TAG_LIST = 6 NO_BUILD_EPUB = False NO_BUILD_TXT = False NO_BUILD_PDF = False +NO_BUILD_MOBI = False ALL_EPUB_ZIP = 'wolnelektury_pl_epub' ALL_PDF_ZIP = 'wolnelektury_pl_pdf' +ALL_MOBI_ZIP = 'wolnelektury_pl_mobi' PAGINATION_INVALID_PAGE_RAISES_404 = True diff --git a/wolnelektury/static/img/mobi.png b/wolnelektury/static/img/mobi.png new file mode 100644 index 0000000000000000000000000000000000000000..b278ec29ed5e30b4ce091b46cd0693112072c660 GIT binary patch literal 6668 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 trans "Read online" %} {% endif %}
- {% if book.has_pdf_file %} + {% if book.pdf_file %} {% trans {% endif %} {% if book.root_ancestor.epub_file %} {% trans {% endif %} - {% if book.has_txt_file %} + {% if book.mobi_file %} + {% trans + {% endif %} + {% if book.txt_file %} {% trans {% endif %} {% for media in book.get_odt %} diff --git a/wolnelektury/templates/catalogue/tagged_object_list.html b/wolnelektury/templates/catalogue/tagged_object_list.html index 4809ad1ce..fec910530 100644 --- a/wolnelektury/templates/catalogue/tagged_object_list.html +++ b/wolnelektury/templates/catalogue/tagged_object_list.html @@ -36,6 +36,7 @@

{% trans "Choose books' formats which you want to download:" %}

  • {% trans "for reading" %} {% trans "and printing using" %} Adobe Reader
  • {% trans "for reading" %} {% trans "on mobile devices" %}
  • +
  • {% trans "for reading" %} {% trans "on mobile devices" %}
  • {% trans "for reading" %} {% trans "and editing using" %} OpenOffice.org
  • {% trans "for reading" %} {% trans "on small displays, for example mobile phones" %}
  • {% trans "for listening" %} {% trans "on favourite MP3 player" %}
  • -- 2.20.1