Merge branch 'master' into sunburnt
authorMarcin Koziej <marcin.koziej@nowoczesnapolska.org.pl>
Wed, 24 Oct 2012 11:03:15 +0000 (13:03 +0200)
committerMarcin Koziej <marcin.koziej@nowoczesnapolska.org.pl>
Wed, 24 Oct 2012 11:03:15 +0000 (13:03 +0200)
19 files changed:
apps/catalogue/models/book.py
apps/catalogue/models/listeners.py
apps/catalogue/models/tag.py
apps/catalogue/templates/catalogue/audiobook_list.html
apps/catalogue/templates/catalogue/book_list.html
apps/catalogue/templates/catalogue/menu.html
apps/catalogue/templates/catalogue/snippets/audiobook_list.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/snippets/book_list.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/snippets/book_list_nav.html [new file with mode: 0755]
apps/catalogue/templatetags/catalogue_tags.py
apps/catalogue/views.py
apps/social/templatetags/social_tags.py
apps/wolnelektury_core/static/js/base.js
apps/wolnelektury_core/templates/superbase.html
fabfile.py
lib/librarian
lib/realip_middleware.py [new file with mode: 0644]
wolnelektury/settings/__init__.py
wolnelektury/wsgi.py [new file with mode: 0644]

index 980ad52..eab6b2d 100644 (file)
@@ -86,6 +86,11 @@ class Book(models.Model):
     def get_absolute_url(self):
         return ('catalogue.views.book_detail', [self.slug])
 
+    @staticmethod
+    @permalink
+    def create_url(slug):
+        return ('catalogue.views.book_detail', [slug])
+
     @property
     def name(self):
         return self.title
@@ -481,19 +486,19 @@ class Book(models.Model):
 
     def pretty_title(self, html_links=False):
         book = self
-        names = list(book.tags.filter(category='author'))
-
-        books = []
-        while book:
-            books.append(book)
-            book = book.parent
-        names.extend(reversed(books))
+        rel_info = book.related_info()
+        names = [(name, Tag.create_url('author', slug))
+                    for name, slug in rel_info['tags']['author']]
+        if 'parents' in rel_info:
+            books = [(name, Book.create_url(slug))
+                        for name, slug in rel_info['parents']]
+            names.extend(reversed(books))
+        names.append((self.title, self.get_absolute_url()))
 
         if html_links:
