Merge branch 'production' into pretty
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Fri, 13 Jan 2012 16:05:36 +0000 (17:05 +0100)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 17 Jan 2012 14:17:44 +0000 (15:17 +0100)
Conflicts:
apps/catalogue/models.py
wolnelektury/settings.py
wolnelektury/templates/catalogue/main_page.html
wolnelektury/urls.py

1  2 
apps/api/handlers.py
apps/catalogue/admin.py
apps/catalogue/migrations/0024_auto__add_collection.py
apps/catalogue/models.py
apps/catalogue/urls.py
apps/catalogue/views.py
requirements.txt
wolnelektury/settings.py
wolnelektury/templates/catalogue/collection.html

diff --combined 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',
          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] = {
                          '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({
                      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:
      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)
                  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
@@@ -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)
index 0000000,0000000..e2e2100
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
@@@ -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')),
      ('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)
      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):
      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)
          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):
          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())
  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)
      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)
          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)
          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()
          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'<a href="%s">%s</a>' % (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'<a href="%s">%s</a>' % (reverse('book_text', kwargs={'slug': self.slug}), _('Read online')))
 -            if self.has_media("pdf"):
 -                formats.append(u'<a href="%s">PDF</a>' % self.get_media('pdf').url)
 -            if self.has_media("mobi"):
 -                formats.append(u'<a href="%s">MOBI</a>' % self.get_media('mobi').url)
 -            if self.root_ancestor.has_media("epub"):
 -                formats.append(u'<a href="%s">EPUB</a>' % self.root_ancestor.get_media('epub').url)
 -            if self.has_media("txt"):
 -                formats.append(u'<a href="%s">TXT</a>' % self.get_media('txt').url)
 -            # other files
 -            for m in self.media.order_by('type'):
 -                formats.append(u'<a href="%s">%s</a>' % (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}))
                  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
      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'
      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
  
              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 = []
                      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
  
      @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)
  
              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)
  
              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
          """
  
          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))
  
          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))
  
      
  # 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:
              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
  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
@@@ -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<picture>%s)/?$' % SLUG, 'picture_detail')
 +        ) + \
 +    patterns('catalogue.views',
 +    url(r'^$', 'catalogue', name='catalogue'),
      url(r'^polki/(?P<shelf>[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'),
 -    url(r'^polki/(?P<shelf>[a-zA-Z0-9-]+)/(?P<book>[a-zA-Z0-9-0-]+)/usun$', 'remove_from_shelf', name='remove_from_shelf'),
 +    url(r'^polki/(?P<shelf>[a-zA-Z0-9-]+)/(?P<slug>%s)/usun$' % SLUG, 'remove_from_shelf', name='remove_from_shelf'),
      url(r'^polki/$', 'user_shelves', name='user_shelves'),
      url(r'^polki/(?P<slug>[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'),
      url(r'^polki/(?P<slug>[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<slug>[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<slug>[a-zA-Z0-9-]+)/polki/', 'book_sets', name='book_shelves'),
 +    url(r'^lektura/(?P<book>%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<slug>[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<slug>%s)\.zip' % SLUG, 'download_zip', {'format': 'mp3'}, 'download_zip_mp3'),
 +    url(r'^zip/ogg/(?P<slug>%s)\.zip' % SLUG, 'download_zip', {'format': 'ogg'}, 'download_zip_ogg'),
  
      # Public interface. Do not change this URLs.
 -    url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)\.html$', 'book_text', name='book_text'),
 -    url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)/$', 'book_detail', name='book_detail'),
 -    url(r'^lektura/(?P<book_slug>[a-zA-Z0-9-]+)/motyw/(?P<theme_slug>[a-zA-Z0-9-]+)/$',
 +    url(r'^lektura/(?P<slug>%s)\.html$' % SLUG, 'book_text', name='book_text'),
 +    url(r'^lektura/(?P<slug>%s)/audiobook/$' % SLUG, 'player', name='book_player'),
 +    url(r'^lektura/(?P<slug>%s)/$' % SLUG, 'book_detail', name='book_detail'),
 +    url(r'^lektura/(?P<slug>%s)/motyw/(?P<theme_slug>[a-zA-Z0-9-]+)/$' % SLUG,
          'book_fragments', name='book_fragments'),
  
      url(r'^(?P<tags>[a-zA-Z0-9-/]*)/$', 'tagged_object_list', name='tagged_object_list'),
  
      url(r'^audiobooki/(?P<type>mp3|ogg|daisy|all).xml$', AudiobookFeed(), name='audiobook_feed'),
 -)
  
 +    url(r'^custompdf$', CustomPDFFormView(), name='custom_pdf_form'),
 +    url(r'^custompdf/(?P<slug>%s).pdf' % SLUG, 'download_custom_pdf'),
 +
 +) 
diff --combined 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:])
  
  
  def tagged_object_list(request, tags=''):
 +    #    import pdb; pdb.set_trace()
      try:
          tags = models.Tag.get_tag_list(tags)
      except models.Tag.DoesNotExist:
          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:
      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(_('<p>To maintain your shelves you need to be logged in.</p>'))
  
      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)
  
              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():
  @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
@@@ -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
@@@ -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',
      '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',
  }
  
  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 *
index 0000000,113f7b7..4bb12c9
mode 000000,100755..100755
--- /dev/null
@@@ -1,0 -1,10 +1,10 @@@
 -{% block title %}{{ context.collection.title }} {% trans "in WolneLektury.pl" %}{% endblock %}
+ {% extends "catalogue/book_list.html" %}
+ {% load i18n %}
++{% block titleextra %}{{ context.collection.title }}{% endblock %}
+ {% block book_list_header %}{{ context.collection.title }}{% endblock %}
+ {% block book_list_info %}
+ {{ context.collection.description|safe }}
+ {% endblock %}