From: Radek Czajka Date: Fri, 13 Jan 2012 16:05:36 +0000 (+0100) Subject: Merge branch 'production' into pretty X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/73ce961f14509aabfa26536f847afd28111029c6?hp=-c Merge branch 'production' into pretty Conflicts: apps/catalogue/models.py wolnelektury/settings.py wolnelektury/templates/catalogue/main_page.html wolnelektury/urls.py --- 73ce961f14509aabfa26536f847afd28111029c6 diff --combined apps/api/handlers.py index 586e921ca,eb15a4edd..fddff7407 --- a/apps/api/handlers.py +++ b/apps/api/handlers.py @@@ -7,6 -7,7 +7,7 @@@ import jso from django.conf import settings from django.contrib.sites.models import Site + from django.core.cache import get_cache from django.core.urlresolvers import reverse from piston.handler import AnonymousBaseHandler, BaseHandler from piston.utils import rc @@@ -15,8 -16,6 +16,8 @@@ from api.helpers import timestam from api.models import Deleted from catalogue.forms import BookImportForm from catalogue.models import Book, Tag, BookMedia, Fragment +from picture.models import Picture +from picture.forms import PictureImportForm from stats.utils import piwik_track @@@ -95,12 -94,13 +96,12 @@@ class BookDetailHandler(BaseHandler) """ allowed_methods = ['GET'] - fields = ['title', 'parent'] + Book.file_types + [ + fields = ['title', 'parent'] + Book.formats + [ 'media', 'url'] + category_singular.keys() @piwik_track def read(self, request, slug): - """ Returns details of a book, identified by a slug. """ - + """ Returns details of a book, identified by a slug and lang. """ try: return Book.objects.get(slug=slug) except Book.DoesNotExist: @@@ -203,7 -203,7 +204,7 @@@ def _file_getter(format) else: return '' return get_file -for format in Book.file_types: +for format in Book.formats: setattr(BooksHandler, format, _file_getter(format)) @@@ -247,8 -247,9 +248,8 @@@ class TagsHandler(BaseHandler) except KeyError, e: return rc.NOT_FOUND - tags = Tag.objects.filter(category=category_sng) - tags = [t for t in tags if t.get_count() > 0] - if tags: + tags = Tag.objects.filter(category=category_sng).exclude(book_count=0) + if tags.exists(): return tags else: return rc.NOT_FOUND @@@ -267,6 -268,7 +268,6 @@@ class FragmentDetailHandler(BaseHandler @piwik_track def read(self, request, slug, anchor): """ Returns details of a fragment, identified by book slug and anchor. """ - try: return Fragment.objects.get(book__slug=slug, anchor=anchor) except Fragment.DoesNotExist: @@@ -305,8 -307,7 +306,8 @@@ class FragmentsHandler(BaseHandler) def href(cls, fragment): """ Returns URI in the API for the fragment. """ - return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor]) + return API_BASE + reverse("api_fragment", + args=[fragment.book.slug, fragment.anchor]) @classmethod def url(cls, fragment): @@@ -354,7 -355,8 +355,7 @@@ class CatalogueHandler(BaseHandler) def book_dict(book, fields=None): all_fields = ['url', 'title', 'description', 'gazeta_link', 'wiki_link', - ] + Book.file_types + [ - 'mp3', 'ogg', 'daisy', + ] + Book.formats + BookMedia.formats + [ 'parent', 'parent_number', 'tags', 'license', 'license_description', 'source_name', @@@ -371,7 -373,7 +372,7 @@@ obj = {} for field in fields: - if field in Book.file_types: + if field in Book.formats: f = getattr(book, field+'_file') if f: obj[field] = { @@@ -379,7 -381,7 +380,7 @@@ 'size': f.size, } - elif field in ('mp3', 'ogg', 'daisy'): + elif field in BookMedia.formats: media = [] for m in book.media.filter(type=field): media.append({ @@@ -508,7 -510,7 +509,7 @@@ changed_at__gte=since, changed_at__lt=until): # only serve non-empty tags - if tag.get_count(): + if tag.book_count: tag_d = cls.tag_dict(tag, fields) updated.append(tag_d) elif tag.created_at < since: @@@ -531,6 -533,16 +532,16 @@@ def changes(cls, request=None, since=0, until=None, book_fields=None, tag_fields=None, tag_categories=None): until = cls.until(until) + since = int(since) + + if not since: + cache = get_cache('api') + key = hash((book_fields, tag_fields, tag_categories, + tuple(sorted(request.GET.items())) + )) + value = cache.get(key) + if value is not None: + return value changes = { 'time_checked': timestamp(until) @@@ -546,6 -558,10 +557,10 @@@ if field == 'time_checked': continue changes.setdefault(field, {})[model] = changes_by_type[model][field] + + if not since: + cache.set(key, changes) + return changes @@@ -571,21 -587,3 +586,21 @@@ class ChangesHandler(CatalogueHandler) @piwik_track def read(self, request, since): return self.changes(request, since) + + +class PictureHandler(BaseHandler): + model = Picture + fields = ('slug', 'title') + allowed_methods = ('POST',) + + def create(self, request): + if not request.user.has_perm('picture.add_picture'): + return rc.FORBIDDEN + + data = json.loads(request.POST.get('data')) + form = PictureImportForm(data) + if form.is_valid(): + form.save() + return rc.CREATED + else: + return rc.NOT_FOUND diff --combined apps/catalogue/admin.py index 7ef2aca6f,61db42fe6..87ab727e1 --- a/apps/catalogue/admin.py +++ b/apps/catalogue/admin.py @@@ -6,11 -6,11 +6,11 @@@ from django.contrib import admi from django import forms from newtagging.admin import TaggableModelAdmin, TaggableModelForm - from catalogue.models import Tag, Book, Fragment, BookMedia + from catalogue.models import Tag, Book, Fragment, BookMedia, Collection class TagAdmin(admin.ModelAdmin): - list_display = ('name', 'slug', 'sort_key', 'category', 'has_description', 'main_page',) + list_display = ('name', 'slug', 'sort_key', 'category', 'has_description',) list_filter = ('category',) search_fields = ('name',) ordering = ('name',) @@@ -54,6 -54,11 +54,11 @@@ class FragmentAdmin(TaggableModelAdmin) ordering = ('book', 'anchor',) + class CollectionAdmin(admin.ModelAdmin): + prepopulated_fields = {'slug': ('title',)} + + admin.site.register(Tag, TagAdmin) admin.site.register(Book, BookAdmin) admin.site.register(Fragment, FragmentAdmin) + admin.site.register(Collection, CollectionAdmin) diff --combined apps/catalogue/migrations/0024_auto__add_collection.py index 000000000,000000000..e2e21007d new file mode 100644 --- /dev/null +++ b/apps/catalogue/migrations/0024_auto__add_collection.py @@@ -1,0 -1,0 +1,138 @@@ ++# 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 model 'Collection' ++ db.create_table('catalogue_collection', ( ++ ('title', self.gf('django.db.models.fields.CharField')(max_length=120, db_index=True)), ++ ('slug', self.gf('django.db.models.fields.SlugField')(max_length=120, primary_key=True, db_index=True)), ++ ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), ++ ('book_slugs', self.gf('django.db.models.fields.TextField')()), ++ )) ++ db.send_create_signal('catalogue', ['Collection']) ++ ++ ++ def backwards(self, orm): ++ ++ # Deleting model 'Collection' ++ db.delete_table('catalogue_collection') ++ ++ ++ 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'}), ++ 'common_slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), ++ 'cover': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': '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'}), ++ 'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': '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.collection': { ++ 'Meta': {'ordering': "('title',)", 'object_name': 'Collection'}, ++ 'book_slugs': ('django.db.models.fields.TextField', [], {}), ++ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), ++ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'primary_key': 'True', 'db_index': 'True'}), ++ 'title': ('django.db.models.fields.CharField', [], {'max_length': '120', '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'}), ++ '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 --combined apps/catalogue/models.py index fbae111ea,1417519f7..9a1e71ad0 --- a/apps/catalogue/models.py +++ b/apps/catalogue/models.py @@@ -2,13 -2,12 +2,13 @@@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from datetime import datetime +from collections import namedtuple from django.db import models from django.db.models import permalink, Q import django.dispatch from django.core.cache import cache +from django.core.files.storage import DefaultStorage from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User from django.template.loader import render_to_string @@@ -23,17 -22,9 +23,17 @@@ from django.conf import setting from newtagging.models import TagBase, tags_updated from newtagging import managers from catalogue.fields import JSONField, OverwritingFileField -from catalogue.utils import create_zip +from catalogue.utils import create_zip, split_tags +from catalogue.tasks import touch_tag, index_book +from shutil import copy +from glob import glob +import re +from os import path +import search + +# Those are hard-coded here so that makemessages sees them. TAG_CATEGORIES = ( ('author', _('author')), ('epoch', _('epoch')), @@@ -44,6 -35,13 +44,6 @@@ ('book', _('book')), ) -MEDIA_FORMATS = ( - ('odt', _('ODT file')), - ('mp3', _('MP3 file')), - ('ogg', _('OGG file')), - ('daisy', _('DAISY file')), -) - # not quite, but Django wants you to set a timeout CACHE_FOREVER = 2419200 # 28 days @@@ -64,6 -62,7 +64,6 @@@ class Tag(TagBase) category = models.CharField(_('category'), max_length=50, blank=False, null=False, db_index=True, choices=TAG_CATEGORIES) description = models.TextField(_('description'), blank=True) - main_page = models.BooleanField(_('main page'), default=False, db_index=True, help_text=_('Show tag on main page')) user = models.ForeignKey(User, blank=True, null=True) book_count = models.IntegerField(_('book count'), blank=True, null=True) @@@ -108,22 -107,25 +108,22 @@@ has_description.boolean = True def get_count(self): - """ returns global book count for book tags, fragment count for themes """ - - if self.book_count is None: - if self.category == 'book': - # never used - objects = Book.objects.none() - elif self.category == 'theme': - objects = Fragment.tagged.with_all((self,)) - else: - objects = Book.tagged.with_all((self,)).order_by() - if self.category != 'set': - # eliminate descendants - l_tags = Tag.objects.filter(slug__in=[book.book_tag_slug() for book in objects]) - descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)] - if descendants_keys: - objects = objects.exclude(pk__in=descendants_keys) - self.book_count = objects.count() - self.save() - return self.book_count + """Returns global book count for book tags, fragment count for themes.""" + + if self.category == 'book': + # never used + objects = Book.objects.none() + elif self.category == 'theme': + objects = Fragment.tagged.with_all((self,)) + else: + objects = Book.tagged.with_all((self,)).order_by() + if self.category != 'set': + # eliminate descendants + l_tags = Tag.objects.filter(slug__in=[book.book_tag_slug() for book in objects]) + descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)] + if descendants_keys: + objects = objects.exclude(pk__in=descendants_keys) + return objects.count() @staticmethod def get_tag_list(tags): @@@ -167,89 -169,28 +167,89 @@@ def url_chunk(self): return '/'.join((Tag.categories_dict[self.category], self.slug)) + @staticmethod + def tags_from_info(info): + from slughifi import slughifi + from sortify import sortify + meta_tags = [] + categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch')) + for field_name, category in categories: + try: + tag_names = getattr(info, field_name) + except: + try: + tag_names = [getattr(info, category)] + except: + # For instance, Pictures do not have 'genre' field. + continue + for tag_name in tag_names: + tag_sort_key = tag_name + if category == 'author': + tag_sort_key = tag_name.last_name + tag_name = tag_name.readable() + tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category) + if created: + tag.name = tag_name + tag.sort_key = sortify(tag_sort_key.lower()) + tag.save() + meta_tags.append(tag) + return meta_tags + + + +def get_dynamic_path(media, filename, ext=None, maxlen=100): + from slughifi import slughifi + + # how to put related book's slug here? + if not ext: + # BookMedia case + ext = media.formats[media.type].ext + if media is None or not media.name: + name = slughifi(filename.split(".")[0]) + else: + name = slughifi(media.name) + return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext) + # TODO: why is this hard-coded ? def book_upload_path(ext=None, maxlen=100): - def get_dynamic_path(media, filename, ext=ext): - from slughifi import slughifi + return lambda *args: get_dynamic_path(*args, ext=ext, maxlen=maxlen) - # how to put related book's slug here? - if not ext: - if media.type == 'daisy': - ext = 'daisy.zip' - else: - ext = media.type - if not media.name: - name = slughifi(filename.split(".")[0]) - else: - name = slughifi(media.name) - return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext) - return get_dynamic_path + +def get_customized_pdf_path(book, customizations): + """ + Returns a MEDIA_ROOT relative path for a customized pdf. The name will contain a hash of customization options. + """ + customizations.sort() + h = hash(tuple(customizations)) + + pdf_name = '%s-custom-%s' % (book.slug, h) + pdf_file = get_dynamic_path(None, pdf_name, ext='pdf') + + return pdf_file + + +def get_existing_customized_pdf(book): + """ + Returns a list of paths to generated customized pdf of a book + """ + pdf_glob = '%s-custom-' % (book.slug,) + pdf_glob = get_dynamic_path(None, pdf_glob, ext='pdf') + pdf_glob = re.sub(r"[.]([a-z0-9]+)$", "*.\\1", pdf_glob) + return glob(path.join(settings.MEDIA_ROOT, pdf_glob)) class BookMedia(models.Model): - type = models.CharField(_('type'), choices=MEDIA_FORMATS, max_length="100") + FileFormat = namedtuple("FileFormat", "name ext") + formats = SortedDict([ + ('mp3', FileFormat(name='MP3', ext='mp3')), + ('ogg', FileFormat(name='Ogg Vorbis', ext='ogg')), + ('daisy', FileFormat(name='DAISY', ext='daisy.zip')), + ]) + format_choices = [(k, _('%s file') % t.name) + for k, t in formats.items()] + + type = models.CharField(_('type'), choices=format_choices, max_length="100") name = models.CharField(_('name'), max_length="100") file = OverwritingFileField(_('file'), upload_to=book_upload_path()) uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False) @@@ -272,7 -213,7 +272,7 @@@ try: old = BookMedia.objects.get(pk=self.pk) except BookMedia.DoesNotExist, e: - pass + old = None else: # if name changed, change the file name, too if slughifi(self.name) != slughifi(old.name): @@@ -281,9 -222,7 +281,9 @@@ super(BookMedia, self).save(*args, **kwargs) # remove the zip package for book with modified media - remove_zip(self.book.slug) + if old: + remove_zip("%s_%s" % (old.book.slug, old.type)) + remove_zip("%s_%s" % (self.book.slug, self.type)) extra_info = self.get_extra_info_value() extra_info.update(self.read_meta()) @@@ -352,11 -291,7 +352,11 @@@ class Book(models.Model): title = models.CharField(_('title'), max_length=120) sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False) - slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True) + slug = models.SlugField(_('slug'), max_length=120, db_index=True, + unique=True) + common_slug = models.SlugField(_('slug'), max_length=120, db_index=True) + language = models.CharField(_('language code'), max_length=3, db_index=True, + default=settings.CATALOGUE_DEFAULT_LANGUAGE) description = models.TextField(_('description'), blank=True) created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True) changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True) @@@ -366,11 -301,8 +366,11 @@@ wiki_link = models.CharField(blank=True, max_length=240) # files generated during publication - file_types = ['epub', 'html', 'mobi', 'pdf', 'txt', 'xml'] - + cover = models.FileField(_('cover'), upload_to=book_upload_path('png'), + null=True, blank=True) + ebook_formats = ['pdf', 'epub', 'mobi', 'txt'] + formats = ebook_formats + ['html', 'xml'] + parent = models.ForeignKey('self', blank=True, null=True, related_name='children') objects = models.Manager() tagged = managers.ModelTaggedItemManager(Tag) @@@ -423,14 -355,14 +423,14 @@@ return book_tag def has_media(self, type): - if type in Book.file_types: + if type in Book.formats: return bool(getattr(self, "%s_file" % type)) else: return self.media.filter(type=type).exists() def get_media(self, type): if self.has_media(type): - if type in Book.file_types: + if type in Book.formats: return getattr(self, "%s_file" % type) else: return self.media.filter(type=type) @@@ -453,7 -385,6 +453,7 @@@ cache_key = "Book.short_html/%d/%s" for lang, langname in settings.LANGUAGES: cache.delete(cache_key % (self.id, lang)) + cache.delete("Book.mini_box/%d" % (self.id, )) # Fragment.short_html relies on book's tags, so reset it here too for fragm in self.fragments.all(): fragm.reset_short_html() @@@ -468,15 -399,26 +468,15 @@@ if short_html is not None: return mark_safe(short_html) else: - tags = self.tags.filter(~Q(category__in=('set', 'theme', 'book'))) - tags = [mark_safe(u'%s' % (tag.get_absolute_url(), tag.name)) for tag in tags] + tags = self.tags.filter(category__in=('author', 'kind', 'genre', 'epoch')) + tags = split_tags(tags) - formats = [] + formats = {} # 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"): - formats.append(u'TXT' % self.get_media('txt').url) - # other files - for m in self.media.order_by('type'): - formats.append(u'%s' % (m.file.url, m.type.upper())) - - formats = [mark_safe(format) for format in formats] + for ebook_format in self.ebook_formats: + if self.has_media(ebook_format): + formats[ebook_format] = self.get_media(ebook_format) + short_html = unicode(render_to_string('catalogue/book_short.html', {'book': self, 'tags': tags, 'formats': formats})) @@@ -485,22 -427,17 +485,22 @@@ cache.set(cache_key, short_html, CACHE_FOREVER) return mark_safe(short_html) - @property - def root_ancestor(self): - """ returns the oldest ancestor """ + def mini_box(self): + if self.id: + cache_key = "Book.mini_box/%d" % (self.id, ) + short_html = cache.get(cache_key) + else: + short_html = None - if not hasattr(self, '_root_ancestor'): - book = self - while book.parent: - book = book.parent - self._root_ancestor = book - return self._root_ancestor + if short_html is None: + authors = self.tags.filter(category='author') + short_html = unicode(render_to_string('catalogue/book_mini_box.html', + {'book': self, 'authors': authors, 'STATIC_URL': settings.STATIC_URL})) + + if self.id: + cache.set(cache_key, short_html, CACHE_FOREVER) + return mark_safe(short_html) def has_description(self): return len(self.description) > 0 @@@ -508,6 -445,11 +508,6 @@@ has_description.boolean = True # ugly ugly ugly - def has_odt_file(self): - return bool(self.has_media("odt")) - has_odt_file.short_description = 'ODT' - has_odt_file.boolean = True - def has_mp3_file(self): return bool(self.has_media("mp3")) has_mp3_file.short_description = 'MP3' @@@ -523,94 -465,103 +523,94 @@@ has_daisy_file.short_description = 'DAISY' has_daisy_file.boolean = True - def build_pdf(self): - """ (Re)builds the pdf file. + def wldocument(self, parse_dublincore=True): + from catalogue.import_utils import ORMDocProvider + from librarian.parser import WLDocument + + return WLDocument.from_file(self.xml_file.path, + provider=ORMDocProvider(self), + parse_dublincore=parse_dublincore) + + def build_cover(self, book_info=None): + """(Re)builds the cover image.""" + from StringIO import StringIO + from django.core.files.base import ContentFile + from librarian.cover import WLCover + + if book_info is None: + book_info = self.wldocument().book_info + cover = WLCover(book_info).image() + imgstr = StringIO() + cover.save(imgstr, 'png') + self.cover.save(None, ContentFile(imgstr.getvalue())) + + def build_pdf(self, customizations=None, file_name=None): + """ (Re)builds the pdf file. + customizations - customizations which are passed to LaTeX class file. + file_name - save the pdf file under a different name and DO NOT save it in db. """ - from tempfile import NamedTemporaryFile from os import unlink from django.core.files import File - from librarian import pdf - from catalogue.utils import ORMDocProvider, remove_zip + from catalogue.utils import remove_zip - try: - pdf_file = NamedTemporaryFile(delete=False) - pdf.transform(ORMDocProvider(self), - file_path=str(self.xml_file.path), - output_file=pdf_file, - ) + pdf = self.wldocument().as_pdf(customizations=customizations) - self.pdf_file.save('%s.pdf' % self.slug, File(open(pdf_file.name))) - finally: - unlink(pdf_file.name) + if file_name is None: + # we'd like to be sure not to overwrite changes happening while + # (timely) pdf generation is taking place (async celery scenario) + current_self = Book.objects.get(id=self.id) + current_self.pdf_file.save('%s.pdf' % self.slug, + File(open(pdf.get_filename()))) + self.pdf_file = current_self.pdf_file - # remove zip with all pdf files - remove_zip(settings.ALL_PDF_ZIP) + # remove cached downloadables + remove_zip(settings.ALL_PDF_ZIP) + + for customized_pdf in get_existing_customized_pdf(self): + unlink(customized_pdf) + else: + print "saving %s" % file_name + print "to: %s" % DefaultStorage().path(file_name) + DefaultStorage().save(file_name, File(open(pdf.get_filename()))) def build_mobi(self): """ (Re)builds the MOBI file. """ - from tempfile import NamedTemporaryFile - from os import unlink from django.core.files import File - from librarian import mobi - from catalogue.utils import ORMDocProvider, remove_zip + from catalogue.utils import remove_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, - ) + mobi = self.wldocument().as_mobi() - self.mobi_file.save('%s.mobi' % self.slug, File(open(mobi_file.name))) - finally: - unlink(mobi_file.name) + self.mobi_file.save('%s.mobi' % self.slug, File(open(mobi.get_filename()))) # remove zip with all mobi files remove_zip(settings.ALL_MOBI_ZIP) - def build_epub(self, remove_descendants=True): - """ (Re)builds the epub file. - If book has a parent, does nothing. - Unless remove_descendants is False, descendants' epubs are removed. - """ - from StringIO import StringIO - from hashlib import sha1 - from django.core.files.base import ContentFile - from librarian import epub, NoDublinCore - from catalogue.utils import ORMDocProvider, remove_zip - - if self.parent: - # don't need an epub - return + def build_epub(self): + """(Re)builds the epub file.""" + from django.core.files import File + from catalogue.utils import remove_zip - epub_file = StringIO() - try: - 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: - pass + epub = self.wldocument().as_epub() - book_descendants = list(self.children.all()) - while len(book_descendants) > 0: - child_book = book_descendants.pop(0) - if remove_descendants and child_book.has_epub_file(): - child_book.epub_file.delete() - # save anyway, to refresh short_html - child_book.save() - book_descendants += list(child_book.children.all()) + self.epub_file.save('%s.epub' % self.slug, + File(open(epub.get_filename()))) # remove zip package with all epub files remove_zip(settings.ALL_EPUB_ZIP) def build_txt(self): - from StringIO import StringIO from django.core.files.base import ContentFile - from librarian import text - out = StringIO() - text.transform(open(self.xml_file.path), out) - self.txt_file.save('%s.txt' % self.slug, ContentFile(out.getvalue())) + text = self.wldocument().as_text() + self.txt_file.save('%s.txt' % self.slug, ContentFile(text.get_string())) def build_html(self): - from tempfile import NamedTemporaryFile from markupstring import MarkupString - from django.core.files import File + from django.core.files.base import ContentFile from slughifi import slughifi from librarian import html @@@ -618,10 -569,9 +618,10 @@@ category__in=('author', 'epoch', 'genre', 'kind'))) book_tag = self.book_tag() - html_file = NamedTemporaryFile() - if html.transform(self.xml_file.path, html_file, parse_dublincore=False): - self.html_file.save('%s.html' % self.slug, File(html_file)) + html_output = self.wldocument(parse_dublincore=False).as_html() + if html_output: + self.html_file.save('%s.html' % self.slug, + ContentFile(html_output.get_string())) # get ancestor l-tags for adding to new fragments ancestor_tags = [] @@@ -682,25 -632,12 +682,25 @@@ getattr(settings, "ALL_%s_ZIP" % format_.upper())) return result.wait() - def zip_audiobooks(self): - bm = BookMedia.objects.filter(book=self, type='mp3') + def zip_audiobooks(self, format_): + bm = BookMedia.objects.filter(book=self, type=format_) paths = map(lambda bm: (None, bm.file.path), bm) - result = create_zip.delay(paths, self.slug) + result = create_zip.delay(paths, "%s_%s" % (self.slug, format_)) return result.wait() + def search_index(self, book_info=None, reuse_index=False): + if reuse_index: + idx = search.ReusableIndex() + else: + idx = search.Index() + + idx.open() + try: + idx.index_book(self, book_info) + idx.index_tags() + finally: + idx.close() + @classmethod def from_xml_file(cls, xml_file, **kwargs): from django.core.files import File @@@ -719,25 -656,25 +719,25 @@@ @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): + build_epub=True, build_txt=True, build_pdf=True, build_mobi=True, + search_index=True): import re - from slughifi import slughifi from sortify import sortify # check for parts before we do anything children = [] if hasattr(book_info, 'parts'): for part_url in book_info.parts: - base, slug = part_url.rsplit('/', 1) try: - children.append(Book.objects.get(slug=slug)) + children.append(Book.objects.get(slug=part_url.slug)) except Book.DoesNotExist, e: - raise Book.DoesNotExist(_('Book with slug = "%s" does not exist.') % slug) + raise Book.DoesNotExist(_('Book "%s" does not exist.') % + part_url.slug) # Read book metadata - book_base, book_slug = book_info.url.rsplit('/', 1) - if re.search(r'[^a-zA-Z0-9-]', book_slug): + 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) @@@ -745,21 -682,32 +745,21 @@@ book_shelves = [] else: if not overwrite: - raise Book.AlreadyExists(_('Book %s already exists') % book_slug) + 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.set_extra_info_value(book_info.to_dict()) book.save() - meta_tags = [] - categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch')) - for field_name, category in categories: - try: - tag_names = getattr(book_info, field_name) - except: - tag_names = [getattr(book_info, category)] - for tag_name in tag_names: - tag_sort_key = tag_name - if category == 'author': - tag_sort_key = tag_name.last_name - tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name - tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category) - if created: - tag.name = tag_name - tag.sort_key = sortify(tag_sort_key.lower()) - tag.save() - meta_tags.append(tag) + meta_tags = Tag.tags_from_info(book_info) book.tags = set(meta_tags + book_shelves) @@@ -780,35 -728,26 +780,35 @@@ 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.root_ancestor.build_epub() + book.build_epub() if not settings.NO_BUILD_PDF and build_pdf: - book.root_ancestor.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: + index_book.delay(book.id, book_info) + book_descendants = list(book.children.all()) + descendants_tags = set() # add l-tag to descendants and their fragments - # delete unnecessary EPUB files 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(): fragment.tags = set(list(fragment.tags) + [book_tag]) book_descendants += list(child_book.children.all()) + for tag in descendants_tags: + touch_tag.delay(tag) + book.save() # refresh cache @@@ -918,8 -857,7 +918,8 @@@ """ books_by_parent = {} - books = cls.objects.all().order_by('parent_number', 'sort_key').only('title', 'parent', 'slug') + books = cls.objects.all().order_by('parent_number', 'sort_key').only( + 'title', 'parent', 'slug') if filter: books = books.filter(filter).distinct() book_ids = set((book.pk for book in books)) @@@ -947,19 -885,6 +947,19 @@@ return books_by_author, orphans, books_by_parent + _audiences_pl = { + "SP1": (1, u"szkoła podstawowa"), + "SP2": (1, u"szkoła podstawowa"), + "P": (1, u"szkoła podstawowa"), + "G": (2, u"gimnazjum"), + "L": (3, u"liceum"), + "LP": (3, u"liceum"), + } + def audiences_pl(self): + audiences = self.get_extra_info_value().get('audiences', []) + audiences = sorted(set([self._audiences_pl[a] for a in audiences])) + return [a[1] for a in audiences] + def _has_factory(ftype): has = lambda self: bool(getattr(self, "%s_file" % ftype)) @@@ -970,7 -895,7 +970,7 @@@ # add the file fields -for t in Book.file_types: +for t in Book.formats: field_name = "%s_file" % t models.FileField(_("%s file" % t.upper()), upload_to=book_upload_path(t), @@@ -995,7 -920,7 +995,7 @@@ class Fragment(models.Model) verbose_name_plural = _('fragments') def get_absolute_url(self): - return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor) + return '%s#m%s' % (reverse('book_text', args=[self.book.slug]), self.anchor) def reset_short_html(self): if self.id is None: @@@ -1022,6 -947,39 +1022,24 @@@ return mark_safe(short_html) -class FileRecord(models.Model): - slug = models.SlugField(_('slug'), max_length=120, db_index=True) - type = models.CharField(_('type'), max_length=20, db_index=True) - sha1 = models.CharField(_('sha-1 hash'), max_length=40) - time = models.DateTimeField(_('time'), auto_now_add=True) - - class Meta: - ordering = ('-time','-slug', '-type') - verbose_name = _('file record') - verbose_name_plural = _('file records') - - def __unicode__(self): - return "%s %s.%s" % (self.sha1, self.slug, self.type) - - + class Collection(models.Model): + """A collection of books, which might be defined before publishing them.""" + title = models.CharField(_('title'), max_length=120, db_index=True) + slug = models.SlugField(_('slug'), max_length=120, primary_key=True) + description = models.TextField(_('description'), null=True, blank=True) + + models.SlugField(_('slug'), max_length=120, unique=True, db_index=True) + book_slugs = models.TextField(_('book slugs')) + + class Meta: + ordering = ('title',) + verbose_name = _('collection') + verbose_name_plural = _('collections') + + def __unicode__(self): + return self.title + + ########### # # SIGNALS @@@ -1032,8 -990,7 +1050,8 @@@ def _tags_updated_handler(sender, affected_tags, **kwargs): # reset tag global counter # we want Tag.changed_at updated for API to know the tag was touched - Tag.objects.filter(pk__in=[tag.pk for tag in affected_tags]).update(book_count=None, changed_at=datetime.now()) + for tag in affected_tags: + touch_tag.delay(tag) # if book tags changed, reset book tag counter if isinstance(sender, Book) and \ diff --combined apps/catalogue/urls.py index 4baf225a5,0e0da4b4a..db044fc1c --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@@ -4,53 -4,42 +4,54 @@@ # from django.conf.urls.defaults import * from catalogue.feeds import AudiobookFeed +from catalogue.models import Book +from picture.models import Picture +from catalogue.views import CustomPDFFormView -urlpatterns = patterns('catalogue.views', - url(r'^$', 'main_page', name='main_page'), +SLUG = r'[a-z0-9-]*' + +urlpatterns = patterns('picture.views', + # pictures - currently pictures are coupled with catalogue, hence the url is here + url(r'^obraz/?$', 'picture_list'), + url(r'^obraz/(?P%s)/?$' % SLUG, 'picture_detail') + ) + \ + patterns('catalogue.views', + url(r'^$', 'catalogue', name='catalogue'), url(r'^polki/(?P[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'), - url(r'^polki/(?P[a-zA-Z0-9-]+)/(?P[a-zA-Z0-9-0-]+)/usun$', 'remove_from_shelf', name='remove_from_shelf'), + url(r'^polki/(?P[a-zA-Z0-9-]+)/(?P%s)/usun$' % SLUG, 'remove_from_shelf', name='remove_from_shelf'), url(r'^polki/$', 'user_shelves', name='user_shelves'), url(r'^polki/(?P[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'), url(r'^polki/(?P[a-zA-Z0-9-]+)\.zip$', 'download_shelf', name='download_shelf'), - url(r'^lektury/', 'book_list', name='book_list'), + url(r'^lektury/$', 'book_list', name='book_list'), + url(r'^lektury/(?P[a-zA-Z0-9-]+)/$', 'collection', name='collection'), url(r'^audiobooki/$', 'audiobook_list', name='audiobook_list'), url(r'^daisy/$', 'daisy_list', name='daisy_list'), - url(r'^lektura/(?P[a-zA-Z0-9-]+)/polki/', 'book_sets', name='book_shelves'), + url(r'^lektura/(?P%s)/polki/' % SLUG, 'book_sets', name='book_shelves'), url(r'^polki/nowa/$', 'new_set', name='new_set'), url(r'^tags/$', 'tags_starting_with', name='hint'), url(r'^jtags/$', 'json_tags_starting_with', name='jhint'), - url(r'^szukaj/$', 'search', name='search'), + url(r'^szukaj/$', 'search', name='old_search'), # 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 - url(r'^zegar/$', 'clock', name='clock'), + 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/mp3/(?P%s)\.zip' % SLUG, 'download_zip', {'format': 'mp3'}, 'download_zip_mp3'), + url(r'^zip/ogg/(?P%s)\.zip' % SLUG, 'download_zip', {'format': 'ogg'}, 'download_zip_ogg'), # Public interface. Do not change this URLs. - url(r'^lektura/(?P[a-zA-Z0-9-]+)\.html$', 'book_text', name='book_text'), - url(r'^lektura/(?P[a-zA-Z0-9-]+)/$', 'book_detail', name='book_detail'), - url(r'^lektura/(?P[a-zA-Z0-9-]+)/motyw/(?P[a-zA-Z0-9-]+)/$', + url(r'^lektura/(?P%s)\.html$' % SLUG, 'book_text', name='book_text'), + url(r'^lektura/(?P%s)/audiobook/$' % SLUG, 'player', name='book_player'), + url(r'^lektura/(?P%s)/$' % SLUG, 'book_detail', name='book_detail'), + url(r'^lektura/(?P%s)/motyw/(?P[a-zA-Z0-9-]+)/$' % SLUG, 'book_fragments', name='book_fragments'), url(r'^(?P[a-zA-Z0-9-/]*)/$', 'tagged_object_list', name='tagged_object_list'), url(r'^audiobooki/(?Pmp3|ogg|daisy|all).xml$', AudiobookFeed(), name='audiobook_feed'), -) + url(r'^custompdf$', CustomPDFFormView(), name='custom_pdf_form'), + url(r'^custompdf/(?P%s).pdf' % SLUG, 'download_custom_pdf'), + +) diff --combined apps/catalogue/views.py index 34c9c1f1f,bf0c42f70..f57797da1 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@@ -17,45 -17,65 +17,46 @@@ from django.utils.datastructures impor from django.views.decorators.http import require_POST from django.contrib import auth from django.contrib.auth.forms import UserCreationForm, AuthenticationForm -from django.utils import simplejson -from django.utils.functional import Promise -from django.utils.encoding import force_unicode from django.utils.http import urlquote_plus from django.views.decorators import cache from django.utils import translation from django.utils.translation import ugettext as _ from django.views.generic.list_detail import object_list +from ajaxable.utils import LazyEncoder, JSONResponse, AjaxableFormView + from catalogue import models from catalogue import forms -from catalogue.utils import split_tags +from catalogue.utils import (split_tags, AttachmentHttpResponse, + async_build_pdf, MultiQuerySet) +from catalogue.tasks import touch_tag from pdcounter import models as pdcounter_models from pdcounter import views as pdcounter_views from suggest.forms import PublishingSuggestForm +from picture.models import Picture +from os import path staff_required = user_passes_test(lambda user: user.is_staff) -class LazyEncoder(simplejson.JSONEncoder): - def default(self, obj): - if isinstance(obj, Promise): - return force_unicode(obj) - return obj - -# shortcut for JSON reponses -class JSONResponse(HttpResponse): - def __init__(self, data={}, callback=None, **kwargs): - # get rid of mimetype - kwargs.pop('mimetype', None) - data = simplejson.dumps(data) - if callback: - data = callback + "(" + data + ");" - super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs) - - -def main_page(request): - if request.user.is_authenticated(): - shelves = models.Tag.objects.filter(category='set', user=request.user) - new_set_form = forms.NewSetForm() - - tags = models.Tag.objects.exclude(category__in=('set', 'book')) +def catalogue(request): + tags = models.Tag.objects.exclude( + category__in=('set', 'book')).exclude(book_count=0) + tags = list(tags) for tag in tags: - tag.count = tag.get_count() + tag.count = tag.book_count categories = split_tags(tags) fragment_tags = categories.get('theme', []) - form = forms.SearchForm() - return render_to_response('catalogue/main_page.html', locals(), + return render_to_response('catalogue/catalogue.html', locals(), context_instance=RequestContext(request)) - def book_list(request, filter=None, template_name='catalogue/book_list.html'): + def book_list(request, filter=None, template_name='catalogue/book_list.html', + context=None): """ generates a listing of all books, optionally filtered with a test function """ - form = forms.SearchForm() - books_by_author, orphans, books_by_parent = models.Book.book_list(filter) books_nav = SortedDict() for tag in books_by_author: @@@ -76,6 -96,17 +77,17 @@@ def daisy_list(request) template_name='catalogue/daisy_list.html') + def collection(request, slug): + coll = get_object_or_404(models.Collection, slug=slug) + slugs = coll.book_slugs.split() + # allow URIs + slugs = [slug.rstrip('/').rsplit('/', 1)[-1] if '/' in slug else slug + for slug in slugs] + return book_list(request, Q(slug__in=slugs), + template_name='catalogue/collection.html', + context={'collection': coll}) + + def differentiate_tags(request, tags, ambiguous_slugs): beginning = '/'.join(tag.url_chunk for tag in tags) unparsed = '/'.join(ambiguous_slugs[1:]) @@@ -91,7 -122,6 +103,7 @@@ def tagged_object_list(request, tags=''): + # import pdb; pdb.set_trace() try: tags = models.Tag.get_tag_list(tags) except models.Tag.DoesNotExist: @@@ -170,29 -200,28 +182,29 @@@ only_author = len(tags) == 1 and tags[0].category == 'author' objects = models.Book.objects.none() - return object_list( - request, - objects, - template_name='catalogue/tagged_object_list.html', - extra_context={ + # Add pictures + objects = MultiQuerySet(Picture.tagged.with_all(tags), objects) + + return render_to_response('catalogue/tagged_object_list.html', + { + 'object_list': objects, 'categories': categories, 'only_shelf': only_shelf, 'only_author': only_author, 'only_my_shelf': only_my_shelf, 'formats_form': forms.DownloadFormatsForm(), 'tags': tags, - } - ) + }, + context_instance=RequestContext(request)) -def book_fragments(request, book_slug, theme_slug): - book = get_object_or_404(models.Book, slug=book_slug) - book_tag = get_object_or_404(models.Tag, slug='l-' + book_slug, category='book') +def book_fragments(request, slug, theme_slug): + book = get_object_or_404(models.Book, slug=slug) + + book_tag = book.book_tag() theme = get_object_or_404(models.Tag, slug=theme_slug, category='theme') fragments = models.Fragment.tagged.with_all([book_tag, theme]) - form = forms.SearchForm() return render_to_response('catalogue/book_fragments.html', locals(), context_instance=RequestContext(request)) @@@ -201,13 -230,13 +213,13 @@@ def book_detail(request, slug) try: book = models.Book.objects.get(slug=slug) except models.Book.DoesNotExist: - return pdcounter_views.book_stub_detail(request, slug) + return pdcounter_views.book_stub_detail(request, kwargs['slug']) book_tag = book.book_tag() tags = list(book.tags.filter(~Q(category='set'))) categories = split_tags(tags) book_children = book.children.all().order_by('parent_number', 'sort_key') - + _book = book parents = [] while _book.parent: @@@ -223,52 -252,25 +235,52 @@@ extra_info = book.get_extra_info_value() hide_about = extra_info.get('about', '').startswith('http://wiki.wolnepodreczniki.pl') + custom_pdf_form = forms.CustomPDFForm() + return render_to_response('catalogue/book_detail.html', locals(), + context_instance=RequestContext(request)) + + +def player(request, slug): + book = get_object_or_404(models.Book, slug=slug) + if not book.has_media('mp3'): + raise Http404 + + ogg_files = {} + for m in book.media.filter(type='ogg').order_by(): + ogg_files[m.name] = m + + audiobooks = [] + have_oggs = True projects = set() - for m in book.media.filter(type='mp3'): + for mp3 in book.media.filter(type='mp3'): # ogg files are always from the same project - meta = m.get_extra_info_value() + meta = mp3.get_extra_info_value() project = meta.get('project') if not project: # temporary fallback project = u'CzytamySłuchając' projects.add((project, meta.get('funded_by', ''))) + + media = {'mp3': mp3} + + ogg = ogg_files.get(mp3.name) + if ogg: + media['ogg'] = ogg + else: + have_oggs = False + audiobooks.append(media) + print audiobooks + projects = sorted(projects) - form = forms.SearchForm() - return render_to_response('catalogue/book_detail.html', locals(), + return render_to_response('catalogue/player.html', locals(), context_instance=RequestContext(request)) def book_text(request, slug): book = get_object_or_404(models.Book, slug=slug) + if not book.has_html_file(): raise Http404 book_themes = {} @@@ -400,7 -402,7 +412,7 @@@ def books_starting_with(prefix) def find_best_matches(query, user=None): - """ Finds a Book, Tag, BookStub or Author best matching a query. + """ Finds a models.Book, Tag, models.BookStub or Author best matching a query. Returns a with: - zero elements when nothing is found, @@@ -455,7 -457,7 +467,7 @@@ def search(request) context_instance=RequestContext(request)) else: form = PublishingSuggestForm(initial={"books": prefix + ", "}) - return render_to_response('catalogue/search_no_hits.html', + return render_to_response('catalogue/search_no_hits.html', {'tags':tag_list, 'prefix':prefix, "pubsuggest_form": form}, context_instance=RequestContext(request)) @@@ -466,7 -468,7 +478,7 @@@ def tags_starting_with(request) if len(prefix) < 2: return HttpResponse('') tags_list = [] - result = "" + result = "" for tag in _tags_starting_with(prefix, request.user): if not tag.name in tags_list: result += "\n" + tag.name @@@ -507,7 -509,6 +519,7 @@@ def book_sets(request, slug) return HttpResponse(_('