-            names = ['<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name) for tag in names]
+            names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
         else:
-            names = [tag.name for tag in names]
-
+            names = [tag[0] for tag in names]
         return ', '.join(names)
 
     @classmethod
index 9e9fcab..93ad2d7 100644 (file)
@@ -3,13 +3,17 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.conf import settings
+from django.core.cache import get_cache
 from django.db.models.signals import post_save, pre_delete, post_delete
 import django.dispatch
-from catalogue.models import Tag, BookMedia, Book, Fragment
+from catalogue.models import Tag, BookMedia, Book, Fragment, Collection
 from catalogue import tasks
 from newtagging.models import tags_updated
 
 
+permanent_cache = get_cache('permanent')
+
+
 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
@@ -40,9 +44,18 @@ def _post_save_handler(sender, instance, **kwargs):
     """ refresh all the short_html stuff on BookMedia update """
     if sender == BookMedia:
         instance.book.save()
+        permanent_cache.delete_many([
+            'catalogue.audiobook_list', 'catalogue.daisy_list'])
+    elif sender == Collection:
+        permanent_cache.delete('catalogue.collection:%s' % instance.slug)
 post_save.connect(_post_save_handler)
 
 
+def post_publish(sender, **kwargs):
+    permanent_cache.delete('catalogue.book_list')
+Book.published.connect(post_publish)
+
+
 if not settings.NO_SEARCH_INDEX:
     @django.dispatch.receiver(post_delete, sender=Book)
     def _remove_book_from_index_handler(sender, instance, **kwargs):
index 353d567..3c4509d 100644 (file)
@@ -71,6 +71,13 @@ class Tag(TagBase):
     def get_absolute_url(self):
         return ('catalogue.views.tagged_object_list', [self.url_chunk])
 
+    @classmethod
+    @permalink
+    def create_url(cls, category, slug):
+        return ('catalogue.views.tagged_object_list', [
+                '/'.join((cls.categories_dict[category], slug))
+            ])
+
     def has_description(self):
         return len(self.description) > 0
     has_description.short_description = _('description')
index 427f899..c74f78c 100644 (file)
 Możecie z niej korzystać bezpłatnie i bez ograniczeń.
 Audiobooki nagrywają znani aktorzy, wśród nich Danuta Stenka i Jan Peszek.{% endblocktrans %}</p>
 {% endblock %}
-
-
-{% block book_list %}
-    {% audiobook_tree orphans books_by_parent %}
-    {% for author, group in books_by_author.items %}
-        {% if group %}
-            <a name="{{ author.slug }}"></a>
-            <div class="group">
-                <h2><a href="{{ author.get_absolute_url }}">{{ author }}</a></h2>
-                {% audiobook_tree group books_by_parent %}
-            </div>
-        {% endif %}
-    {% endfor %}
-{% endblock %}
index a32f60f..ddeb3ce 100644 (file)
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 {% load i18n %}
-{% load catalogue_tags chunks %}
+{% load catalogue_tags %}
 
 {% block bodyid %}book-a-list{% endblock %}
 
 
     <div id="book-list-nav" class="normal-text">
         {% trans "Table of Content" %}
-        {% for index, authors in books_nav.items %}
-                <ul>
-                    <li><a class="book-list-index" href="#">{{ index|upper }}</a></li>
-                    <ul class="book-list-show-index">
-                    {% for author in authors %}
-                        <li><a href="#{{ author.slug }}">{{ author }}</a></li>
-                    {% endfor %}
-                    </ul>
-                </ul>
-        {% endfor %}    
+        {{ rendered_nav }}
     </div>
     <div id="book-list" class="normal-text">
       {% block book_list %}
-        {% book_tree orphans books_by_parent %}
-        {% for author, group in books_by_author.items %}
-            {% if group %}
-                <a name="{{ author.slug }}"></a>
-                <div class="group">
-                    <h2><a href="{{ author.get_absolute_url }}">{{ author }}</a></h2>
-                    {% book_tree group books_by_parent %}
-                </div>
-            {% endif %}
-        {% endfor %}
+        {{ rendered_book_list }}
       {% endblock %}
     </div>
     <div id="book-list-up">
index 1a90b5c..72bf5e6 100644 (file)
@@ -1,32 +1,16 @@
-{% load i18n %}
-{% load tag_list from catalogue_tags %}
+{% load i18n static %}
 
 <ul id="menu">
+    {% for category, name, hash in categories %}
        <li class="hidden-box-wrapper menu">
-               <a href="{% url catalogue %}#autorzy" class="hidden-box-trigger menu">
-                       <span class='mono'>{% trans "Authors" %}</span></a>
-               <div class="hidden-box">{% if author %}{% tag_list author %}{% endif %}</div>
-       </li>
-       <li class="hidden-box-wrapper menu">
-               <a href="{% url catalogue %}#gatunki" class="hidden-box-trigger menu">
-                       <span class='mono'>{% trans "Genres" %}</span></a>
-               <div class="hidden-box">{% if genre %}{% tag_list genre %}{% endif %}</div>
-       </li>
-       <li class="hidden-box-wrapper menu">
-               <a href="{% url catalogue %}#rodzaje" class="hidden-box-trigger menu">
-                       <span class='mono'>{% trans "Kinds" %}</span></a>
-               <div class="hidden-box">{% if kind %}{% tag_list kind %}{% endif %}</div>
-       </li>
-       <li class="hidden-box-wrapper menu">
-               <a href="{% url catalogue %}#epoki" class="hidden-box-trigger menu">
-                       <span class='mono'>{% trans "Epochs" %}</span></a>
-               <div class="hidden-box">{% if epoch %}{% tag_list epoch %}{% endif %}</div>
-       </li>
-       <li class="hidden-box-wrapper menu">
-               <a href="{% url catalogue %}#motywy" class="hidden-box-trigger menu">
-                       <span class='mono'>{% trans "Themes" %}</span></a>
-               <div class="hidden-box">{% if theme %}{% tag_list theme %}{% endif %}</div>
+               <a href="{% url catalogue %}#{{ hash }}" class="hidden-box-trigger menu load-menu">
+                       <span class='mono'>{{ name }}</span></a>
+               <div class="hidden-box" id="menu-{{ category }}">
+            <img src="{% static "img/indicator.gif" %}" alt="{% trans "Please wait…" %}" />
+        </div>
        </li>
+    {% endfor %}
+
        <li class="menu">
                <a href="{% url book_list %}" class="menu">
                        <span class='mono'>{% trans "All books" %}</span></a>
diff --git a/apps/catalogue/templates/catalogue/snippets/audiobook_list.html b/apps/catalogue/templates/catalogue/snippets/audiobook_list.html
new file mode 100755 (executable)
index 0000000..d7f5992
--- /dev/null
@@ -0,0 +1,12 @@
+{% load catalogue_tags %}
+
+{% audiobook_tree orphans books_by_parent %}
+{% for author, group in books_by_author.items %}
+    {% if group %}
+        <a name="{{ author.slug }}"></a>
+        <div class="group">
+            <h2><a href="{{ author.get_absolute_url }}">{{ author }}</a></h2>
+            {% audiobook_tree group books_by_parent %}
+        </div>
+    {% endif %}
+{% endfor %}
diff --git a/apps/catalogue/templates/catalogue/snippets/book_list.html b/apps/catalogue/templates/catalogue/snippets/book_list.html
new file mode 100755 (executable)
index 0000000..526dcbb
--- /dev/null
@@ -0,0 +1,12 @@
+{% load catalogue_tags %}
+
+{% book_tree orphans books_by_parent %}
+{% for author, group in books_by_author.items %}
+    {% if group %}
+        <a name="{{ author.slug }}"></a>
+        <div class="group">
+            <h2><a href="{{ author.get_absolute_url }}">{{ author }}</a></h2>
+            {% book_tree group books_by_parent %}
+        </div>
+    {% endif %}
+{% endfor %}
diff --git a/apps/catalogue/templates/catalogue/snippets/book_list_nav.html b/apps/catalogue/templates/catalogue/snippets/book_list_nav.html
new file mode 100755 (executable)
index 0000000..258824d
--- /dev/null
@@ -0,0 +1,10 @@
+{% for index, authors in books_nav.items %}
+    <ul>
+        <li><a class="book-list-index" href="#">{{ index|upper }}</a></li>
+        <ul class="book-list-show-index">
+        {% for author in authors %}
+            <li><a href="#{{ author.slug }}">{{ author }}</a></li>
+        {% endfor %}
+        </ul>
+    </ul>
+{% endfor %}
index ad60962..8cf3acf 100644 (file)
@@ -4,6 +4,7 @@
 #
 import datetime
 import feedparser
+from random import randint
 
 from django.conf import settings
 from django import template
@@ -388,9 +389,14 @@ def related_books(book, limit=6, random=1):
                     ignore_by_tag=book.book_tag())[:limit-random]
         cache.set(cache_key, related, 1800)
     if random:
-        related += list(Book.objects.exclude(
-                        pk__in=[b.pk for b in related] + [book.pk]
-                    ).order_by('?')[:random])
+        random_books = Book.objects.exclude(
+                        pk__in=[b.pk for b in related] + [book.pk])
+        if random == 1:
+            count = random_books.count()
+            if count:
+                related.append(random_books[randint(0, count - 1)])
+        else:
+            related += list(random_books.order_by('?')[:random])
     return {
         'books': related,
     }
@@ -398,18 +404,18 @@ def related_books(book, limit=6, random=1):
 
 @register.inclusion_tag('catalogue/menu.html')
 def catalogue_menu():
-    tags = Tag.objects.filter(
-            category__in=('author', 'epoch', 'genre', 'kind', 'theme')
-        ).exclude(book_count=0)
-    return split_tags(tags)
-    
+    return {'categories': [
+                ('author', _('Authors'), 'autorzy'),
+                ('genre', _('Genres'), 'gatunki'),
+                ('kind', _('Kinds'), 'rodzaje'),
+                ('epoch', _('Epochs'), 'epoki'),
+                ('theme', _('Themes'), 'autorzy'),
+        ]}
 
 
 @register.simple_tag
 def tag_url(category, slug):
-    return reverse('catalogue.views.tagged_object_list', args=[
-        '/'.join((Tag.categories_dict[category], slug))
-    ])
+    return Tag.create_url(category, slug)
 
 
 @register.simple_tag
index fc27e02..66036bf 100644 (file)
@@ -6,7 +6,9 @@ import re
 import itertools
 
 from django.conf import settings
+from django.core.cache import get_cache
 from django.template import RequestContext
+from django.template.loader import render_to_string
 from django.shortcuts import render_to_response, get_object_or_404, redirect
 from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect
 from django.core.urlresolvers import reverse
@@ -23,12 +25,14 @@ from ajaxable.utils import JSONResponse, AjaxableFormView
 from catalogue import models
 from catalogue import forms
 from catalogue.utils import split_tags, MultiQuerySet
+from catalogue.templatetags.catalogue_tags import tag_list
 from pdcounter import models as pdcounter_models
 from pdcounter import views as pdcounter_views
 from suggest.forms import PublishingSuggestForm
 from picture.models import Picture
 
 staff_required = user_passes_test(lambda user: user.is_staff)
+permanent_cache = get_cache('permanent')
 
 
 def catalogue(request):