To maintain your shelves you need to be logged in.

')) book = get_object_or_404(models.Book, slug=slug) + user_sets = models.Tag.objects.filter(category='set', user=request.user) book_sets = book.tags.filter(category='set', user=request.user) @@@ -518,10 -519,12 +530,10 @@@ new_shelves = [models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']] for shelf in [shelf for shelf in old_shelves if shelf not in new_shelves]: - shelf.book_count = None - shelf.save() + touch_tag(shelf) for shelf in [shelf for shelf in new_shelves if shelf not in old_shelves]: - shelf.book_count = None - shelf.save() + touch_tag(shelf) book.tags = new_shelves + list(book.tags.filter(~Q(category='set') | ~Q(user=request.user))) if request.is_ajax(): @@@ -539,14 -542,15 +551,14 @@@ @login_required @require_POST @cache.never_cache -def remove_from_shelf(request, shelf, book): - book = get_object_or_404(models.Book, slug=book) +def remove_from_shelf(request, shelf, slug): + book = get_object_or_404(models.Book, slug=slug) + shelf = get_object_or_404(models.Tag, slug=shelf, category='set', user=request.user) if shelf in book.tags: models.Tag.objects.remove_tag(book, shelf) - - shelf.book_count = None - shelf.save() + touch_tag(shelf) return HttpResponse(_('Book was successfully removed from the shelf')) else: @@@ -584,17 -588,31 +596,17 @@@ def download_shelf(request, slug) if form.is_valid(): formats = form.cleaned_data['formats'] if len(formats) == 0: - formats = ['pdf', 'epub', 'mobi', 'odt', 'txt'] + formats = models.Book.ebook_formats # Create a ZIP archive temp = tempfile.TemporaryFile() archive = zipfile.ZipFile(temp, 'w') - already = set() for book in collect_books(models.Book.tagged.with_all(shelf)): - 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)) - already.add(book.root_ancestor) - if 'odt' in formats and book.has_media("odt"): - for file in book.get_media("odt"): - filename = file.file.path - archive.write(filename, str('%s.odt' % slughifi(file.name))) - if 'txt' in formats and book.txt_file: - filename = book.txt_file.path - archive.write(filename, str('%s.txt' % book.slug)) + for ebook_format in models.Book.ebook_formats: + if ebook_format in formats and book.has_media(ebook_format): + filename = book.get_media(ebook_format).path + archive.write(filename, str('%s.%s' % (book.slug, ebook_format))) archive.close() response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed') @@@ -613,14 -631,20 +625,14 @@@ def shelf_book_formats(request, shelf) """ shelf = get_object_or_404(models.Tag, slug=shelf, category='set') - formats = {'pdf': False, 'epub': False, 'mobi': False, 'odt': False, 'txt': False} + formats = {} + for ebook_format in models.Book.ebook_formats: + formats[ebook_format] = 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',): - if book.has_media(format): - formats[format] = True + for ebook_format in models.Book.ebook_formats: + if book.has_media(ebook_format): + formats[ebook_format] = True return HttpResponse(LazyEncoder().encode(formats)) @@@ -654,6 -678,45 +666,6 @@@ def delete_shelf(request, slug) return HttpResponseRedirect('/') -# ================== -# = Authentication = -# ================== -@require_POST -@cache.never_cache -def login(request): - form = AuthenticationForm(data=request.POST, prefix='login') - if form.is_valid(): - auth.login(request, form.get_user()) - response_data = {'success': True, 'errors': {}} - else: - response_data = {'success': False, 'errors': form.errors} - return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data)) - - -@require_POST -@cache.never_cache -def register(request): - registration_form = UserCreationForm(request.POST, prefix='registration') - if registration_form.is_valid(): - user = registration_form.save() - user = auth.authenticate( - username=registration_form.cleaned_data['username'], - password=registration_form.cleaned_data['password1'] - ) - auth.login(request, user) - response_data = {'success': True, 'errors': {}} - else: - response_data = {'success': False, 'errors': registration_form.errors} - return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data)) - - -@cache.never_cache -def logout_then_redirect(request): - auth.logout(request) - return HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?=')) - - - # ========= # = Admin = # ========= @@@ -678,6 -741,14 +690,6 @@@ def import_book(request) return HttpResponse(_("Error importing file: %r") % book_import_form.errors) - -def clock(request): - """ Provides server time for jquery.countdown, - in a format suitable for Date.parse() - """ - return HttpResponse(datetime.now().strftime('%Y/%m/%d %H:%M:%S')) - - # info views for API def book_info(request, id, lang='pl'): @@@ -693,50 -764,13 +705,50 @@@ def tag_info(request, id) return HttpResponse(tag.description) -def download_zip(request, format, slug): +def download_zip(request, format, slug=None): url = None - if format in ('pdf', 'epub', 'mobi'): + if format in models.Book.ebook_formats: url = models.Book.zip_format(format) - elif format == 'audiobook' and slug is not None: - book = models.Book.objects.get(slug=slug) - url = book.zip_audiobooks() + elif format in ('mp3', 'ogg') and slug is not None: + book = get_object_or_404(models.Book, slug=slug) + url = book.zip_audiobooks(format) else: raise Http404('No format specified for zip package') return HttpResponseRedirect(urlquote_plus(settings.MEDIA_URL + url, safe='/?=')) + + +def download_custom_pdf(request, slug, method='GET'): + book = get_object_or_404(models.Book, slug=slug) + + if request.method == method: + form = forms.CustomPDFForm(method == 'GET' and request.GET or request.POST) + if form.is_valid(): + cust = form.customizations + pdf_file = models.get_customized_pdf_path(book, cust) + + if not path.exists(pdf_file): + result = async_build_pdf.delay(book.id, cust, pdf_file) + result.wait() + return AttachmentHttpResponse(file_name=("%s.pdf" % book.slug), file_path=pdf_file, mimetype="application/pdf") + else: + raise Http404(_('Incorrect customization options for PDF')) + else: + raise Http404(_('Bad method')) + + +class CustomPDFFormView(AjaxableFormView): + form_class = forms.CustomPDFForm + title = _('Download custom PDF') + submit = _('Download') + + def __call__(self, request): + from copy import copy + if request.method == 'POST': + request.GET = copy(request.GET) + request.GET['next'] = "%s?%s" % (reverse('catalogue.views.download_custom_pdf', args=[request.GET['slug']]), + request.POST.urlencode()) + return super(CustomPDFFormView, self).__call__(request) + + + def success(self, *args): + pass diff --combined requirements.txt index 2b69d7f6f,bc8b138d9..81cc3936a --- a/requirements.txt +++ b/requirements.txt @@@ -1,7 -1,7 +1,7 @@@ --find-links=http://www.pythonware.com/products/pil/ # django - Django>=1.2.4,<1.3 + Django>=1.3,<1.4 South>=0.7 # migrations for django django-pagination>=1.0 django-rosetta>=0.5.3 @@@ -17,7 -17,7 +17,7 @@@ Feedparser>=4. # PIL PIL>=1.1.6 mutagen>=1.17 -sorl-thumbnail>=3.2,<10 +sorl-thumbnail>=11.09,<12 # home-brewed & dependencies lxml>=2.2.2 @@@ -28,6 -28,3 +28,6 @@@ # celery tasks django-celery django-kombu + +# spell checking +pyenchant diff --combined wolnelektury/settings.py index afd42e96c,e34b9052b..a69b0508e --- a/wolnelektury/settings.py +++ b/wolnelektury/settings.py @@@ -58,9 -58,8 +58,9 @@@ USE_I18N = Tru # Absolute path to the directory that holds media. # Example: "/home/media/media.lawrence.com/" - MEDIA_ROOT = path.join(PROJECT_DIR, '../media') - STATIC_ROOT = path.join(PROJECT_DIR, 'static') - SEARCH_INDEX = path.join(MEDIA_ROOT, 'search') + MEDIA_ROOT = path.join(PROJECT_DIR, '../media/') + STATIC_ROOT = path.join(PROJECT_DIR, 'static/') ++SEARCH_INDEX = path.join(MEDIA_ROOT, 'search/') # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash if there is a path component (optional in other cases). @@@ -77,19 -76,18 +77,19 @@@ ADMIN_MEDIA_PREFIX = '/admin-media/ # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = [ - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', - # 'django.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + # 'django.template.loaders.eggs.Loader', ] TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.core.context_processors.auth', + 'django.contrib.auth.context_processors.auth', 'django.core.context_processors.debug', 'django.core.context_processors.i18n', 'django.core.context_processors.media', 'django.core.context_processors.request', 'wolnelektury.context_processors.extra_settings', + 'search.context_processors.search_form', ) MIDDLEWARE_CLASSES = [ @@@ -112,7 -110,7 +112,7 @@@ TEMPLATE_DIRS = path.join(PROJECT_DIR, 'templates'), ] -LOGIN_URL = '/uzytkownicy/login/' +LOGIN_URL = '/uzytkownicy/zaloguj/' LOGIN_REDIRECT_URL = '/' @@@ -139,7 -137,6 +139,7 @@@ INSTALLED_APPS = 'modeltranslation', # our + 'ajaxable', 'api', 'catalogue', 'chunks', @@@ -154,45 -151,34 +154,56 @@@ 'sponsors', 'stats', 'suggest', + 'picture', + 'search', ] - #CACHE_BACKEND = 'locmem:///?max_entries=3000' - CACHE_BACKEND = 'memcached://127.0.0.1:11211/' - #CACHE_BACKEND = None + CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': [ + '127.0.0.1:11211', + ] + }, + 'api': { + 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', + 'LOCATION': path.join(PROJECT_DIR, 'django_cache/'), + 'KEY_PREFIX': 'api', + 'TIMEOUT': 86400, + }, + } CACHE_MIDDLEWARE_ANONYMOUS_ONLY=True # CSS and JavaScript file groups COMPRESS_CSS = { 'all': { - 'source_filenames': ('css/master.css', 'css/jquery.autocomplete.css', 'css/jquery.countdown.css', 'css/master.plain.css', 'css/sponsors.css', 'css/facelist_2-0.css',), + #'source_filenames': ('css/master.css', 'css/jquery.autocomplete.css', 'css/master.plain.css', 'css/facelist_2-0.css',), + 'source_filenames': [ + 'css/jquery.countdown.css', + + 'css/base.css', + 'css/header.css', + 'css/main_page.css', + 'css/dialogs.css', + 'css/picture_box.css', + 'css/book_box.css', + 'css/catalogue.css', + 'css/sponsors.css', + + 'css/ui-lightness/jquery-ui-1.8.16.custom.css', + ], 'output_filename': 'css/all.min?.css', }, 'book': { 'source_filenames': ('css/master.book.css',), 'output_filename': 'css/book.min?.css', }, + 'player': { + 'source_filenames': [ + 'jplayer/jplayer.blue.monday.css', + ], + 'output_filename': 'css/player.min?.css', + }, 'simple': { 'source_filenames': ('css/simple.css',), 'output_filename': 'css/simple.min?.css', @@@ -200,37 -186,20 +211,37 @@@ } COMPRESS_JS = { - 'jquery': { - 'source_filenames': ('js/jquery.js',), - 'output_filename': 'js/jquery.min.js', - }, - 'all': { - 'source_filenames': ('js/jquery.autocomplete.js', 'js/jquery.form.js', + 'base': { + 'source_filenames': ( + 'js/jquery.cycle.min.js', + 'js/jquery.jqmodal.js', + 'js/jquery.form.js', 'js/jquery.countdown.js', 'js/jquery.countdown-pl.js', 'js/jquery.countdown-de.js', 'js/jquery.countdown-uk.js', 'js/jquery.countdown-es.js', 'js/jquery.countdown-lt.js', 'js/jquery.countdown-ru.js', 'js/jquery.countdown-fr.js', - 'js/jquery.cycle.min.js', - 'js/jquery.jqmodal.js', 'js/jquery.labelify.js', 'js/catalogue.js', + + 'js/jquery-ui-1.8.16.custom.min.js', + + 'js/locale.js', + 'js/dialogs.js', + 'js/sponsors.js', + 'js/base.js', + 'js/pdcounter.js', + + 'js/search.js', + + 'js/jquery.labelify.js', ), - 'output_filename': 'js/all?.min.js', + 'output_filename': 'js/base?.min.js', + }, + 'player': { + 'source_filenames': [ + 'jplayer/jquery.jplayer.min.js', + 'jplayer/jplayer.playlist.min.js', + 'js/player.js', + ], + 'output_filename': 'js/player.min?.js', }, 'book': { 'source_filenames': ('js/jquery.eventdelegation.js', 'js/jquery.scrollto.js', 'js/jquery.highlightfade.js', 'js/book.js',), @@@ -271,16 -240,12 +282,16 @@@ MAX_TAG_LIST = NO_BUILD_EPUB = False NO_BUILD_TXT = False NO_BUILD_PDF = False -NO_BUILD_MOBI = False +NO_BUILD_MOBI = True +NO_SEARCH_INDEX = False ALL_EPUB_ZIP = 'wolnelektury_pl_epub' ALL_PDF_ZIP = 'wolnelektury_pl_pdf' ALL_MOBI_ZIP = 'wolnelektury_pl_mobi' +CATALOGUE_DEFAULT_LANGUAGE = 'pol' +PUBLISH_PLAN_FEED = 'http://redakcja.wolnelektury.pl/documents/track/editor-proofreading/' + PAGINATION_INVALID_PAGE_RAISES_404 = True import djcelery @@@ -294,7 -259,6 +305,7 @@@ BROKER_PASSWORD = "guest BROKER_VHOST = "/" + # Load localsettings, if they exist try: from localsettings import * diff --combined wolnelektury/templates/catalogue/collection.html index 000000000,113f7b76d..4bb12c938 mode 000000,100755..100755 --- a/wolnelektury/templates/catalogue/collection.html +++ b/wolnelektury/templates/catalogue/collection.html @@@ -1,0 -1,10 +1,10 @@@ + {% extends "catalogue/book_list.html" %} + {% load i18n %} + -{% block title %}{{ context.collection.title }} {% trans "in WolneLektury.pl" %}{% endblock %} ++{% block titleextra %}{{ context.collection.title }}{% endblock %} + + {% block book_list_header %}{{ context.collection.title }}{% endblock %} + + {% block book_list_info %} + {{ context.collection.description|safe }} + {% endblock %}