@@ -40,32 +44,52 @@ def catalogue(request):
     categories = split_tags(tags)
     fragment_tags = categories.get('theme', [])
 
-    return render_to_response('catalogue/catalogue.html', locals(),
-        context_instance=RequestContext(request))
+    if request.is_ajax():
+        render_tag_list = lambda x: render_to_string(
+            'catalogue/tag_list.html', tag_list(x))
+        output = {'theme': render_tag_list(fragment_tags)}
+        for category, tags in categories.items():
+            output[category] = render_tag_list(tags)
+        return JSONResponse(output)
+    else:
+        return render_to_response('catalogue/catalogue.html', locals(),
+            context_instance=RequestContext(request))
 
 
 def book_list(request, filter=None, template_name='catalogue/book_list.html',
-        context=None):
+        nav_template_name='catalogue/snippets/book_list_nav.html',
+        list_template_name='catalogue/snippets/book_list.html',
+        cache_key='catalogue.book_list',
+        context=None,
+        ):
     """ generates a listing of all books, optionally filtered with a test function """
-
-    books_by_author, orphans, books_by_parent = models.Book.book_list(filter)
-    books_nav = SortedDict()
-    for tag in books_by_author:
-        if books_by_author[tag]:
-            books_nav.setdefault(tag.sort_key[0], []).append(tag)
-
+    cached = permanent_cache.get(cache_key)
+    if cached is not None:
+        rendered_nav, rendered_book_list = cached
+    else:
+        books_by_author, orphans, books_by_parent = models.Book.book_list(filter)
+        books_nav = SortedDict()
+        for tag in books_by_author:
+            if books_by_author[tag]:
+                books_nav.setdefault(tag.sort_key[0], []).append(tag)
+        rendered_nav = render_to_string(nav_template_name, locals())
+        rendered_book_list = render_to_string(list_template_name, locals())
+        permanent_cache.set(cache_key, (rendered_nav, rendered_book_list))
     return render_to_response(template_name, locals(),
         context_instance=RequestContext(request))
 
 
 def audiobook_list(request):
     return book_list(request, Q(media__type='mp3') | Q(media__type='ogg'),
-                     template_name='catalogue/audiobook_list.html')
+                     template_name='catalogue/audiobook_list.html',
+                     list_template_name='catalogue/snippets/audiobook_list.html',
+                     cache_key='catalogue.audiobook_list')
 
 
 def daisy_list(request):
     return book_list(request, Q(media__type='daisy'),
-                     template_name='catalogue/daisy_list.html')
+                     template_name='catalogue/daisy_list.html',
+                     cache_key='catalogue.daisy_list')
 
 
 def collection(request, slug):
@@ -76,6 +100,7 @@ def collection(request, slug):
                 for slug in slugs]
     return book_list(request, Q(slug__in=slugs),
                      template_name='catalogue/collection.html',
+                     cache_key='catalogue.collection:%s' % coll.slug,
                      context={'collection': coll})
 
 
index 8891602..d6d3f71 100755 (executable)
@@ -2,6 +2,7 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
+from random import randint
 from django import template
 from catalogue.models import Book
 from social.models import Cite
@@ -29,7 +30,16 @@ def choose_cite(context, ctx=None):
                 cites = cites_for_tags([ctx.book_tag()])
         else:
             cites = cites_for_tags(ctx)
-        cite = cites.order_by('-sticky', '?')[0] if cites.exists() else None
+        stickies = cites.filter(sticky=True)
+        count = stickies.count()
+        if count:
+            cite = stickies[randint(0, count - 1)]
+        else:
+            count = cites.count()
+            if count:
+                cite = cites[randint(0, count - 1)]
+            else:
+                cite = None
     return cite
 
 
index d43f1f2..9676d5b 100755 (executable)
@@ -59,6 +59,7 @@
 
                (function() {
                        var $current = null;
+            var menu_loaded = false;
                        $('.hidden-box-wrapper').each(function() {
                                var $hidden = $('.hidden-box', this);
                                $('.hidden-box-trigger', this).click(function(event) {
                                                $current && $current.hide('fast');
                                                $hidden.show('fast');
                                                $current = $hidden;
+                        if ($(this).hasClass('load-menu') && !menu_loaded) {
+                            $.ajax({
+                                url: '/katalog/',
+                                dataType: "json",
+                            }).done(function(data) {
+                                $.each(data, function(index, value) {
+                                    $('#menu-' + index).html(value);
+                                });
+                                menu_loaded = true;
+                            });
+                        }
                                        } 
                                });
                        });
index 79542ec..b0067ac 100644 (file)
         <div id="main-content">
 
             <div id="nav-line">
-               {% cache 60 catalogue-menu LANGUAGE_CODE %}
-                       {% catalogue_menu %}
-               {% endcache %}
+                {% catalogue_menu %}
 
             <div id="lang-menu" class="hoverget">
                 <span id='lang-button' class='mono hoverclick'>
index 30bce66..6a92b7e 100644 (file)
@@ -1,6 +1,7 @@
 from __future__ import with_statement # needed for python 2.5
 from fabric.api import *
 from fabric.contrib import files
+from fabric.context_managers import path
 
 import os
 
@@ -30,6 +31,7 @@ def production():
     env.python = '/usr/bin/python'
     env.virtualenv = '/usr/bin/virtualenv'
     env.pip = 've/bin/pip'
+    env.restart_webserver = restart_gunicorn_debian
 
 # =========
 # = Tasks =
@@ -72,6 +74,7 @@ def deploy():
     migrate()
     collectstatic()
     restart_webserver()
+    restart_celery()
 
 def deploy_version(version):
     "Specify a specific version to be made live"
@@ -81,6 +84,7 @@ def deploy_version(version):
         run('rm releases/previous; mv releases/current releases/previous;', pty=True)
         run('ln -s %(version)s releases/current' % env, pty=True)
     restart_webserver()
+    restart_celery()
 
 def rollback():
     """
@@ -94,6 +98,7 @@ def rollback():
         run('mv releases/previous releases/current;', pty=True)
         run('mv releases/_previous releases/previous;', pty=True)
     restart_webserver()
+    restart_celery()
 
 
 # =====================================================================
@@ -168,9 +173,24 @@ def collectstatic():
     with cd('%(path)s/releases/current/%(project_name)s' % env):
         run('../../../ve/bin/python manage.py collectstatic --noinput' % env, pty=True)
 
+def restart_gunicorn_debian():
+    """Restarts gunicorn server using debian script."""
+    print '>>> restart gunicorn'
+    require('project_name', provided_by=[staging, production])
+    with path('/sbin'):
+        sudo('gunicorn-debian restart %(project_name)s' % env, shell=False)
+
 def restart_webserver():
-    "Restart the web server"
-    print '>>> restart webserver'
-    run('touch %(path)s/%(project_name)s.wsgi' % env)
+    """Restarts the web server."""
+    if hasattr(env, 'restart_webserver'):
+        env.restart_webserver()
+    else:
+        require('project_name', provided_by=[staging, production])
+        print '>>> restart webserver'
+        run('touch %(path)s/%(project_name)s.wsgi' % env)
+
+def restart_celery():
+    """Restarts the Celery task queue manager."""
     print '>>> restart Celery'
+    require('project_name', provided_by=[staging, production])
     sudo('supervisorctl restart celery.%(project_name)s:' % env, shell=False)
index 1ffcf32..3754989 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 1ffcf32a8d795681f54bd125ca45e908da5ed3fb
+Subproject commit 3754989331c91f1d78cd5c1904f768a4cf80f07a
diff --git a/lib/realip_middleware.py b/lib/realip_middleware.py
new file mode 100644 (file)
index 0000000..9f97b7f
--- /dev/null
@@ -0,0 +1,7 @@
+class SetRemoteAddrFromXRealIP(object):
+    """Sets REMOTE_ADDR from the X-Real-IP header, as set by Nginx."""
+    def process_request(self, request):
+        try:
+            request.META['REMOTE_ADDR'] = request.META['HTTP_X_REAL_IP']
+        except KeyError:
+            return None
index db6ba45..46e61bd 100644 (file)
@@ -38,6 +38,7 @@ MIDDLEWARE_CLASSES = [
     'django.middleware.common.CommonMiddleware',
     'django.middleware.cache.FetchFromCacheMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
+    'realip_middleware.SetRemoteAddrFromXRealIP',
 ]
 
 ROOT_URLCONF = 'wolnelektury.urls'
diff --git a/wolnelektury/wsgi.py b/wolnelektury/wsgi.py
new file mode 100644 (file)
index 0000000..46ea2fd
--- /dev/null
@@ -0,0 +1,22 @@
+import os
+import os.path
+import sys
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Add apps and lib directories to PYTHONPATH
+sys.path = [
+    ROOT,
+    os.path.join(ROOT, 'wolnelektury'),
+    os.path.join(ROOT, 'apps'),
+    os.path.join(ROOT, 'lib'),
+    os.path.join(ROOT, 'lib/librarian'),
+] + sys.path
+
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
+
+# This application object is used by the development server
+# as well as any WSGI server configured to use this file.
+from django.core.wsgi import get_wsgi_application
+application = get_wsgi_application()