Using cache middleware instead of various caching micro-strategies,
authorRadek Czajka <radekczajka@nowoczesnapolska.org.pl>
Fri, 19 Sep 2014 14:38:11 +0000 (16:38 +0200)
committerRadek Czajka <radekczajka@nowoczesnapolska.org.pl>
Fri, 19 Sep 2014 14:38:11 +0000 (16:38 +0200)
together with django-ssify for two-phased rendering.
Remove dynfunctional, unused and undocumented  API.
Finally, what looks like correct ancestry-aware counters on related tags
(it's a shame all tests were passing, probably need more of those).
Removed unneeded build_absolute_uri tag.

115 files changed:
apps/ajaxable/templates/ajaxable/form.html
apps/api/emitters.py [new file with mode: 0644]
apps/api/handlers.py
apps/api/templates/api/main.html
apps/api/tests.py
apps/api/urls.py
apps/catalogue/__init__.py
apps/catalogue/apps.py [new file with mode: 0644]
apps/catalogue/fields.py
apps/catalogue/helpers.py
apps/catalogue/management/commands/checkintegrity.py
apps/catalogue/migrations/0002_book_ancestor.py
apps/catalogue/migrations/0003_populate_ancestors.py [new file with mode: 0644]
apps/catalogue/migrations/0004_remove_booktags_count_related_info.py [new file with mode: 0644]
apps/catalogue/models/__init__.py
apps/catalogue/models/book.py
apps/catalogue/models/collection.py
apps/catalogue/models/fragment.py
apps/catalogue/models/listeners.py [deleted file]
apps/catalogue/models/source.py
apps/catalogue/models/tag.py
apps/catalogue/signals.py [new file with mode: 0644]
apps/catalogue/tasks.py
apps/catalogue/templates/catalogue/audiobook_list.html
apps/catalogue/templates/catalogue/book_detail.html
apps/catalogue/templates/catalogue/book_list.html
apps/catalogue/templates/catalogue/book_mini_box.html
apps/catalogue/templates/catalogue/book_searched.html
apps/catalogue/templates/catalogue/book_short.html
apps/catalogue/templates/catalogue/book_text.html
apps/catalogue/templates/catalogue/book_wide.html
apps/catalogue/templates/catalogue/daisy_list.html
apps/catalogue/templates/catalogue/latest_blog_posts.html [deleted file]
apps/catalogue/templates/catalogue/menu.html
apps/catalogue/templates/catalogue/related_books.html
apps/catalogue/templates/catalogue/search_multiple_hits.html
apps/catalogue/templates/catalogue/tag_list.html
apps/catalogue/templates/catalogue/tagged_object_list.html
apps/catalogue/templates/catalogue/work-list.html
apps/catalogue/templatetags/catalogue_tags.py
apps/catalogue/test_utils.py
apps/catalogue/tests/__init__.py
apps/catalogue/tests/tags.py
apps/catalogue/tests/test_visit.py [new file with mode: 0644]
apps/catalogue/urls.py
apps/catalogue/utils.py
apps/catalogue/views.py
apps/chunks/migrations/0002_auto_20140911_1253.py [new file with mode: 0644]
apps/chunks/models.py
apps/chunks/templatetags/__init__.py [deleted file]
apps/chunks/templatetags/chunks.py [deleted file]
apps/chunks/urls.py [new file with mode: 0644]
apps/chunks/views.py [new file with mode: 0644]
apps/dictionary/models.py
apps/funding/management/commands/funding_notify.py
apps/funding/models.py
apps/funding/templates/funding/disable_notifications.html [changed mode: 0755->0644]
apps/funding/templates/funding/includes/funding.html [new file with mode: 0644]
apps/funding/templates/funding/includes/fundings.html [new file with mode: 0644]
apps/funding/templates/funding/includes/offer_status.html [new file with mode: 0644]
apps/funding/templates/funding/includes/offer_status_more.html [new file with mode: 0644]
apps/funding/templates/funding/offer_detail.html
apps/funding/templates/funding/snippets/any_remaining.html [changed mode: 0755->0644]
apps/funding/templates/funding/tags/funding.html [deleted file]
apps/funding/templates/funding/tags/offer_status.html [deleted file]
apps/funding/templates/funding/tags/offer_status_more.html [deleted file]
apps/funding/templatetags/funding_tags.py
apps/funding/urls.py
apps/funding/views.py
apps/infopages/templates/infopages/infopage.html
apps/libraries/urls.py
apps/newtagging/models.py
apps/pdcounter/views.py
apps/picture/models.py
apps/picture/templates/picture/picture_detail.html
apps/picture/templates/picture/picture_list_thumb.html
apps/picture/templates/picture/picture_wide.html
apps/picture/templatetags/picture_tags.py
apps/picture/views.py
apps/polls/templates/polls/tags/poll.html
apps/polls/views.py
apps/search/templatetags/search_tags.py
apps/social/models.py
apps/social/templates/social/cite_info.html [new file with mode: 0644]
apps/social/templates/social/cite_promo.html
apps/social/templates/social/sets_form.html
apps/social/templates/social/shelf_tags.html
apps/social/templatetags/social_tags.py
apps/social/urls.py
apps/social/utils.py
apps/social/views.py
apps/sponsors/models.py
apps/sponsors/templatetags/__init__.py [deleted file]
apps/sponsors/templatetags/sponsor_tags.py [deleted file]
apps/sponsors/urls.py [new file with mode: 0644]
apps/sponsors/views.py [new file with mode: 0644]
apps/suggest/templates/publishing_suggest.html
apps/wolnelektury_core/__init__.py
apps/wolnelektury_core/apps.py [new file with mode: 0644]
apps/wolnelektury_core/models.py [deleted file]
apps/wolnelektury_core/signals.py [new file with mode: 0644]
apps/wolnelektury_core/static/js/base.js
apps/wolnelektury_core/templates/auth/login.html
apps/wolnelektury_core/templates/auth/login_register.html
apps/wolnelektury_core/templates/latest_blog_posts.html [new file with mode: 0644]
apps/wolnelektury_core/templates/main_page.html
apps/wolnelektury_core/templates/pagination/pagination.html
apps/wolnelektury_core/templates/superbase.html
apps/wolnelektury_core/templatetags/common_tags.py
apps/wolnelektury_core/views.py
requirements.txt
wolnelektury/settings/__init__.py
wolnelektury/settings/cache.py
wolnelektury/settings/custom.py
wolnelektury/urls.py

index 38113db..13586ac 100755 (executable)
@@ -1,10 +1,11 @@
 {% load i18n %}
+{% load ssi_csrf_token from ssify %}
 
 <h1>{{ title }}</h1>
 
 <form action="{{ request.get_full_path }}" method="post" accept-charset="utf-8"
        class="cuteform{% if placeholdize %} hidelabels{% endif %}">
-{% csrf_token %}
+{% ssi_csrf_token %}
 {% if honeypot %}
     {% load honeypot %}
     {% render_honeypot_field %}
diff --git a/apps/api/emitters.py b/apps/api/emitters.py
new file mode 100644 (file)
index 0000000..2f6f7e7
--- /dev/null
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+"""
+Wrappers for piston Emitter classes.
+
+When outputting a queryset of selected models, instead of returning
+XML or JSON stanzas, SSI include statements are returned.
+
+"""
+from django.core.urlresolvers import reverse
+from django.db.models.query import QuerySet
+from piston.emitters import Emitter, XMLEmitter, JSONEmitter
+from catalogue.models import Book, Fragment, Tag
+from django.utils.translation import get_language
+
+
+class SsiQS(object):
+    """A wrapper for QuerySet that won't serialize."""
+
+    def __init__(self, queryset):
+        self.queryset = queryset
+
+    def __unicode__(self):
+        raise TypeError("This is not serializable.")
+
+    def get_ssis(self, emitter_format):
+        """Yields SSI include statements for the queryset."""
+        url_pattern = reverse('api_include',
+                kwargs={'model': self.queryset.model.__name__.lower(),
+                    'pk': '0000',
+                    'emitter_format': emitter_format,
+                    'lang': get_language(),
+                    })
+        for instance in self.queryset:
+            yield "<!--#include file='%s'-->" % url_pattern.replace('0000',
+                    str(instance.pk))
+
+
+class SsiEmitterMixin(object):
+    def construct(self):
+        if isinstance(self.data, QuerySet) and self.data.model in (Book,
+                Fragment, Tag):
+            return SsiQS(self.data)
+        else:
+            return super(SsiEmitterMixin, self).construct()
+
+
+class SsiJsonEmitter(SsiEmitterMixin, JSONEmitter):
+    def render(self, request):
+        try:
+            return super(SsiJsonEmitter, self).render(request)
+        except TypeError:
+            return '[%s]' % ",".join(self.construct().get_ssis('json'))
+
+Emitter.register('json', SsiJsonEmitter, 'application/json; charset=utf-8')
+
+
+class SsiXmlEmitter(SsiEmitterMixin, XMLEmitter):
+    def render(self, request):
+        try:
+            return super(SsiXmlEmitter, self).render(request)
+        except TypeError:
+            return '<?xml version="1.0" encoding="utf-8"?>\n' \
+                '<response><resource>%s</resource></response>' % \
+                '</resource><resource>'.join(self.construct().get_ssis('xml'))
+
+Emitter.register('xml', SsiXmlEmitter, 'text/xml; charset=utf-8')
+
index a3a5ce5..5fe931e 100644 (file)
@@ -2,29 +2,24 @@
 # 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, timedelta
 import json
 
-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 django.utils.functional import lazy
-from django.utils.timezone import utc
 from piston.handler import AnonymousBaseHandler, BaseHandler
 from piston.utils import rc
 from sorl.thumbnail import default
 
-from api.helpers import timestamp
-from api.models import Deleted
 from catalogue.forms import BookImportForm
 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
 from picture.models import Picture
 from picture.forms import PictureImportForm
-from wolnelektury.utils import tz
 
 from stats.utils import piwik_track
 
+from . import emitters # Register our emitters
+
 API_BASE = WL_BASE = MEDIA_BASE = lazy(
     lambda: u'http://' + Site.objects.get_current().domain, unicode)()
 
@@ -71,7 +66,10 @@ def read_tags(tags, allowed):
             raise ValueError('Category not allowed.')
 
         if category == 'book':
-            books.append(Book.objects.get(slug=slug))
+            try:
+                books.append(Book.objects.get(slug=slug))
+            except Book.DoesNotExist:
+                raise ValueError('Unknown book.')
 
         try:
             real_tags.append(Tag.objects.get(category=category, slug=slug))
@@ -174,8 +172,8 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
         return book.tags.filter(category='genre')
 
     @piwik_track
-    def read(self, request, tags, top_level=False,
-                audiobooks=False, daisy=False):
+    def read(self, request, tags=None, top_level=False,
+                audiobooks=False, daisy=False, pk=None):
         """ Lists all books with given tags.
 
         :param tags: filtering tags; should be a path of categories
@@ -184,8 +182,14 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
              it's children are aren't. By default all books matching the tags
              are returned.
         """
+        if pk is not None:
+            try:
+                return Book.objects.get(pk=pk)
+            except Book.DoesNotExist:
+                return rc.NOT_FOUND
+
         try:
-            tags, ancestors_ = read_tags(tags, allowed=book_tag_categories)
+            tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
         except ValueError:
             return rc.NOT_FOUND
 
@@ -360,8 +364,13 @@ class TagsHandler(BaseHandler, TagDetails):
     fields = ['name', 'href', 'url']
 
     @piwik_track
-    def read(self, request, category):
+    def read(self, request, category=None, pk=None):
         """ Lists all tags in the category (eg. all themes). """
+        if pk is not None:
+            try:
+                return Tag.objects.exclude(category='set').get(pk=pk)
+            except Book.DoesNotExist:
+                return rc.NOT_FOUND
 
         try:
             category_sng = category_singular[category]
@@ -442,274 +451,6 @@ class FragmentsHandler(BaseHandler, FragmentDetails):
             return rc.NOT_FOUND
 
 
-
-# Changes handlers
-
-class CatalogueHandler(BaseHandler):
-
-    @staticmethod
-    def fields(request, name):
-        fields_str = request.GET.get(name) if request is not None else None
-        return fields_str.split(',') if fields_str is not None else None
-
-    @staticmethod
-    def until(t=None):
-        """ Returns time suitable for use as upper time boundary for check.
-
-            Used to avoid issues with time between setting the change stamp
-            and actually saving the model in database.
-            Cuts the microsecond part to avoid issues with DBs where time has
-            more precision.
-
-            :param datetime t: manually sets the upper boundary
-
-        """
-        # set to five minutes ago, to avoid concurrency issues
-        if t is None:
-            t = datetime.utcnow().replace(tzinfo=utc) - timedelta(seconds=settings.API_WAIT)
-        # set to whole second in case DB supports something smaller
-        return t.replace(microsecond=0)
-
-    @staticmethod
-    def book_dict(book, fields=None):
-        all_fields = ['url', 'title', 'description',
-                      'gazeta_link', 'wiki_link',
-                      ] + Book.formats + BookMedia.formats.keys() + [
-                      'parent', 'parent_number',
-                      'tags',
-                      'license', 'license_description', 'source_name',
-                      'technical_editors', 'editors',
-                      'author', 'sort_key',
-                     ]
-        if fields:
-            fields = (f for f in fields if f in all_fields)
-        else:
-            fields = all_fields
-
-        extra_info = book.extra_info
-
-        obj = {}
-        for field in fields:
-
-            if field in Book.formats:
-                f = getattr(book, field+'_file')
-                if f:
-                    obj[field] = {
-                        'url': f.url,
-                        'size': f.size,
-                    }
-
-            elif field in BookMedia.formats:
-                media = []
-                for m in book.media.filter(type=field).iterator():
-                    media.append({
-                        'url': m.file.url,
-                        'size': m.file.size,
-                    })
-                if media:
-                    obj[field] = media
-
-            elif field == 'url':
-                obj[field] = book.get_absolute_url()
-
-            elif field == 'tags':
-                obj[field] = [t.id for t in book.tags.exclude(category='set').iterator()]
-
-            elif field == 'author':
-                obj[field] = ", ".join(t.name for t in book.tags.filter(category='author').iterator())
-
-            elif field == 'parent':
-                obj[field] = book.parent_id
-
-            elif field in ('license', 'license_description', 'source_name',
-                      'technical_editors', 'editors'):
-                f = extra_info.get(field)
-                if f:
-                    obj[field] = f
-
-            else:
-                f = getattr(book, field)
-                if f:
-                    obj[field] = f
-
-        obj['id'] = book.id
-        return obj
-
-    @classmethod
-    def book_changes(cls, request=None, since=0, until=None, fields=None):
-        since = datetime.fromtimestamp(int(since), tz)
-        until = cls.until(until)
-
-        changes = {
-            'time_checked': timestamp(until)
-        }
-
-        if not fields:
-            fields = cls.fields(request, 'book_fields')
-
-        added = []
-        updated = []
-        deleted = []
-
-        last_change = since
-        for book in Book.objects.filter(changed_at__gte=since,
-                    changed_at__lt=until).iterator():
-            book_d = cls.book_dict(book, fields)
-            updated.append(book_d)
-        if updated:
-            changes['updated'] = updated
-
-        for book in Deleted.objects.filter(content_type=Book,
-                    deleted_at__gte=since,
-                    deleted_at__lt=until,
-                    created_at__lt=since).iterator():
-            deleted.append(book.id)
-        if deleted:
-            changes['deleted'] = deleted
-
-        return changes
-
-    @staticmethod
-    def tag_dict(tag, fields=None):
-        all_fields = ('name', 'category', 'sort_key', 'description',
-                      'gazeta_link', 'wiki_link',
-                      'url', 'books',
-                     )
-
-        if fields:
-            fields = (f for f in fields if f in all_fields)
-        else:
-            fields = all_fields
-
-        obj = {}
-        for field in fields:
-
-            if field == 'url':
-                obj[field] = tag.get_absolute_url()
-
-            elif field == 'books':
-                obj[field] = [b.id for b in Book.tagged_top_level([tag]).iterator()]
-
-            elif field == 'sort_key':
-                obj[field] = tag.sort_key
-
-            else:
-                f = getattr(tag, field)
-                if f:
-                    obj[field] = f
-
-        obj['id'] = tag.id
-        return obj
-
-    @classmethod
-    def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
-        since = datetime.fromtimestamp(int(since), tz)
-        until = cls.until(until)
-
-        changes = {
-            'time_checked': timestamp(until)
-        }
-
-        if not fields:
-            fields = cls.fields(request, 'tag_fields')
-        if not categories:
-            categories = cls.fields(request, 'tag_categories')
-
-        all_categories = ('author', 'epoch', 'kind', 'genre')
-        if categories:
-            categories = (c for c in categories if c in all_categories)
-        else:
-            categories = all_categories
-
-        updated = []
-        deleted = []
-
-        for tag in Tag.objects.filter(category__in=categories,
-                    changed_at__gte=since,
-                    changed_at__lt=until
-                    ).exclude(items=None).iterator():
-            tag_d = cls.tag_dict(tag, fields)
-            updated.append(tag_d)
-        for tag in Tag.objects.filter(category__in=categories,
-                    created_at__lt=since,
-                    changed_at__gte=since,
-                    changed_at__lt=until,
-                    items=None).iterator():
-            deleted.append(tag.id)
-        if updated:
-            changes['updated'] = updated
-
-        for tag in Deleted.objects.filter(category__in=categories,
-                content_type=Tag,
-                    deleted_at__gte=since,
-                    deleted_at__lt=until,
-                    created_at__lt=since).iterator():
-            deleted.append(tag.id)
-        if deleted:
-            changes['deleted'] = deleted
-
-        return changes
-
-    @classmethod
-    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)
-        }
-
-        changes_by_type = {
-            'books': cls.book_changes(request, since, until, book_fields),
-            'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
-        }
-
-        for model in changes_by_type:
-            for field in changes_by_type[model]:
-                if field == 'time_checked':
-                    continue
-                changes.setdefault(field, {})[model] = changes_by_type[model][field]
-
-        if not since:
-            cache.set(key, changes)
-
-        return changes
-
-
-class BookChangesHandler(CatalogueHandler):
-    allowed_methods = ('GET',)
-
-    @piwik_track
-    def read(self, request, since):
-        return self.book_changes(request, since)
-
-
-class TagChangesHandler(CatalogueHandler):
-    allowed_methods = ('GET',)
-
-    @piwik_track
-    def read(self, request, since):
-        return self.tag_changes(request, since)
-
-
-class ChangesHandler(CatalogueHandler):
-    allowed_methods = ('GET',)
-
-    @piwik_track
-    def read(self, request, since):
-        return self.changes(request, since)
-
-
 class PictureHandler(BaseHandler):
     model = Picture
     fields = ('slug', 'title')
index 5c81a2d..04244dc 100755 (executable)
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 {% load i18n %}
-{% load common_tags %}
+{% load build_absolute_uri from fnp_common %}
 
 {% block title %}{% trans "WolneLektury.pl API" %}{% endblock %}
 
index 87c4f75..94abe53 100644 (file)
@@ -16,89 +16,23 @@ import picture.tests
 
 
 @override_settings(
-    API_WAIT=-1,
-    NO_SEARCH_INDEX = True,
-    CACHES = {'api': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
-              'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
-              'permanent': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}}
+    NO_SEARCH_INDEX=True,
+    CACHES={'default': {
+        'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}},
+    SSIFY_CACHE_ALIASES=['default'],
+    SSIFY_RENDER=True,
 )
 class ApiTest(TestCase):
-    pass
+    def load_json(self, url):
+        content = self.client.get(url).content
+        try:
+            data = json.loads(content)
+        except ValueError:
+            self.fail('No JSON could be decoded:', content)
+        return data
 
 
-class ChangesTest(ApiTest):
-
-    def test_basic(self):
-        book = Book(title='A Book')
-        book.save()
-        tag = Tag.objects.create(category='author', name='Author')
-        book.tags = [tag]
-        book.save()
-
-        changes = json.loads(self.client.get('/api/changes/0.json?book_fields=title&tag_fields=name').content)
-        self.assertEqual(changes['updated']['books'],
-                         [{'id': book.id, 'title': book.title}],
-                         'Invalid book format in changes')
-        self.assertEqual(changes['updated']['tags'],
-                         [{'id': tag.id, 'name': tag.name}],
-                         'Invalid tag format in changes')
-
-
-class BookChangesTests(ApiTest):
-
-    def setUp(self):
-        super(BookChangesTests, self).setUp()
-        self.book = Book.objects.create(slug='slug')
-
-    def test_basic(self):
-        # test book in book_changes.added
-        changes = json.loads(self.client.get('/api/book_changes/0.json').content)
-        self.assertEqual(len(changes['updated']),
-                         1,
-                         'Added book not in book_changes.updated')
-
-    def test_deleted_disappears(self):
-        # test deleted book disappears
-        Book.objects.all().delete()
-        changes = json.loads(self.client.get('/api/book_changes/0.json').content)
-        self.assertEqual(len(changes), 1,
-                         'Deleted book should disappear.')
-
-    def test_shelf(self):
-        changed_at = self.book.changed_at
-
-        # putting on a shelf should not update changed_at
-        shelf = Tag.objects.create(category='set', slug='shelf')
-        self.book.tags = [shelf]
-        self.assertEqual(self.book.changed_at,
-                         changed_at)
-
-class TagChangesTests(ApiTest):
-
-    def setUp(self):
-        super(TagChangesTests, self).setUp()
-        self.tag = Tag.objects.create(category='author')
-        self.book = Book.objects.create()
-        self.book.tags = [self.tag]
-        self.book.save()
-
-    def test_added(self):
-        # test tag in tag_changes.added
-        changes = json.loads(self.client.get('/api/tag_changes/0.json').content)
-        self.assertEqual(len(changes['updated']),
-                         1,
-                         'Added tag not in tag_changes.updated')
-
-    def test_empty_disappears(self):
-        self.book.tags = []
-        self.book.save()
-        changes = json.loads(self.client.get('/api/tag_changes/0.json').content)
-        self.assertEqual(len(changes), 1,
-                         'Empty or deleted tag should disappear.')
-
-
-
-class BookTests(TestCase):
+class BookTests(ApiTest):
 
     def setUp(self):
         self.tag = Tag.objects.create(category='author', slug='joe')
@@ -108,23 +42,23 @@ class BookTests(TestCase):
         self.book_tagged.save()
 
     def test_book_list(self):
-        books = json.loads(self.client.get('/api/books/').content)
+        books = self.load_json('/api/books/')
         self.assertEqual(len(books), 2,
                          'Wrong book list.')
 
     def test_tagged_books(self):
-        books = json.loads(self.client.get('/api/authors/joe/books/').content)
+        books = self.load_json('/api/authors/joe/books/')
 
         self.assertEqual([b['title'] for b in books], [self.book_tagged.title],
                         'Wrong tagged book list.')
 
     def test_detail(self):
-        book = json.loads(self.client.get('/api/books/a-book/').content)
+        book = self.load_json('/api/books/a-book/')
         self.assertEqual(book['title'], self.book.title,
                         'Wrong book details.')
 
 
-class TagTests(TestCase):
+class TagTests(ApiTest):
 
     def setUp(self):
         self.tag = Tag.objects.create(category='author', slug='joe', name='Joe')
@@ -133,12 +67,12 @@ class TagTests(TestCase):
         self.book.save()
 
     def test_tag_list(self):
-        tags = json.loads(self.client.get('/api/authors/').content)
+        tags = self.load_json('/api/authors/')
         self.assertEqual(len(tags), 1,
                         'Wrong tag list.')
 
     def test_tag_detail(self):
-        tag = json.loads(self.client.get('/api/authors/joe/').content)
+        tag = self.load_json('/api/authors/joe/')
         self.assertEqual(tag['name'], self.tag.name,
                         'Wrong tag details.')
 
index 7c12c01..e9b106c 100644 (file)
@@ -7,16 +7,12 @@ from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import TemplateView
 from piston.authentication import OAuthAuthentication, oauth_access_token
 from piston.resource import Resource
-
+from ssify import ssi_included
 from api import handlers
 from api.helpers import CsrfExemptResource
 
 auth = OAuthAuthentication(realm="Wolne Lektury")
 
-book_changes_resource = Resource(handler=handlers.BookChangesHandler)
-tag_changes_resource = Resource(handler=handlers.TagChangesHandler)
-changes_resource = Resource(handler=handlers.ChangesHandler)
-
 book_list_resource = CsrfExemptResource(handler=handlers.BooksHandler, authentication=auth)
 ebook_list_resource = Resource(handler=handlers.EBooksHandler)
 #book_list_resource = Resource(handler=handlers.BooksHandler)
@@ -33,6 +29,21 @@ fragment_list_resource = Resource(handler=handlers.FragmentsHandler)
 
 picture_resource = CsrfExemptResource(handler=handlers.PictureHandler, authentication=auth)
 
+
+@ssi_included
+def incl(request, model, pk, emitter_format):
+    resource = {
+        'book': book_list_resource,
+        'fragment': fragment_list_resource,
+        'tag': tag_list_resource,
+        }[model]
+    resp = resource(request, pk=pk, emitter_format=emitter_format)
+    if emitter_format == 'xml':
+        # Ugly, but quick way of stripping <?xml?> header and <response> tags.
+        resp.content = resp.content[49:-11]
+    return resp
+
+
 urlpatterns = patterns(
     'piston.authentication',
     url(r'^oauth/request_token/$', 'oauth_request_token'),
@@ -41,19 +52,13 @@ urlpatterns = patterns(
 
 ) + patterns('',
     url(r'^$', TemplateView.as_view(template_name='api/main.html'), name='api'),
-
-
-    # changes handlers
-    url(r'^book_changes/(?P<since>\d*?)\.(?P<emitter_format>xml|json|yaml)$', book_changes_resource),
-    url(r'^tag_changes/(?P<since>\d*?)\.(?P<emitter_format>xml|json|yaml)$', tag_changes_resource),
-    # used by mobile app
-    url(r'^changes/(?P<since>\d*?)\.(?P<emitter_format>xml|json|yaml)$', changes_resource),
+    url(r'^include/(?P<model>book|fragment|tag)/(?P<pk>\d+)\.(?P<lang>.+)\.(?P<emitter_format>xml|json)$',
+        incl, name='api_include'),
 
     # info boxes (used by mobile app)
     url(r'book/(?P<id>\d*?)/info\.html$', 'catalogue.views.book_info'),
     url(r'tag/(?P<id>\d*?)/info\.html$', 'catalogue.views.tag_info'),
 
-
     # books by collections
     url(r'^collections/$', collection_list_resource, name="api_collections"),
     url(r'^collections/(?P<slug>[^/]+)/$', collection_resource, name="api_collection"),
index 2e7a89f..f3d44e1 100644 (file)
@@ -7,6 +7,9 @@ from django.conf import settings as settings
 from catalogue.utils import AppSettings
 
 
+default_app_config = 'catalogue.apps.CatalogueConfig'
+
+
 class Settings(AppSettings):
     """Default settings for catalogue app."""
     DEFAULT_LANGUAGE = u'pol'
diff --git a/apps/catalogue/apps.py b/apps/catalogue/apps.py
new file mode 100644 (file)
index 0000000..54bfc8f
--- /dev/null
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.apps import AppConfig
+
+class CatalogueConfig(AppConfig):
+    name = 'catalogue'
+
+    def ready(self):
+        from . import signals
index d5cec2e..884ecef 100644 (file)
@@ -204,7 +204,7 @@ class BuildHtml(BuildEbook):
 
                 new_fragment.save()
                 new_fragment.tags = set(meta_tags + themes)
-            book.html_built.send(sender=book)
+            book.html_built.send(sender=type(self), instance=book)
             return True
         return False
 
@@ -235,19 +235,3 @@ class OverwritingFieldFile(FieldFile):
 
 class OverwritingFileField(models.FileField):
     attr_class = OverwritingFieldFile
-
-
-try:
-    # check for south
-    from south.modelsinspector import add_introspection_rules
-except ImportError:
-    pass
-else:
-    add_introspection_rules([
-        (
-            [EbookField],
-            [],
-            {'format_name': ('format_name', {})}
-        )
-    ], ["^catalogue\.fields\.EbookField"])
-    add_introspection_rules([], ["^catalogue\.fields\.OverwritingFileField"])
index ddfa482..7ca2cbd 100644 (file)
-from django.db import connection
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
 from django.contrib.contenttypes.models import ContentType
-from django.utils.translation import get_language
-from picture.models import Picture, PictureArea
-from catalogue.models import Fragment, Tag, Book
+from django.db.models import Count
+from .models import Tag, Book
 
 
-def _get_tag_relations_sql(tags):
-    select = """
-        SELECT Rx.object_id, Rx.content_type_id
-        FROM catalogue_tag_relation Rx"""
-    joins = []
-    where = ['WHERE Rx.tag_id = %d' % tags[0].pk]
-    for i, tag in enumerate(tags[1:]):
-        joins.append('INNER JOIN catalogue_tag_relation TR%(i)d '
-            'ON TR%(i)d.object_id = Rx.object_id '
-            'AND TR%(i)d.content_type_id = Rx.content_type_id' % {'i': i})
-        where.append('AND TR%d.tag_id = %d' % (i, tag.pk))
-    return " ".join([select] + joins + where)
+BOOK_CATEGORIES = ('author', 'epoch', 'genre', 'kind')
 
 
+def get_top_level_related_tags(tags=None, categories=BOOK_CATEGORIES):
+    """
+    Finds tags related to given tags through books, and counts their usage.
 
-def get_related_tags(tags):
-    # Get Tag fields for constructing tags in a raw query.
-    tag_fields = ('id', 'category', 'slug', 'sort_key', 'name_%s' % get_language())
-    tag_fields = ', '.join(
-            'T.%s' % connection.ops.quote_name(field)
-        for field in tag_fields)
-    tag_ids = tuple(t.pk for t in tags)
-
-    # This is based on fragments/areas sharing their works tags
-    qs = Tag.objects.raw('''
-        SELECT ''' + tag_fields + ''', COUNT(T.id) count
-        FROM (
-            -- R: TagRelations of all objects tagged with the given tags.
-            WITH R AS (
-                ''' + _get_tag_relations_sql(tags) + '''
-            )
-
-            SELECT ''' + tag_fields + ''', MAX(R4.object_id) ancestor
-
-            FROM R R1
-
-            -- R2: All tags of the found objects.
-            JOIN catalogue_tag_relation R2
-                ON R2.object_id = R1.object_id
-                    AND R2.content_type_id = R1.content_type_id
-
-            -- Tag data for output.
-            JOIN catalogue_tag T
-                ON T.id=R2.tag_id
-
-            -- Special case for books:
-            -- We want to exclude from output all the relations
-            -- between a book and a tag, if there's a relation between
-            -- the the book's ancestor and the tag in the result.
-            LEFT JOIN catalogue_book_ancestor A
-                ON A.from_book_id = R1.object_id
-                    AND R1.content_type_id = %s
-            LEFT JOIN catalogue_tag_relation R3
-                ON R3.tag_id = R2.tag_id
-                    AND R3.content_type_id = R1.content_type_id
-                    AND R3.object_id = A.to_book_id
-            LEFT JOIN R R4
-                ON R4.object_id = R3.object_id
-                AND R4.content_type_id = R3.content_type_id
-
-            WHERE
-                -- Exclude from the result the tags we started with.
-                R2.tag_id NOT IN %s
-                -- Special case for books: exclude descendants.
-                -- AND R4.object_id IS NULL
-                AND (
-                    -- Only count fragment tags on fragments
-                    -- and book tags for books.
-                    (R2.content_type_id IN %s AND T.category IN %s)
-                    OR
-                    (R2.content_type_id IN %s AND T.category IN %s)
-                )
-
-            GROUP BY T.id, R2.object_id, R2.content_type_id
-
-        ) T
-        -- Now group by tag and count occurencies.
-        WHERE ancestor IS NULL
-        GROUP BY ''' + tag_fields + '''
-        ORDER BY T.sort_key
-        ''', params=(
-            ContentType.objects.get_for_model(Book).pk,
-            tag_ids,
-            tuple(ContentType.objects.get_for_model(model).pk
-                for model in (Fragment, PictureArea)),
-            ('theme', 'object'),
-            tuple(ContentType.objects.get_for_model(model).pk
-                for model in (Book, Picture)),
-            ('author', 'epoch', 'genre', 'kind'),
-        ))
-    return qs
+    Takes ancestry into account: if a tag is applied to a book, its
+    usage on the book's descendants is ignored.
 
+    This is tested for PostgreSQL 9.1+, and might not work elsewhere.
+    It particular, it uses raw SQL using WITH clause, which is
+    supported in SQLite from v. 3.8.3, and is missing in MySQL.
+    http://bugs.mysql.com/bug.php?id=16244
 
-def get_fragment_related_tags(tags):
-    tag_fields = ', '.join(
-        'T.%s' % (connection.ops.quote_name(field.column))
-        for field in Tag._meta.fields)
+    """
+    # First, find all tag relations of relevant books.
+    bct = ContentType.objects.get_for_model(Book)
+    relations = Tag.intermediary_table_model.objects.filter(
+        content_type=bct)
+    if tags is not None:
+        tagged_books = Book.tagged.with_all(tags).only('pk')
+        relations = relations.filter(
+            object_id__in=tagged_books).exclude(
+            tag_id__in=[tag.pk for tag in tags])
 
-    tag_ids = tuple(t.pk for t in tags)
-        # This is based on fragments/areas sharing their works tags
-    return Tag.objects.raw('''
-        SELECT T.*, COUNT(T.id) count
-        FROM (
-
-            SELECT T.*
-
-            -- R1: TagRelations of all objects tagged with the given tags.
-            FROM (
-                ''' + _get_tag_relations_sql(tags) + '''
-            ) R1
-
-            -- R2: All tags of the found objects.
-            JOIN catalogue_tag_relation R2
-                ON R2.object_id = R1.object_id
-                    AND R2.content_type_id = R1.content_type_id
-
-            -- Tag data for output.
-            JOIN catalogue_tag T
-                ON T.id = R2.tag_id
+    rel_sql, rel_params = relations.query.sql_with_params()
 
-            WHERE
-                -- Exclude from the result the tags we started with.
-                R2.tag_id NOT IN %s
-            GROUP BY T.id, R2.object_id, R2.content_type_id
+    # Exclude those relations between a book and a tag,
+    # for which there is a relation between the book's ancestor
+    # and the tag and 
 
-        ) T
-        -- Now group by tag and count occurencies.
-        GROUP BY ''' + tag_fields + '''
-        ORDER BY T.sort_key
-        ''', params=(
-            tag_ids,
-        ))
-
-
-def tags_usage_for_books(categories):
-    tag_fields = ', '.join(
-            'T.%s' % (connection.ops.quote_name(field.column))
-        for field in Tag._meta.fields)
-
-    # This is based on fragments/areas sharing their works tags
     return Tag.objects.raw('''
-        SELECT T.*, COUNT(T.id) count
-        FROM (
-            SELECT T.*
-
-            FROM catalogue_tag_relation R1
-
-            -- Tag data for output.
-            JOIN catalogue_tag T
-                ON T.id=R1.tag_id
-
-            -- We want to exclude from output all the relations
-            -- between a book and a tag, if there's a relation between
-            -- the the book's ancestor and the tag in the result.
-            LEFT JOIN catalogue_book_ancestor A
-                ON A.from_book_id=R1.object_id
-            LEFT JOIN catalogue_tag_relation R3
-                ON R3.tag_id = R1.tag_id
-                    AND R3.content_type_id = R1.content_type_id
-                    AND R3.object_id = A.to_book_id
-
-            WHERE
-                R1.content_type_id = %s
-                -- Special case for books: exclude descendants.
-                AND R3.object_id IS NULL
-                AND T.category IN %s
-
-            -- TODO:
-            -- Shouldn't it just be 'distinct'?
-            -- Maybe it's faster this way.
-            GROUP BY T.id, R1.object_id, R1.content_type_id
-
-        ) T
-        -- Now group by tag and count occurencies.
-        GROUP BY ''' + tag_fields + '''
-        ORDER BY T.sort_key
-        ''', params=(
-            ContentType.objects.get_for_model(Book).pk,
-            tuple(categories),
-        ))
-
-
-def tags_usage_for_works(categories):
-    tag_fields = ', '.join(
-            'T.%s' % (connection.ops.quote_name(field.column))
-        for field in Tag._meta.fields)
-
-    return Tag.objects.raw('''
-        SELECT T.*, COUNT(T.id) count
-        FROM (
-
-            SELECT T.*
-
-            FROM catalogue_tag_relation R1
-
-            -- Tag data for output.
-            JOIN catalogue_tag T
-                ON T.id = R1.tag_id
-
-            -- Special case for books:
-            -- We want to exclude from output all the relations
-            -- between a book and a tag, if there's a relation between
-            -- the the book's ancestor and the tag in the result.
-            LEFT JOIN catalogue_book_ancestor A
-                ON A.from_book_id = R1.object_id
-                    AND R1.content_type_id = %s
-            LEFT JOIN catalogue_tag_relation R3
-                ON R3.tag_id = R1.tag_id
-                    AND R3.content_type_id = R1.content_type_id
-                    AND R3.object_id = A.to_book_id
-
-            WHERE
-                R1.content_type_id IN %s
-                -- Special case for books: exclude descendants.
-                AND R3.object_id IS NULL
-                AND T.category IN %s
-
-            -- TODO:
-            -- Shouldn't it just be 'distinct'?
-            -- Maybe it's faster this way.
-            GROUP BY T.id, R1.object_id, R1.content_type_id
-
-        ) T
-        -- Now group by tag and count occurencies.
-        GROUP BY ''' + tag_fields + '''
-        ORDER BY T.sort_key
-       
-        ''', params=(
-            ContentType.objects.get_for_model(Book).pk,
-            tuple(ContentType.objects.get_for_model(model).pk for model in (Book, Picture)),
-            categories,
-        ))
-
-
-def tags_usage_for_fragments(categories):
-    return Tag.objects.raw('''
-        SELECT t.*, count(t.id)
-        from catalogue_tag_relation r
-        join catalogue_tag t
-            on t.id = r.tag_id
-        where t.category IN %s
-        group by t.id
-        order by t.sort_key
-        ''', params=(
-            categories,
-        ))
+        WITH AllTagged AS (''' + rel_sql + ''')
+        SELECT catalogue_tag.*, COUNT(catalogue_tag.id) AS count
+        FROM catalogue_tag, AllTagged
+        WHERE catalogue_tag.id=AllTagged.tag_id
+            AND catalogue_tag.category IN %s
+            AND NOT EXISTS (
+                SELECT AncestorTagged.id
+                FROM catalogue_book_ancestor Ancestor,
+                    AllTagged AncestorTagged
+                WHERE Ancestor.from_book_id=AllTagged.object_id
+                    AND AncestorTagged.content_type_id=%s
+                    AND AncestorTagged.object_id=Ancestor.to_book_id
+                    AND AncestorTagged.tag_id=AllTagged.tag_id
+            )
+        GROUP BY catalogue_tag.id
+        ORDER BY sort_key''', rel_params + (categories, bct.pk))
index 51fcd94..6ae2b9a 100644 (file)
@@ -55,7 +55,7 @@ class Command(BaseCommand):
                         print "Is:       ", ", ".join(ancestors)
                         print "Should be:", ", ".join(parents)
                     if not options['dry_run']:
-                        book.fix_tree_tags()
+                        book.repopulate_ancestors()
                         if options['verbose']:
                             print "Fixed."
                     if options['verbose']:
index 4aa5828..9f304c3 100644 (file)
@@ -4,44 +4,6 @@ from __future__ import unicode_literals
 from django.db import models, migrations
 
 
-def fix_tree_tags(apps, schema_editor):
-    """Fixes the ancestry cache."""
-    # TODO: table names
-    from django.db import connection, transaction
-    if connection.vendor == 'postgres':
-        cursor = connection.cursor()
-        cursor.execute("""
-            WITH RECURSIVE ancestry AS (
-                SELECT book.id, book.parent_id
-                FROM catalogue_book AS book
-                WHERE book.parent_id IS NOT NULL
-                UNION
-                SELECT ancestor.id, book.parent_id
-                FROM ancestry AS ancestor, catalogue_book AS book
-                WHERE ancestor.parent_id = book.id
-                    AND book.parent_id IS NOT NULL
-                )
-            INSERT INTO catalogue_book_ancestor
-                (from_book_id, to_book_id)
-                SELECT id, parent_id
-                FROM ancestry
-                ORDER BY id;
-            """)
-    else:
-        Book = apps.get_model("catalogue", "Book")
-        for b in Book.objects.exclude(parent=None):
-            parent = b.parent
-            while parent is not None:
-                b.ancestor.add(parent)
-                parent = parent.parent
-
-
-def remove_book_tags(apps, schema_editor):
-    Tag = apps.get_model("catalogue", "Tag")
-    Book = apps.get_model("catalogue", "Book")
-    Tag.objects.filter(category='book').delete()
-
-
 class Migration(migrations.Migration):
 
     dependencies = [
@@ -55,26 +17,4 @@ class Migration(migrations.Migration):
             field=models.ManyToManyField(related_name=b'descendant', null=True, editable=False, to='catalogue.Book', blank=True),
             preserve_default=True,
         ),
-
-        migrations.RunPython(fix_tree_tags),
-        migrations.RunPython(remove_book_tags),
-
-        migrations.AlterField(
-            model_name='tag',
-            name='category',
-            field=models.CharField(db_index=True, max_length=50, verbose_name='Category', choices=[(b'author', 'author'), (b'epoch', 'period'), (b'kind', 'form'), (b'genre', 'genre'), (b'theme', 'motif'), (b'set', 'set'), (b'thing', 'thing')]),
-        ),
-
-        migrations.RemoveField(
-            model_name='tag',
-            name='book_count',
-        ),
-        migrations.RemoveField(
-            model_name='tag',
-            name='picture_count',
-        ),
-        migrations.RemoveField(
-            model_name='book',
-            name='_related_info',
-        ),
     ]
diff --git a/apps/catalogue/migrations/0003_populate_ancestors.py b/apps/catalogue/migrations/0003_populate_ancestors.py
new file mode 100644 (file)
index 0000000..b611757
--- /dev/null
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+def populate_ancestors(apps, schema_editor):
+    """Fixes the ancestry cache."""
+    # TODO: table names
+    from django.db import connection, transaction
+    if connection.vendor == 'postgres':
+        cursor = connection.cursor()
+        cursor.execute("""
+            WITH RECURSIVE ancestry AS (
+                SELECT book.id, book.parent_id
+                FROM catalogue_book AS book
+                WHERE book.parent_id IS NOT NULL
+                UNION
+                SELECT ancestor.id, book.parent_id
+                FROM ancestry AS ancestor, catalogue_book AS book
+                WHERE ancestor.parent_id = book.id
+                    AND book.parent_id IS NOT NULL
+                )
+            INSERT INTO catalogue_book_ancestor
+                (from_book_id, to_book_id)
+                SELECT id, parent_id
+                FROM ancestry
+                ORDER BY id;
+            """)
+    else:
+        Book = apps.get_model("catalogue", "Book")
+        for book in Book.objects.exclude(parent=None):
+            parent = book.parent
+            while parent is not None:
+                book.ancestor.add(parent)
+                parent = parent.parent
+
+
+def remove_book_tags(apps, schema_editor):
+    Tag = apps.get_model("catalogue", "Tag")
+    Book = apps.get_model("catalogue", "Book")
+    Tag.objects.filter(category='book').delete()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('catalogue', '0002_book_ancestor'),
+    ]
+
+    operations = [
+        migrations.RunPython(populate_ancestors),
+        migrations.RunPython(remove_book_tags),
+    ]
diff --git a/apps/catalogue/migrations/0004_remove_booktags_count_related_info.py b/apps/catalogue/migrations/0004_remove_booktags_count_related_info.py
new file mode 100644 (file)
index 0000000..916224b
--- /dev/null
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('catalogue', '0003_populate_ancestors'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='tag',
+            name='category',
+            field=models.CharField(db_index=True, max_length=50, verbose_name='Category', choices=[(b'author', 'author'), (b'epoch', 'period'), (b'kind', 'form'), (b'genre', 'genre'), (b'theme', 'motif'), (b'set', 'set'), (b'thing', 'thing')]),
+        ),
+
+        migrations.RemoveField(
+            model_name='tag',
+            name='book_count',
+        ),
+        migrations.RemoveField(
+            model_name='tag',
+            name='picture_count',
+        ),
+        migrations.RemoveField(
+            model_name='book',
+            name='_related_info',
+        ),
+    ]
index 7651a9f..73b5109 100644 (file)
@@ -8,4 +8,3 @@ from catalogue.models.fragment import Fragment
 from catalogue.models.book import Book
 from catalogue.models.collection import Collection
 from catalogue.models.source import Source
-from catalogue.models.listeners import *
index e499afc..3c32481 100644 (file)
@@ -6,7 +6,6 @@ from collections import OrderedDict
 from random import randint
 import re
 from django.conf import settings
-from django.core.cache import caches
 from django.db import connection, models, transaction
 from django.db.models import permalink
 import django.dispatch
@@ -15,18 +14,17 @@ from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext_lazy as _
 import jsonfield
 from fnpdjango.storage import BofhFileSystemStorage
+from ssify import flush_ssi_includes
+from newtagging import managers
 from catalogue import constants
 from catalogue.fields import EbookField
 from catalogue.models import Tag, Fragment, BookMedia
 from catalogue.utils import create_zip
 from catalogue import app_settings
 from catalogue import tasks
-from newtagging import managers
 
 bofh_storage = BofhFileSystemStorage()
 
-permanent_cache = caches['permanent']
-
 
 def _cover_upload_to(i, n):
     return 'book/cover/%s.jpg' % i.slug
@@ -84,6 +82,8 @@ class Book(models.Model):
     html_built = django.dispatch.Signal()
     published = django.dispatch.Signal()
 
+    short_html_url_name = 'catalogue_book_short'
+
     class AlreadyExists(Exception):
         pass
 
@@ -96,16 +96,19 @@ class Book(models.Model):
     def __unicode__(self):
         return self.title
 
-    def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
+    def save(self, force_insert=False, force_update=False, **kwargs):
         from sortify import sortify
 
         self.sort_key = sortify(self.title)
         self.title = unicode(self.title) # ???
 
-        ret = super(Book, self).save(force_insert, force_update, **kwargs)
+        try:
+            author = self.tags.filter(category='author')[0].sort_key
+        except IndexError:
+            author = u''
+        self.sort_key_author = author
 
-        if reset_short_html:
-            self.reset_short_html()
+        ret = super(Book, self).save(force_insert, force_update, **kwargs)
 
         return ret
 
@@ -152,20 +155,6 @@ class Book(models.Model):
     def get_daisy(self):
         return self.get_media("daisy")
 
-    def reset_short_html(self):
-        if self.id is None:
-            return
-
-        # Fragment.short_html relies on book's tags, so reset it here too
-        for fragm in self.fragments.all().iterator():
-            fragm.reset_short_html()
-
-        try:
-            author = self.tags.filter(category='author')[0].sort_key
-        except IndexError:
-            author = u''
-        type(self).objects.filter(pk=self.pk).update(sort_key_author=author)
-
     def has_description(self):
         return len(self.description) > 0
     has_description.short_description = _('description')
@@ -318,11 +307,10 @@ class Book(models.Model):
             child.parent = None
             child.parent_number = 0
             child.save()
-            tasks.fix_tree_tags.delay(child)
             if old_cover:
                 notify_cover_changed.append(child)
 
-        cls.fix_tree_tags()
+        cls.repopulate_ancestors()
 
         # No saves beyond this point.
 
@@ -347,11 +335,11 @@ class Book(models.Model):
         for child in notify_cover_changed:
             child.parent_cover_changed()
 
-        cls.published.send(sender=book)
+        cls.published.send(sender=cls, instance=book)
         return book
 
     @classmethod
-    def fix_tree_tags(cls):
+    def repopulate_ancestors(cls):
         """Fixes the ancestry cache."""
         # TODO: table names
         with transaction.atomic():
@@ -383,6 +371,24 @@ class Book(models.Model):
                         b.ancestor.add(parent)
                         parent = parent.parent
 
+    def flush_includes(self, languages=True):
+        if not languages:
+            return
+        if languages is True:
+            languages = [lc for (lc, _ln) in settings.LANGUAGES]
+        flush_ssi_includes([
+            template % (self.pk, lang)
+            for template in [
+                '/katalog/b/%d/mini.%s.html',
+                '/katalog/b/%d/mini_nolink.%s.html',
+                '/katalog/b/%d/short.%s.html',
+                '/katalog/b/%d/wide.%s.html',
+                '/api/include/book/%d.%s.json',
+                '/api/include/book/%d.%s.xml',
+                ]
+            for lang in languages
+            ])
+
     def cover_info(self, inherit=True):
         """Returns a dictionary to serve as fallback for BookInfo.
 
index acb01b6..098501e 100644 (file)
@@ -2,8 +2,10 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
+from django.conf import settings
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
+from ssify import flush_ssi_includes
 
 
 class Collection(models.Model):
@@ -36,3 +38,12 @@ class Collection(models.Model):
         slugs = [slug.rstrip('/').rsplit('/', 1)[-1] if '/' in slug else slug
                     for slug in slugs]
         return models.Q(slug__in=slugs)
+
+    def flush_includes(self, languages=True):
+        if not languages:
+            return
+        if languages is True:
+            languages = [lc for (lc, _ln) in settings.LANGUAGES]
+
+        flush_ssi_includes([
+            '/katalog/%s.json' % lang for lang in languages])
index 283a7d9..a3dbdea 100644 (file)
@@ -4,17 +4,12 @@
 #
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
-from django.core.cache import caches
 from django.core.urlresolvers import reverse
 from django.db import models
-from django.template.loader import render_to_string
-from django.utils.safestring import mark_safe
-from django.utils.translation import get_language, ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _
 from newtagging import managers
 from catalogue.models import Tag
-
-
-permanent_cache = caches['permanent']
+from ssify import flush_ssi_includes
 
 
 class Fragment(models.Model):
@@ -29,6 +24,8 @@ class Fragment(models.Model):
     tags = managers.TagDescriptor(Tag)
     tag_relations = GenericRelation(Tag.intermediary_table_model)
 
+    short_html_url_name = 'catalogue_fragment_short'
+
     class Meta:
         ordering = ('book', 'anchor',)
         verbose_name = _('fragment')
@@ -38,30 +35,21 @@ class Fragment(models.Model):
     def get_absolute_url(self):
         return '%s#m%s' % (reverse('book_text', args=[self.book.slug]), self.anchor)
 
-    def reset_short_html(self):
-        if self.id is None:
-            return
-
-        cache_key = "Fragment.short_html/%d/%s"
-        for lang, langname in settings.LANGUAGES:
-            permanent_cache.delete(cache_key % (self.id, lang))
-
     def get_short_text(self):
         """Returns short version of the fragment."""
         return self.short_text if self.short_text else self.text
 
-    def short_html(self):
-        if self.id:
-            cache_key = "Fragment.short_html/%d/%s" % (self.id, get_language())
-            short_html = permanent_cache.get(cache_key)
-        else:
-            short_html = None
-
-        if short_html is not None:
-            return mark_safe(short_html)
-        else:
-            short_html = unicode(render_to_string('catalogue/fragment_short.html',
-                {'fragment': self}))
-            if self.id:
-                permanent_cache.set(cache_key, short_html)
-            return mark_safe(short_html)
+    def flush_includes(self, languages=True):
+        if not languages:
+            return
+        if languages is True:
+            languages = [lc for (lc, _ln) in settings.LANGUAGES]
+        flush_ssi_includes([
+            template % (self.pk, lang)
+            for template in [
+                '/katalog/f/%d/short.%s.html',
+                '/api/include/fragment/%d.%s.json',
+                '/api/include/fragment/%d.%s.xml',
+                ]
+            for lang in languages
+            ])
diff --git a/apps/catalogue/models/listeners.py b/apps/catalogue/models/listeners.py
deleted file mode 100644 (file)
index d414eb2..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-# -*- coding: utf-8 -*-
-# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.conf import settings
-from django.core.cache import caches
-from django.db.models.signals import post_save, pre_delete, post_delete
-import django.dispatch
-from catalogue.models import BookMedia, Book, Collection
-from catalogue.utils import delete_from_cache_by_language
-
-
-permanent_cache = caches['permanent']
-
-
-def _pre_delete_handler(sender, instance, **kwargs):
-    """ refresh Book on BookMedia delete """
-    if sender == BookMedia:
-        instance.book.save()
-pre_delete.connect(_pre_delete_handler)
-
-
-def _post_delete_handler(sender, instance, **kwargs):
-    """ refresh Book on BookMedia delete """
-    if sender == Collection:
-        delete_from_cache_by_language(permanent_cache, 'catalogue.collection:%s/%%s' % instance.slug)
-        delete_from_cache_by_language(permanent_cache, 'catalogue.catalogue/%s')
-post_delete.connect(_post_delete_handler)
-
-
-def _post_save_handler(sender, instance, **kwargs):
-    """ refresh all the short_html stuff on BookMedia update """
-    if sender == BookMedia:
-        instance.book.save()
-        delete_from_cache_by_language(permanent_cache, 'catalogue.audiobook_list/%s')
-        delete_from_cache_by_language(permanent_cache, 'catalogue.daisy_list/%s')
-    elif sender == Collection:
-        delete_from_cache_by_language(permanent_cache, 'catalogue.collection:%s/%%s' % instance.slug)
-        delete_from_cache_by_language(permanent_cache, 'catalogue.catalogue/%s')
-post_save.connect(_post_save_handler)
-
-
-def post_publish(sender, **kwargs):
-    delete_from_cache_by_language(permanent_cache, 'catalogue.book_list/%s')
-    delete_from_cache_by_language(permanent_cache, 'catalogue.catalogue/%s')
-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):
-        """ remove the book from search index, when it is deleted."""
-        from search.index import Index
-        idx = Index()
-        idx.remove_book(instance)
-        idx.index_tags()
index d131613..9aff4ef 100644 (file)
@@ -19,3 +19,27 @@ class Source(models.Model):
 
     def __unicode__(self):
         return self.netloc
+
+    def save(self, *args, **kwargs):
+        from catalogue.models import Book
+        try:
+            str(self.pk)
+            old_self = type(self).objects.get(pk=self)
+        except type(self).DoesNotExist:
+            old_name = u''
+            old_netloc = self.netloc
+        else:
+            old_name = old_self.name
+            old_netloc = old_self.netloc
+
+        ret = super(Source, self).save(*args, **kwargs)
+
+        # If something really changed here, find relevant books
+        # and invalidate their cached includes.
+        if old_name != self.name or old_netloc != self.netloc:
+            for book in Book.objects.all():
+                source = book.extra_info.get('source_url', '')
+                if self.netloc in source or (old_netloc != self.netloc
+                        and old_netloc in source):
+                    book.flush_includes()
+        return ret
index fb25118..0e9442f 100644 (file)
@@ -3,11 +3,14 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.conf import settings
+from django.core.cache import caches
 from django.contrib.auth.models import User
 from django.db import models
 from django.db.models import permalink
+from django.dispatch import Signal
 from django.utils.translation import ugettext_lazy as _
 from newtagging.models import TagBase
+from ssify import flush_ssi_includes
 
 
 # Those are hard-coded here so that makemessages sees them.
@@ -42,6 +45,8 @@ class Tag(TagBase):
     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)
 
+    after_change = Signal(providing_args=['instance', 'languages'])
+
     class UrlDeprecationWarning(DeprecationWarning):
         pass
 
@@ -63,6 +68,60 @@ class Tag(TagBase):
         unique_together = (("slug", "category"),)
         app_label = 'catalogue'
 
+    def save(self, *args, **kwargs):
+        flush_cache = flush_all_includes = False
+        if self.pk and self.category != 'set':
+            # Flush the whole views cache.
+            # Seem a little harsh, but changed tag names, descriptions
+            # and links come up at any number of places.
+            flush_cache = True
+
+            # Find in which languages we need to flush related includes.
+            old_self = type(self).objects.get(pk=self.pk)
+            # Category shouldn't normally be changed, but just in case.
+            if self.category != old_self.category:
+                flush_all_includes = True
+            languages_changed = self.languages_changed(old_self)
+
+        ret = super(Tag, self).save(*args, **kwargs)
+
+        if flush_cache:
+            caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
+            if flush_all_includes:
+                flush_ssi_includes()
+            else:
+                self.flush_includes()
+            self.after_change.send(sender=type(self), instance=self, languages=languages_changed)
+
+        return ret
+
+    def languages_changed(self, old):
+        all_langs = [lc for (lc, _ln) in settings.LANGUAGES]
+        if (old.category, old.slug) != (self.category, self.slug):
+            return all_langs
+        languages = set()
+        for lang in all_langs:
+            name_field = 'name_%s' % lang
+            if getattr(old, name_field) != getattr(self, name_field):
+                languages.add(lang)
+        return languages
+
+    def flush_includes(self, languages=True):
+        if not languages:
+            return
+        if languages is True:
+            languages = [lc for (lc, _ln) in settings.LANGUAGES]
+        flush_ssi_includes([
+            template % (self.pk, lang)
+            for template in [
+                '/api/include/tag/%d.%s.json',
+                '/api/include/tag/%d.%s.xml',
+                ]
+            for lang in languages
+            ])
+        flush_ssi_includes([
+            '/katalog/%s.json' % lang for lang in languages])
+
     def __unicode__(self):
         return self.name
 
diff --git a/apps/catalogue/signals.py b/apps/catalogue/signals.py
new file mode 100644 (file)
index 0000000..7260721
--- /dev/null
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf import settings
+from django.core.cache import caches
+from django.db.models.signals import post_save, post_delete
+from django.dispatch import receiver
+from ssify import flush_ssi_includes
+from newtagging.models import tags_updated
+from picture.models import Picture, PictureArea
+from .models import BookMedia, Book, Collection, Fragment, Tag
+
+
+####
+# BookMedia
+####
+
+
+@receiver([post_save, post_delete], sender=BookMedia)
+def bookmedia_save(sender, instance, **kwargs):
+    instance.book.save()
+
+
+####
+# Collection
+####
+
+
+@receiver(post_save, sender=Collection)
+def collection_save(sender, instance, **kwargs):
+    caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
+    flush_ssi_includes([
+        '/katalog/%s.json' % lang
+        for lang in [lc for (lc, _ln) in settings.LANGUAGES]])
+
+
+@receiver(post_delete, sender=Collection)
+def collection_delete(sender, instance, **kwargs):
+    flush_ssi_includes([
+        '/katalog/%s.json' % lang
+        for lang in [lc for (lc, _ln) in settings.LANGUAGES]])
+
+####
+# Book
+####
+
+
+@receiver(post_save, sender=Book)
+def book_save(sender, instance, **kwargs):
+    # Books come out anywhere.
+    caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
+    instance.flush_includes()
+
+
+@receiver(post_delete, sender=Book)
+def book_delete(sender, instance, **kwargs):
+    caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
+    flush_ssi_includes([
+        '/katalog/%s.json' % lang
+        for lang in [lc for (lc, _ln) in settings.LANGUAGES]])
+
+    if not settings.NO_SEARCH_INDEX:
+        # remove the book from search index, when it is deleted.
+        from search.index import Index
+        idx = Index()
+        idx.remove_book(instance)
+        idx.index_tags()
+
+
+####
+# Tag
+####
+
+
+@receiver(Tag.after_change)
+def tag_after_change(sender, instance, languages, **kwargs):
+    caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
+    flush_ssi_includes([
+        '/katalog/%s.json' % lang
+        for lang in [lc for (lc, _ln) in settings.LANGUAGES]])
+
+    for model in Book, Picture:
+        for instance in model.tagged.with_all([instance]).only('pk'):
+            instance.flush_includes()
+
+    if instance.category == 'author':
+        for model in Fragment, PictureArea:
+            for instance in model.tagged.with_all([instance]).only('pk'):
+                instance.flush_includes()
+
+
+@receiver(tags_updated)
+def receive_tags_updated(sender, instance, affected_tags, **kwargs):
+    categories = set(tag.category for tag in affected_tags
+        if tag.category not in ('set', 'book'))
+    if not categories:
+        return
+
+    caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
+    instance.flush_includes()
+    flush_ssi_includes([
+        '/katalog/%s.json' % lang
+        for lang in [lc for (lc, _ln) in settings.LANGUAGES]])
index 7d18068..159494e 100644 (file)
@@ -18,11 +18,6 @@ def touch_tag(tag):
     type(tag).objects.filter(pk=tag.pk).update(**update_dict)
 
 
-@task(ignore_result=True)
-def fix_tree_tags(book):
-    book.fix_tree_tags()
-
-
 @task
 def index_book(book_id, book_info=None, **kwargs):
     from catalogue.models import Book
index 1358025..75576f4 100644 (file)
@@ -1,7 +1,7 @@
 {% extends "catalogue/book_list.html" %}
 {% load i18n %}
 {% load catalogue_tags %}
-{% load chunks %}
+{% load ssi_include from ssify %}
 
 {% block bodyid %}book-a-list{% endblock %}
 
@@ -17,5 +17,5 @@
 {% block book_list_header %}{% trans "Listing of all audiobooks" %}{% endblock %}
 
 {% block book_list_info %}
-{% chunk 'audiobook-list' %}
+{% ssi_include 'chunk' key='audiobook-list' %}
 {% endblock %}
index 454bce1..398a43c 100644 (file)
@@ -1,6 +1,8 @@
 {% extends "base.html" %}
 {% load i18n %}
 {% load common_tags catalogue_tags %}
+{% load ssify %}
+{% load build_absolute_uri from fnp_common %}
 
 {% block titleextra %}{{ book.pretty_title }}{% endblock %}
 {% block ogimage %}{% if book.cover %}{{ book.cover.url|build_absolute_uri:request }}{% endif %}{% endblock %}
@@ -11,7 +13,7 @@
 
 {% block body %}
 
-{% book_wide book %}
+{% ssi_include 'catalogue_book_wide' pk=book.pk %}
 
 {% work_list book_children %}
 
@@ -20,7 +22,7 @@
 <section class="see-also" style="display: inline-block;">
 <h1>{% trans "Other versions" %}:</h1>
 {% for rel in book.other_versions %}
-    {% book_mini rel %}
+    {% ssi_include 'book_mini' pk=rel.pk %}
 {% endfor %}
 </section>
 {% endif %}
index 260c71f..e29cd50 100644 (file)
@@ -1,18 +1,20 @@
 {% extends "base.html" %}
 {% load i18n %}
 {% load catalogue_tags %}
-{% load chunks %}
+{% load ssi_include from ssify %}
 
 {% block bodyid %}book-a-list{% endblock %}
 
 {% block titleextra %}{% trans "Listing of all works" %}{% endblock %}
 
 {% block body %}
+{% spaceless %}
+
     <h1>{% block book_list_header %}{% trans "Listing of all works" %}{% endblock %}</h1>
 
     <div class="left-column"><div class="normal-text" style="margin-bottom: 2em">
         {% block book_list_info %}
-            {% chunk 'book-list' %}
+            {% ssi_include 'chunk' key='book-list' %}
         {% endblock %}
     </div></div>
 
@@ -32,4 +34,6 @@
       {% endblock %}
     </div>
     <a id="book-list-up" href="#top">{% trans "↑ top ↑" %}</a>
+
+{% endspaceless %}
 {% endblock %}
index 49257b1..88ec16d 100755 (executable)
@@ -1,11 +1,11 @@
+{% spaceless %}
 <div class="book-mini-box">
     <div class="book-mini-box-inner">
     {% if with_link %}
     <a href="{{ book.get_absolute_url }}">
     {% endif %}
         {% if book.cover_thumb %}
-            <img src="{{ book.cover_thumb.url }}"
-                alt="{{ author_str }} – {{ book.title }}" class="cover" />
+            <img src="{{ book.cover_thumb.url }}" alt="{{ author_str }} – {{ book.title }}" class="cover" />
         {% endif %}
         {% if show_lang %}
             <span class="language" title="{{ book.language_name }}">{{ book.language_code }}</span>
@@ -19,5 +19,4 @@
     {% endif %}
     </div>
 </div>
-
-
+{% endspaceless %}
\ No newline at end of file
index 783b14a..357491c 100644 (file)
@@ -1,10 +1,13 @@
-{% extends "catalogue/book_short.html" %}
-{% load i18n catalogue_tags %}
+{% spaceless %}
 
+{% load i18n %}
+{% load inline_tag_list from catalogue_tags %}
+{% load ssi_include from ssify %}
 
-{% block box-class %}search-result{% endblock %}
+<div class="search-result">
+
+{% ssi_include 'catalogue_book_short' pk=book.pk %}
 
-{% block right-column %}
 <div class="snippets">
   {% for hit in hits %}
   {% if hit.snippet %}
@@ -28,5 +31,8 @@
   {% endfor %}
 </div>
 
-{% endblock %}
+<div style="clear: right"></div>
+
+</div>
 
+{% endspaceless %}
\ No newline at end of file
index 58859af..b069cb5 100644 (file)
@@ -1,5 +1,7 @@
+{% spaceless %}
 {% load i18n %}
-{% load catalogue_tags social_tags %}
+{% load catalogue_tags ssify %}
+{% load likes_book book_shelf_tags from social_tags %}
 <div class="{% block box-class %}book-box{% endblock %}">
 <div class="book-box-inner">
 <div class="book-left-column">
 <div class="book-box-body">
 
 
-<div class="star {% if not request.user|likes:book %}un{% endif %}like">
+{% likes_book book.pk as likes %}
+<div class="star {{ likes.if }}{{ likes.else }}un{{ likes.endif }}like">
     <div class="if-like" >
-        <a id="social-book-sets-{{ book.slug }}" data-callback='social-book-sets' class='ajaxable' href='{% url "social_book_sets" book.slug %}'>
-            ★
-        </a>
+        <a id="social-book-sets-{{ book.slug }}" data-callback='social-book-sets' class='ajaxable' href='{% url "social_book_sets" book.slug %}'>★</a>
     </div>
     <div class="if-unlike">
         <form id="social-like-book-{{ book.slug }}" data-callback='social-like-book' method='post' class='ajax-form' action='{% url "social_like_book" book.slug %}'>
-            {% csrf_token %}
+            {% ssi_csrf_token %}
             <button type='submit'>☆</button>
         </form>
     </div>
                     <a href="{{ parent.get_absolute_url }}">{{ parent.title }}</a>{% endfor %}
             </div>
             <div class="title">
-                {% if main_link %}<a href="{{ main_link }}">{% endif %}
-                    {{ book.title }}
-                {% if main_link %}</a>{% endif %}
+                {% if main_link %}<a href="{{ main_link }}">{% endif %}{{ book.title }}{% if main_link %}</a>{% endif %}
             </div>
         </div>
 
 <div class="cover-area">
     {% if book.cover_thumb %}
         {% if main_link %}<a href="{{ main_link }}">{% endif %}
-            <img src="{{ book.cover_thumb.url }}"
-                alt="Cover" class="cover" />
+            <img src="{{ book.cover_thumb.url }}" alt="Cover" class="cover" />
         {% if main_link %}</a>{% endif %}
     {% endif %}
     {% block cover-area-extra %}{% endblock %}
@@ -91,7 +89,7 @@
            {% endspaceless %}
         </div>
     </div>
-    {% shelf_tags book %}
+    {% book_shelf_tags book.pk %}
 
     <ul class="book-box-tools">
         <li class="book-box-read">
     <div class="clearboth"></div>
 </div>
 </div>
+{% endspaceless %}
\ No newline at end of file
index 8dd1546..339cfc7 100644 (file)
@@ -1,6 +1,6 @@
 {% extends "catalogue/viewer_base.html" %}
 {% load i18n %}
-{% load catalogue_tags %}
+{% load catalogue_tags ssify %}
 {% load thumbnail %}
 
 
@@ -49,7 +49,7 @@
 <div id="big-pane" style="">
 
 <article id="main-text">
-{{ book.html_file.read|safe }}
+<!--#include file='{{ book.html_file.url }}'-->
 </article>
 
 <article id="other-text">
@@ -78,7 +78,7 @@
         <li><a class="display-other" 
             data-other="{{ other_version.html_file.url }}"
             href="{% url 'book_text' other_version.slug %}">
-                {% book_mini other_version with_link=False %}
+                {% ssi_include 'catalogue_book_mini_nolink' pk=other_version.pk %}
                 </a>
         </li>
     {% endfor %}
@@ -95,6 +95,6 @@
 </div>
 
 <div class="box" id="book-short">
-    {% book_short book %}
+    {% ssi_include 'catalogue_book_short' pk=book.pk %}
 </div>
 {% endblock footer %}
index 5ce66a3..b84acdb 100644 (file)
@@ -1,7 +1,8 @@
 {% extends "catalogue/book_short.html" %}
 {% load i18n %}
-{% load download_audio tag_list custom_pdf_link_li license_icon source_name from catalogue_tags %}
-{% load cite_promo from social_tags %}
+{% load choose_fragment download_audio tag_list custom_pdf_link_li license_icon source_name from catalogue_tags %}
+{% load choose_cite from social_tags %}
+{% load ssi_include from ssify %}
 
 
 {% block box-class %}book-wide-box{% endblock %}
 
 {% block right-column %}
 <div class="right-column">
-    <div class="quote">
-  {% cite_promo book 1 %}
+  <div class="quote">
+    {% choose_cite book.pk as cite_promo %}
+    {% choose_fragment book.pk unless=cite_promo as fragment_promo %}
+    {{ cite_promo.if }}
+        {% ssi_include 'social_cite' pk=cite_promo %}
+    {{ cite_promo.endif }}
+    {{ fragment_promo.if }}
+        {% ssi_include 'catalogue_fragment_promo' pk=fragment_promo %}
+    {{ fragment_promo.endif }}
   </div>
 
   <div class="other-tools">
index 4f570b2..65d9d6b 100644 (file)
@@ -1,6 +1,6 @@
 {% extends "catalogue/book_list.html" %}
 {% load i18n %}
-{% load chunks %}
+{% load ssi_include from ssify %}
 
 {% block bodyid %}book-a-list{% endblock %}
 
@@ -13,5 +13,5 @@
 {% block book_list_header %}{% trans "Listing of all DAISY files" %}{% endblock %}
 
 {% block book_list_info %}
-{% chunk 'daisy-list' %}
+{% ssi_include 'chunk' key='daisy-list' %}
 {% endblock %}
diff --git a/apps/catalogue/templates/catalogue/latest_blog_posts.html b/apps/catalogue/templates/catalogue/latest_blog_posts.html
deleted file mode 100644 (file)
index d2c90e1..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<ol>
-{% for post in posts %}
-    <li><a href="{{ post.link }}">{{ post.title }}</a></li>
-{% endfor %}
-</ol>
\ No newline at end of file
index b6bc172..5ddbd3b 100644 (file)
@@ -1,3 +1,4 @@
+{% spaceless %}
 {% load i18n static %}
 
 <a id="show-menu" href="{% url 'catalogue' %}">
@@ -7,8 +8,7 @@
 <ul id="menu">
     {% for category, name, hash in categories %}
        <li class="hidden-box-wrapper menu">
-               <a href="{% url 'catalogue' %}#{{ hash }}" class="hidden-box-trigger menu load-menu">
-                       {{ name }}</a>
+               <a href="{% url 'catalogue' %}#{{ hash }}" class="hidden-box-trigger menu load-menu">{{ name }}</a>
                <div class="hidden-box" id="menu-{{ category }}">
             <img src="{% static "img/indicator.gif" %}" alt="{% trans "Please wait…" %}" />
         </div>
     {% endfor %}
 
        <li class="hidden-box-wrapper menu">
-               <a href="{% url 'catalogue' %}#kolekcje" class="hidden-box-trigger menu load-menu">
-                       {% trans "Collections" %}</a>
+               <a href="{% url 'catalogue' %}#kolekcje" class="hidden-box-trigger menu load-menu">{% trans "Collections" %}</a>
                <div class="hidden-box" id="menu-collections">
             <img src="{% static "img/indicator.gif" %}" alt="{% trans "Please wait…" %}" />
         </div>
        </li>
 
        <li class="menu">
-               <a href="{% url 'book_list' %}" class="menu">
-                       {% trans "All books" %}</a>
+               <a href="{% url 'book_list' %}" class="menu">{% trans "All books" %}</a>
        </li>
        <li class="menu">
-               <a href="{% url 'audiobook_list' %}" class="menu">
-                       {% trans "Audiobooks" %}</a>
+               <a href="{% url 'audiobook_list' %}" class="menu">{% trans "Audiobooks" %}</a>
        </li>
        <li class="menu">
-               <a href="{% url 'daisy_list' %}" class="menu">
-                       {% trans "DAISY" %}</a>
+               <a href="{% url 'daisy_list' %}" class="menu">{% trans "DAISY" %}</a>
        </li>
        <li class="menu">
                <a href="{% url 'picture_list_thumb' %}" class="menu">
@@ -41,3 +37,5 @@
        </li>
 
 </ul>
+
+{% endspaceless %}
\ No newline at end of file
index 219876e..48fb2ee 100755 (executable)
@@ -1,10 +1,16 @@
-{% load book_mini from catalogue_tags %}
-
 {% spaceless %}
+{% load catalogue_random_book from catalogue_tags %}
+{% load ssi_include from ssify %}
+
 {% for book in books %}
-    {% book_mini book %}
-{% endfor %}
-{% for book in random_related %}
-    {% book_mini book %}
+    {% ssi_include 'catalogue_book_mini' pk=book.pk %}
 {% endfor %}
+
+{% if random %}
+    {% catalogue_random_book random_excluded as random_pk %}
+    {{ random_pk.if }}
+        {% ssi_include 'catalogue_book_mini' pk=random_pk %}
+    {{ random_pk.endif }}
+{% endif %}
+
 {% endspaceless %}
\ No newline at end of file
index 667de9c..d1e1ded 100644 (file)
@@ -1,6 +1,9 @@
 {% extends "base.html" %}
 {% load i18n %}
-{% load catalogue_tags search_tags pagination_tags %}
+{% load pagination_tags %}
+{% load inline_tag_list from catalogue_tags %}
+{% load book_searched from search_tags %}
+{% load ssi_include from ssify %}
 
 {% block titleextra %}{% trans "Search" %}{% endblock %}
 
@@ -55,7 +58,7 @@
     <div>
       <ol class="work-list">
        {% for result in results.title %}<li class="Book-item">
-         {% book_short result.book %}
+         {% ssi_include 'catalogue_book_short' pk=result.book.pk %}
        </li>{% endfor %}
       </ol>
     </div>
@@ -69,7 +72,7 @@
     </div>
     <div>
       <ol class="work-list">
-       {% for author in results.author %}<li class="Book-item">{% book_short author.book %}</li>{% endfor %}
+       {% for author in results.author %}<li class="Book-item">{% ssi_include 'catalogue_book_short' pk=author.book.pk %}</li>{% endfor %}
       </ol>
     </div>
     {% endif %}
@@ -82,7 +85,7 @@
     </div>
     <div>
       <ol class="work-list">
-       {% for translator in results.translator %}<li class="Book-item">{% book_short translator.book %}</li>{% endfor %}
+       {% for translator in results.translator %}<li class="Book-item">{% ssi_include 'catalogue_book_short' pk=translator.book.pk %}</li>{% endfor %}
       </ol>
     </div>
     {% endif %}
index 2143048..e0fecc0 100644 (file)
@@ -1,10 +1,12 @@
+{% spaceless %}
+
 {% load i18n %}
 {% load catalogue_tags %}
 {% if one_tag %}
     <p>{% trans "See full category" %} <a href="{% catalogue_url one_tag %}">{{ one_tag }}</a></p>
 {% else %}
     <ul>
-       {% if choices %}
+        {% if choices %}
         {% for tag in tags %}
             <li><a href="{% catalogue_url choices tag %}">{{ tag }}{% if tag.count %}&nbsp;({{ tag.count }}){% endif %}</a></li>
         {% endfor %}
@@ -15,3 +17,5 @@
         {% endif %}
     </ul>
 {% endif %}
+
+{% endspaceless %}
\ No newline at end of file
index f2c4308..9027027 100644 (file)
@@ -1,6 +1,7 @@
 {% extends "base.html" %}
 {% load i18n %}
 {% load catalogue_tags switch_tag social_tags %}
+{% load ssi_include from ssify %}
 
 {% block titleextra %}{% title_from_tags tags %}{% endblock %}
 
         {% if theme_is_set %}
             {% work_list object_list %}
         {% else %}
-        {% cite_promo tags 1 %}
+
+        {% choose_cite tag_ids=tag_ids as cite_promo_pk %}
+        {% choose_fragment tag_ids=tag_ids unless=cite_promo as fragment_promo_pk %}
+        {{ cite_promo_pk.if }}
+            {% ssi_include 'social_cite' pk=cite_promo_pk %}
+        {{ cite_promo_pk.endif }}
+        {{ fragment_promo_pk.if }}
+            {% ssi_include 'catalogue_fragment_promo' pk=fragment_promo_pk %}
+        {{ fragment_promo_pk.endif }}
 
         <div class="see-also">
             {% if last_tag.gazeta_link or last_tag.wiki_link %}
index fa33557..3026525 100755 (executable)
@@ -1,22 +1,19 @@
+{% spaceless %}
+
 {% load pagination_tags %}
-{% load book_short class_name book_short from catalogue_tags %}
-{% load picture_short from picture_tags %}
+{% load class_name from catalogue_tags %}
+{% load ssi_include from ssify %}
 
 {% autopaginate object_list 10 %}
-{% spaceless %}
+
 <ol class='work-list'>
 {% for item in object_list %}
     <li class='{{ item|class_name }}-item'>
-        {% if item.short_html %}
-            {{ item.short_html }}
-{# since we are using shor_html eerywhere, is it needed anymore? #}
-        {% elif item|class_name == "Picture"  %}
-            {% picture_short item %}
-        {% else %}
-            {% book_short item %}
-        {% endif %}
+        {% ssi_include item.short_html_url_name pk=item.pk %}
     </li>
 {% endfor %}
 </ol>
-{% endspaceless %}
+
 {% paginate %}
+
+{% endspaceless %}
index 2edae9b..54fcf5a 100644 (file)
@@ -2,20 +2,18 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
-import datetime
-import feedparser
 from random import randint
 from urlparse import urlparse
 
 from django.conf import settings
 from django import template
 from django.template import Node, Variable, Template, Context
-from django.core.cache import cache
 from django.core.urlresolvers import reverse
 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
+from django.utils.cache import add_never_cache_headers
 from django.utils.translation import ugettext as _
 
-from catalogue.utils import split_tags
+from ssify import ssi_variable
 from catalogue.models import Book, BookMedia, Fragment, Tag, Source
 from catalogue.constants import LICENSES
 
@@ -279,25 +277,6 @@ class CatalogueURLNode(Node):
             return reverse('main_page')
 
 
-@register.inclusion_tag('catalogue/latest_blog_posts.html')
-def latest_blog_posts(feed_url, posts_to_show=5):
-    try:
-        feed = feedparser.parse(str(feed_url))
-        posts = []
-        for i in range(posts_to_show):
-            pub_date = feed['entries'][i].published_parsed
-            published = datetime.date(pub_date[0], pub_date[1], pub_date[2])
-            posts.append({
-                'title': feed['entries'][i].title,
-                'summary': feed['entries'][i].summary,
-                'link': feed['entries'][i].link,
-                'date': published,
-                })
-        return {'posts': posts}
-    except:
-        return {'posts': []}
-
-
 @register.inclusion_tag('catalogue/tag_list.html')
 def tag_list(tags, choices=None):
     if choices is None:
@@ -322,96 +301,24 @@ def book_info(book):
     return locals()
 
 
-@register.inclusion_tag('catalogue/book_wide.html', takes_context=True)
-def book_wide(context, book):
-    ctx = book_short(context, book)
-    ctx['extra_info'] = book.extra_info
-    ctx['hide_about'] = ctx['extra_info'].get('about', '').startswith('http://wiki.wolnepodreczniki.pl')
-    ctx['themes'] = book.related_themes()
-    ctx['main_link'] = reverse('book_text', args=[book.slug]) if book.html_file else None
-    return ctx
-
-
-@register.inclusion_tag('catalogue/book_short.html', takes_context=True)
-def book_short(context, book):
-    stage_note, stage_note_url = book.stage_note()
-
-    return {
-        'book': book,
-        'has_audio': book.has_media('mp3'),
-        'main_link': book.get_absolute_url(),
-        'parents': book.parents(),
-        'tags': split_tags(book.tags.exclude(category__in=('set', 'theme'))),
-        'request': context.get('request'),
-        'show_lang': book.language_code() != settings.LANGUAGE_CODE,
-        'stage_note': stage_note,
-        'stage_note_url': stage_note_url,
-    }
-
-
-@register.inclusion_tag('catalogue/book_mini_box.html')
-def book_mini(book, with_link=True):
-    author_str = ", ".join(tag.name
-        for tag in book.tags.filter(category='author'))
-    return {
-        'book': book,
-        'author_str': author_str,
-        'with_link': with_link,
-        'show_lang': book.language_code() != settings.LANGUAGE_CODE,
-    }
-
-
 @register.inclusion_tag('catalogue/work-list.html', takes_context=True)
 def work_list(context, object_list):
     request = context.get('request')
     return locals()
 
 
-@register.inclusion_tag('catalogue/fragment_promo.html')
-def fragment_promo(arg=None):
-    if isinstance(arg, Book):
-        fragment = arg.choose_fragment()
-    else:
-        if arg is None:
-            fragments = Fragment.objects.all()
-        else:
-            fragments = Fragment.tagged.with_all(arg)
-        fragments = fragments.order_by().only('id')
-        fragments_count = fragments.count()
-        if fragments_count:
-            fragment = fragments.order_by()[randint(0, fragments_count - 1)]
-        else:
-            fragment = None
-
-    return {
-        'fragment': fragment,
-    }
-
-
-@register.inclusion_tag('catalogue/related_books.html')
-def related_books(book, limit=6, random=1, taken=0):
+@register.inclusion_tag('catalogue/related_books.html', takes_context=True)
+def related_books(context, book, limit=6, random=1, taken=0):
     limit = limit - taken
-    cache_key = "catalogue.related_books.%d.%d" % (book.id, limit - random)
-    related = cache.get(cache_key)
-    if related is None:
-        related = Book.tagged.related_to(book,
-                Book.objects.exclude(common_slug=book.common_slug)
-                ).exclude(ancestor=book)[:limit-random]
-        cache.set(cache_key, related, 1800)
-    if 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:
-                random_related = [random_books[randint(0, count - 1)]]
-        else:
-            random_related = list(random_books.order_by('?')[:random])
-    else:
-        random_related = []
+    related = Book.tagged.related_to(book,
+            Book.objects.exclude(common_slug=book.common_slug)
+            ).exclude(ancestor=book)[:limit-random]
+    random_excluded = [b.pk for b in related] + [book.pk]
     return {
+        'request': context['request'],
         'books': related,
-        'random_related': random_related,
+        'random': random,
+        'random_excluded': random_excluded,
     }
 
 
@@ -426,11 +333,6 @@ def catalogue_menu():
         ]}
 
 
-@register.simple_tag
-def tag_url(category, slug):
-    return Tag.create_url(category, slug)
-
-
 @register.simple_tag
 def download_audio(book, daisy=True):
     links = []
@@ -485,3 +387,31 @@ def source_name(url):
         return ''
     source, created = Source.objects.get_or_create(netloc=netloc)
     return source.name or netloc
+
+
+@ssi_variable(register, patch_response=[add_never_cache_headers])
+def catalogue_random_book(request, exclude_ids):
+    queryset = Book.objects.exclude(pk__in=exclude_ids)
+    count = queryset.count()
+    if count:
+        return queryset[randint(0, count - 1)].pk
+    else:
+        return None
+
+
+@ssi_variable(register, patch_response=[add_never_cache_headers])
+def choose_fragment(request, book_id=None, tag_ids=None, unless=False):
+    if unless:
+        return None
+
+    if book_id is not None:
+        fragment = Book.objects.get(pk=book_id).choose_fragment()
+    else:
+        if tag_ids is not None:
+            tags = Tag.objects.filter(pk__in=tag_ids)
+            fragments = Fragment.tagged.with_all(tags).order_by().only('id')
+        else:
+            fragments = Fragment.objects.all().order_by().only('id')
+        fragment_count = fragments.count()
+        fragment = fragments[randint(0, fragment_count - 1)] if fragment_count else None
+    return fragment.pk if fragment is not None else None
index dd11e93..101a508 100644 (file)
@@ -15,14 +15,12 @@ from django.conf import settings
 @override_settings(
     MEDIA_ROOT=tempfile.mkdtemp(prefix='djangotest_'),
     CATALOGUE_DONT_BUILD=set(['pdf', 'mobi', 'epub', 'txt', 'fb2', 'cover']),
-    NO_SEARCH_INDEX = True,
-    CELERY_ALWAYS_EAGER = True,
+    NO_SEARCH_INDEX=True,
+    CELERY_ALWAYS_EAGER=True,
     CACHES={
-            'api': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
             'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
-            'permanent': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
         },
-    SOLR = settings.SOLR_TEST,
+    SOLR=settings.SOLR_TEST,
 )
 class WLTestCase(TestCase):
     """
index 9c7a77c..79aed52 100644 (file)
@@ -8,3 +8,4 @@ from catalogue.tests.cover import *
 from catalogue.tests.search import *
 from catalogue.tests.tags import *
 from catalogue.tests.templatetags import *
+from .test_visit import *
index f10780c..42ea6e2 100644 (file)
@@ -31,8 +31,7 @@ class BooksByTagTests(WLTestCase):
 
     def test_nonexistent_tag(self):
         """ Looking for a non-existent tag should yield 404 """
-        # NOTE: this yields a false positive, 'cause of URL change
-        self.assertEqual(404, self.client.get('/katalog/autor/czeslaw_milosz/').status_code)
+        self.assertEqual(404, self.client.get('/katalog/autor/czeslaw-milosz/').status_code)
 
     def test_book_tag(self):
         """ Looking for a book tag isn't permitted """
@@ -105,7 +104,8 @@ class TagRelatedTagsTests(WLTestCase):
         """ empty tag should have no related tags """
 
         cats = self.client.get('/katalog/autor/empty/').context['categories']
-        self.assertEqual(cats, {}, 'tags related to empty tag')
+        self.assertEqual({k: v for (k, v) in cats.items() if v}, {},
+            'tags related to empty tag')
 
     def test_has_related(self):
         """ related own and descendants' tags should be generated """
@@ -115,7 +115,7 @@ class TagRelatedTagsTests(WLTestCase):
                         'missing `author` related tag')
         self.assertTrue('Epoch' in [tag.name for tag in cats['epoch']],
                         'missing `epoch` related tag')
-        self.assertFalse("kind" in cats,
+        self.assertFalse(cats.get("kind", False),
                         "There should be no child-only related `kind` tags")
         self.assertTrue("Genre" in [tag.name for tag in cats['genre']],
                         'missing `genre` related tag')
@@ -135,7 +135,7 @@ class TagRelatedTagsTests(WLTestCase):
 
         response = self.client.get('/katalog/rodzaj/kind/')
         cats = response.context['categories']
-        self.assertFalse('kind' in cats,
+        self.assertFalse(cats.get('kind', False),
                          'filtering tag wrongly included in related')
         cats = self.client.get('/katalog/motyw/theme/').context['categories']
         self.assertFalse('Theme' in [tag.name for tag in cats['theme']],
@@ -155,12 +155,23 @@ class TagRelatedTagsTests(WLTestCase):
 
         cats = self.client.get('/katalog/epoka/epoch/').context['categories']
         self.assertTrue(('ChildKind', 2) in [(tag.name, tag.count) for tag in cats['kind']],
-                    'wrong related kind tags on tag page')
+                    'wrong related kind tags on tag page, got: ' +
+                    unicode([(tag.name, tag.count) for tag in cats['kind']]))
 
         # all occurencies of theme should be counted
         self.assertTrue(('Theme', 4) in [(tag.name, tag.count) for tag in cats['theme']],
                     'wrong related theme count')
 
+    def test_query_child_tag(self):
+        """
+        If child and parent have a common tag, but parent isn't included
+        in the result, child should still count.
+        """
+        cats = self.client.get('/katalog/gatunek/childgenre/').context['categories']
+        self.assertTrue(('Epoch', 2) in [(tag.name, tag.count) for tag in cats['epoch']],
+                    'wrong related kind tags on tag page, got: ' +
+                    unicode([(tag.name, tag.count) for tag in cats['epoch']]))
+
 
 class CleanTagRelationTests(WLTestCase):
     """ tests for tag relations cleaning after deleting things """
@@ -183,7 +194,7 @@ class CleanTagRelationTests(WLTestCase):
 
         models.Book.objects.all().delete()
         cats = self.client.get('/katalog/rodzaj/k/').context['categories']
-        self.assertEqual(cats, {})
+        self.assertEqual({k: v for (k, v) in cats.items() if v}, {})
         self.assertEqual(models.Fragment.objects.all().count(), 0,
                          "orphaned fragments left")
         self.assertEqual(models.Tag.intermediary_table_model.objects.all().count(), 0,
@@ -236,7 +247,7 @@ class TestIdenticalTag(WLTestCase):
             context = self.client.get('/katalog/%s/tag/' % localcat).context
             self.assertEqual(1, len(context['object_list']))
             self.assertNotEqual({}, context['categories'])
-            self.assertFalse(cat in context['categories'])
+            self.assertFalse(context['categories'].get(cat, False))
 
 
 class BookTagsTests(WLTestCase):
@@ -281,6 +292,6 @@ class BookTagsTests(WLTestCase):
         context = self.client.get('/katalog/').context
         self.assertEqual([(tag.name, tag.count) for tag in context['categories']['author']],
                          [('Jim Lazy', 1), ('Common Man', 1)])
-        self.assertEqual([(tag.name, tag.count) for tag in context['fragment_tags']],
+        self.assertEqual([(tag.name, tag.count) for tag in context['categories']['theme']],
                          [('ChildTheme', 1), ('ParentTheme', 1), ('Theme', 2)])
 
diff --git a/apps/catalogue/tests/test_visit.py b/apps/catalogue/tests/test_visit.py
new file mode 100644 (file)
index 0000000..1bdbdbd
--- /dev/null
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from catalogue import models
+from catalogue.test_utils import BookInfoStub, PersonStub, WLTestCase, info_args
+from django.core.files.base import ContentFile
+
+
+class VisitTest(WLTestCase):
+    """Simply create some objects and visit some views."""
+
+    def setUp(self):
+        WLTestCase.setUp(self)
+        author = PersonStub(("Jane",), "Doe")
+        book_info = BookInfoStub(author=author, genre="Genre",
+            epoch='Epoch', kind="Kind", **info_args(u"A book"))
+        self.book = models.Book.from_text_and_meta(ContentFile('''
+            <utwor>
+            <opowiadanie>
+                <akap>
+                    <begin id="b1" />
+                    <motyw id="m1">Theme</motyw>
+                    Test
+                    <end id="e1" />
+                </akap>
+            </opowiadanie>
+            </utwor>
+            '''), book_info)
+        self.collection = models.Collection.objects.create(
+            title='Biblioteczka Boya', slug='boy', book_slugs='a-book')
+
+    def test_visit_urls(self):
+        """ book description should return authors, ancestors, book """
+        url_map = {
+            200: [
+                '',
+                'lektury/',
+                'lektury/boy/',
+                'nowe/',
+                'lektura/a-book/',
+                'lektura/a-book.html',
+                'lektura/a-book/motyw/theme/',
+                'motyw/theme/',
+                'autor/jane-doe/',
+                'autor/jane-doe/gatunek/genre/',
+                'autor/jane-doe/gatunek/genre/motyw/theme/',
+                'pl.json',
+                'b/%d/mini.pl.html' % self.book.pk,
+                'b/%d/mini_nolink.pl.html' % self.book.pk,
+                'b/%d/short.pl.html' % self.book.pk,
+                'b/%d/wide.pl.html' % self.book.pk,
+                'f/%d/promo.pl.html' % self.book.fragments.all()[0].pk,
+                'f/%d/short.pl.html' % self.book.fragments.all()[0].pk,
+                ],
+            404: [
+                'lektury/nonexistent/',  # Nonexistent Collection.
+                'lektura/nonexistent/',  # Nonexistent Book.
+                'lektura/nonexistent.html',  # Nonexistent Book's HTML.
+                'lektura/nonexistent/motyw/theme/',  # Nonexistent Book's theme.
+                'lektura/a-book/motyw/nonexistent/',  # Nonexistent theme in a Book.
+                'autor/nonexistent/',  # Nonexistent author.
+                'motyw/nonexistent/',  # Nonexistent theme.
+                'zh.json',  # Nonexistent language.
+                'b/%d/mini.pl.html' % (self.book.pk + 100),  # Nonexistent book.
+                'b/%d/mini_nolink.pl.html' % (self.book.pk + 100),  # Nonexistent book.
+                'b/%d/short.pl.html' % (self.book.pk + 100),  # Nonexistent book.
+                'b/%d/wide.pl.html' % (self.book.pk + 100),  # Nonexistent book.
+                'f/%d/promo.pl.html' % (self.book.fragments.all()[0].pk + 100),  # Nonexistent fragment.
+                'f/%d/short.pl.html' % (self.book.fragments.all()[0].pk + 100),  # Nonexistent fragment.
+                ]
+            }
+        prefix = '/katalog/'
+        for expected_status, urls in url_map.items():
+            for url in urls:
+                status = self.client.get(prefix + url).status_code
+                self.assertEqual(status, expected_status,
+                    "Wrong status code for '%s'. Expected %d, got %d." % (
+                        prefix + url, expected_status, status))
index 7b1fb3d..05f8766 100644 (file)
@@ -18,6 +18,8 @@ urlpatterns = patterns('picture.views',
     url(r'^obraz/(?P<slug>%s).html$' % SLUG, 'picture_viewer', name='picture_viewer'),
     url(r'^obraz/(?P<slug>%s)/$' % SLUG, 'picture_detail'),
 
+    url(r'^p/(?P<pk>\d+)/short\.(?P<lang>.+)\.html', 'picture_short', name='picture_short'),
+    url(r'^pa/(?P<pk>\d+)/short\.(?P<lang>.+)\.html', 'picturearea_short', name='picture_area_short'),
 )
 
 urlpatterns += patterns('',
@@ -64,6 +66,15 @@ urlpatterns += patterns('catalogue.views',
     url(r'^lektura/(?P<slug>%s)/motyw/(?P<theme_slug>[a-zA-Z0-9-]+)/$' % SLUG,
         'book_fragments', name='book_fragments'),
 
+    # Includes.
+    url(r'^(?P<lang>[^/]+)\.json$', 'catalogue_json'),
+    url(r'^b/(?P<pk>\d+)/mini\.(?P<lang>.+)\.html', 'book_mini', name='catalogue_book_mini'),
+    url(r'^b/(?P<pk>\d+)/mini_nolink\.(?P<lang>.+)\.html', 'book_mini', {'with_link': False}, name='catalogue_book_mini_nolink'),
+    url(r'^b/(?P<pk>\d+)/short\.(?P<lang>.+)\.html', 'book_short', name='catalogue_book_short'),
+    url(r'^b/(?P<pk>\d+)/wide\.(?P<lang>.+)\.html', 'book_wide', name='catalogue_book_wide'),
+    url(r'^f/(?P<pk>\d+)/promo\.(?P<lang>.+)\.html', 'fragment_promo', name='catalogue_fragment_promo'),
+    url(r'^f/(?P<pk>\d+)/short\.(?P<lang>.+)\.html', 'fragment_short', name='catalogue_fragment_short'),
+
     # This should be the last pattern.
     url(r'^(?P<tags>[a-zA-Z0-9-/]*)/$', 'tagged_object_list', name='tagged_object_list'),
 )
index 91e782e..bcc5a0b 100644 (file)
@@ -2,8 +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 __future__ import with_statement
-
+from collections import defaultdict
 import hashlib
 import random
 import re
@@ -37,14 +36,21 @@ def get_random_hash(seed):
     return urlsafe_b64encode(sha_digest).replace('=', '').replace('_', '-').lower()
 
 
-def split_tags(tags, initial=None):
-    if initial is None:
-        result = {}
+def split_tags(*tag_lists):
+    if len(tag_lists) == 1:
+        result = defaultdict(list)
+        for tag in tag_lists[0]:
+            result[tag.category].append(tag)
     else:
-        result = initial
-
-    for tag in tags:
-        result.setdefault(tag.category, []).append(tag)
+        result = defaultdict(dict)
+        for tag_list in tag_lists:
+            for tag in tag_list:
+                try:
+                    result[tag.category][tag.pk].count += tag.count
+                except KeyError:
+                    result[tag.category][tag.pk] = tag
+        for k, v in result.items():
+            result[k] = sorted(v.values(), key=lambda tag: tag.sort_key)
     return result
 
 
index f47329a..e5514f0 100644 (file)
@@ -6,107 +6,108 @@ from collections import OrderedDict
 import re
 
 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
+from django.shortcuts import render_to_response, get_object_or_404, render
 from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect, JsonResponse
 from django.core.urlresolvers import reverse
 from django.db.models import Q
 from django.contrib.auth.decorators import login_required, user_passes_test
 from django.utils.http import urlquote_plus
 from django.utils import translation
-from django.utils.translation import get_language, ugettext as _, ugettext_lazy
-from django.views.decorators.vary import vary_on_headers
+from django.utils.translation import ugettext as _, ugettext_lazy
 
 from ajaxable.utils import AjaxableFormView
-from catalogue import models
-from catalogue import forms
-from .helpers import get_related_tags, get_fragment_related_tags, tags_usage_for_books, tags_usage_for_works, tags_usage_for_fragments
-from catalogue.utils import split_tags, MultiQuerySet, SortedMultiQuerySet
-from catalogue.templatetags.catalogue_tags import tag_list, collection_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, PictureArea
 from picture.views import picture_list_thumb
+from ssify import ssi_included, ssi_expect, SsiVariable as V
+from suggest.forms import PublishingSuggestForm
+from catalogue import forms
+from catalogue.helpers import get_top_level_related_tags
+from catalogue import models
+from catalogue.utils import split_tags, MultiQuerySet, SortedMultiQuerySet
+from catalogue.templatetags.catalogue_tags import tag_list, collection_list
 
 staff_required = user_passes_test(lambda user: user.is_staff)
-permanent_cache = get_cache('permanent')
-
-
-@vary_on_headers('X-Requested-With')
-def catalogue(request):
-    #cache_key = 'catalogue.catalogue/' + get_language()
-    #output = permanent_cache.get(cache_key)
-    output = None
-
-    if output is None:
-        common_categories = ('author',)
-        split_categories = ('epoch', 'genre', 'kind')
-
-        categories = split_tags(tags_usage_for_works(common_categories))
-        book_categories = split_tags(tags_usage_for_books(split_categories))
-        picture_categories = split_tags(
-            models.Tag.objects.usage_for_model(Picture, counts=True).filter(
-                category__in=split_categories))
-        # we want global usage for themes
-        fragment_tags = list(tags_usage_for_fragments(('theme',)))
-        collections = models.Collection.objects.all()
-
-        render_tag_list = lambda x: render_to_string(
-            'catalogue/tag_list.html', tag_list(x))
-
-        def render_split(with_books, with_pictures):
-            ctx = {}
-            if with_books:
-                ctx['books'] = render_tag_list(with_books)
-            if with_pictures:
-                ctx['pictures'] = render_tag_list(with_pictures)
-            return render_to_string('catalogue/tag_list_split.html', ctx)
-
-        output = {}
-        output['theme'] = render_tag_list(fragment_tags)
-        for category in common_categories:
-            output[category] = render_tag_list(categories.get(category, []))
-        for category in split_categories:
-            output[category] = render_split(
-                book_categories.get(category, []),
-                picture_categories.get(category, []))
-
-        output['collections'] = render_to_string(
-            'catalogue/collection_list.html', collection_list(collections))
-        #permanent_cache.set(cache_key, output)
-    if request.is_ajax():
+
+
+def catalogue(request, as_json=False):
+    common_categories = ('author',)
+    split_categories = ('epoch', 'genre', 'kind')
+
+    categories = split_tags(
+        get_top_level_related_tags(categories=common_categories),
+        models.Tag.objects.usage_for_model(
+            models.Fragment, counts=True).filter(category='theme'),
+        models.Tag.objects.usage_for_model(
+            Picture, counts=True).filter(category__in=common_categories),
+        models.Tag.objects.usage_for_model(
+            PictureArea, counts=True).filter(
+            category='theme')
+    )
+    book_categories = split_tags(
+        get_top_level_related_tags(categories=split_categories)
+        )
+    picture_categories = split_tags(
+        models.Tag.objects.usage_for_model(
+            Picture, counts=True).filter(
+            category__in=split_categories),
+        )
+
+    collections = models.Collection.objects.all()
+
+    def render_tag_list(tags):
+        render_to_string('catalogue/tag_list.html', tag_list(tags))
+
+    def render_split(with_books, with_pictures):
+        ctx = {}
+        if with_books:
+            ctx['books'] = render_tag_list(with_books)
+        if with_pictures:
+            ctx['pictures'] = render_tag_list(with_pictures)
+        return render_to_string('catalogue/tag_list_split.html', ctx)
+
+    output = {}
+    output['theme'] = render_tag_list(categories.get('theme', []))
+    for category in common_categories:
+        output[category] = render_tag_list(categories.get(category, []))
+    for category in split_categories:
+        output[category] = render_split(
+            book_categories.get(category, []),
+            picture_categories.get(category, []))
+
+    output['collections'] = render_to_string(
+        'catalogue/collection_list.html', collection_list(collections))
+    if as_json:
         return JsonResponse(output)
     else:
         return render_to_response('catalogue/catalogue.html', locals(),
             context_instance=RequestContext(request))
 
 
+@ssi_included
+def catalogue_json(request):
+    return catalogue(request, True)
+
+
 def book_list(request, filter=None, get_filter=None,
         template_name='catalogue/book_list.html',
         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 """
-    cache_key = "%s/%s" % (cache_key, get_language())
-    cached = permanent_cache.get(cache_key)
-    if cached is not None:
-        rendered_nav, rendered_book_list = cached
-    else:
-        if get_filter:
-            filter = get_filter()
-        books_by_author, orphans, books_by_parent = models.Book.book_list(filter)
-        books_nav = OrderedDict()
-        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))
+    if get_filter:
+        filter = get_filter()
+    books_by_author, orphans, books_by_parent = models.Book.book_list(filter)
+    books_nav = OrderedDict()
+    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())
     return render_to_response(template_name, locals(),
         context_instance=RequestContext(request))
 
@@ -115,13 +116,13 @@ def audiobook_list(request):
     return book_list(request, Q(media__type='mp3') | Q(media__type='ogg'),
                      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',
-                     cache_key='catalogue.daisy_list')
+                     )
 
 
 def collection(request, slug):
@@ -136,7 +137,6 @@ def collection(request, slug):
         raise ValueError('How do I show this kind of collection? %s' % coll.kind)
     return view(request, get_filter=coll.get_query,
                      template_name=tmpl,
-                     cache_key='catalogue.collection:%s' % coll.slug,
                      context={'collection': coll})
 
 
@@ -179,19 +179,14 @@ def tagged_object_list(request, tags=''):
     except AttributeError:
         pass
 
-    if len([tag for tag in tags if tag.category == 'book']):
-        raise Http404
-
     # beginning of digestion
     theme_is_set = [tag for tag in tags if tag.category == 'theme']
     shelf_is_set = [tag for tag in tags if tag.category == 'set']
     only_shelf = shelf_is_set and len(tags) == 1
     only_my_shelf = only_shelf and request.user.is_authenticated() and request.user == tags[0].user
+    tags_pks = [tag.pk for tag in tags]
 
-
-    objects =  None
-    categories = {}
-    object_queries = []
+    objects = None
 
     if theme_is_set:
         shelf_tags = [tag for tag in tags if tag.category == 'set']
@@ -200,47 +195,53 @@ def tagged_object_list(request, tags=''):
         areas = PictureArea.tagged.with_all(fragment_tags)
 
         if shelf_tags:
-            # FIXME: book tags here
             books = models.Book.tagged.with_all(shelf_tags).order_by()
-            l_tags = models.Tag.objects.filter(category='book',
-                slug__in=[book.book_tag_slug() for book in books.iterator()])
-            fragments = models.Fragment.tagged.with_any(l_tags, fragments)
+            fragments = fragments.filter(Q(book__in=books) | Q(book__ancestor__in=books))
+            areas = PictureArea.objects.none()
 
-        related_tags = get_fragment_related_tags(tags)
-        categories = split_tags(related_tags, categories)
-        object_queries.insert(0, fragments)
-
-        area_keys = [area.pk for area in areas.iterator()]
-        if area_keys:
-            related_tags = PictureArea.tags.usage(counts=True,
-                                                         filters={'pk__in': area_keys})
-            related_tags = (tag for tag in related_tags if tag not in fragment_tags)
-
-            categories = split_tags(related_tags, categories)
+        categories = split_tags(
+            models.Tag.objects.usage_for_queryset(fragments, counts=True
+                ).exclude(pk__in=tags_pks),
+            models.Tag.objects.usage_for_queryset(areas, counts=True
+                ).exclude(pk__in=tags_pks)
+            )
 
         # we want the Pictures to go first
-        object_queries.insert(0, areas)
-        objects = MultiQuerySet(*object_queries)
+        objects = MultiQuerySet(areas, fragments)
     else:
+        all_books = models.Book.tagged.with_all(tags)
         if shelf_is_set:
-            books = models.Book.tagged.with_all(tags).order_by(
-                'sort_key_author', 'title')
+            books = all_books.order_by('sort_key_author', 'title')
+            pictures = Pictures.objects.none()
+            related_book_tags = models.Tag.objects.usage_for_queryset(
+                books, counts=True).exclude(
+                category='set').exclude(pk__in=tags_pks)
         else:
             books = models.Book.tagged_top_level(tags).order_by(
                 'sort_key_author', 'title')
-
-        pictures = Picture.tagged.with_all(tags).order_by(
-            'sort_key_author', 'title')
-
-        categories = split_tags(get_related_tags(tags))
+            pictures = Picture.tagged.with_all(tags).order_by(
+                'sort_key_author', 'title')
+            related_book_tags = get_top_level_related_tags(tags)
+
+        fragments = models.Fragment.objects.filter(book__in=all_books)
+        areas = PictureArea.objects.filter(picture__in=pictures)
+
+        categories = split_tags(
+            related_book_tags,
+            models.Tag.objects.usage_for_queryset(
+                pictures, counts=True).exclude(pk__in=tags_pks),
+            models.Tag.objects.usage_for_queryset(
+                fragments, counts=True).filter(
+                category='theme').exclude(pk__in=tags_pks),
+            models.Tag.objects.usage_for_queryset(
+                areas, counts=True).filter(
+                category__in=('theme', 'thing')).exclude(
+                pk__in=tags_pks),
+        )
 
         objects = SortedMultiQuerySet(pictures, books,
             order_by=('sort_key_author', 'title'))
 
-
-    if not objects:
-        objects = models.Book.objects.none()
-
     return render_to_response('catalogue/tagged_object_list.html',
         {
             'object_list': objects,
@@ -249,6 +250,7 @@ def tagged_object_list(request, tags=''):
             'only_my_shelf': only_my_shelf,
             'formats_form': forms.DownloadFormatsForm(),
             'tags': tags,
+            'tags_ids': tags_pks,
             'theme_is_set': theme_is_set,
         },
         context_instance=RequestContext(request))
@@ -342,7 +344,7 @@ def _no_diacritics_regexp(query):
 
 def unicode_re_escape(query):
     """ Unicode-friendly version of re.escape """
-    return re.sub('(?u)(\W)', r'\\\1', query)
+    return re.sub(r'(?u)(\W)', r'\\\1', query)
 
 def _word_starts_with(name, prefix):
     """returns a Q object getting models having `name` contain a word
@@ -429,10 +431,10 @@ def _get_result_link(match, tag_list):
 
 def _get_result_type(match):
     if isinstance(match, models.Book) or isinstance(match, pdcounter_models.BookStub):
-        type = 'book'
+        match_type = 'book'
     else:
-        type = match.category
-    return type
+        match_type = match.category
+    return match_type
 
 
 def books_starting_with(prefix):
@@ -603,3 +605,84 @@ class CustomPDFFormView(AjaxableFormView):
 
     def context_description(self, request, obj):
         return obj.pretty_title()
+
+
+####
+# Includes
+####
+
+
+@ssi_included
+def book_mini(request, pk, with_link=True):
+    book = get_object_or_404(models.Book, pk=pk)
+    author_str = ", ".join(tag.name
+        for tag in book.tags.filter(category='author'))
+    return render(request, 'catalogue/book_mini_box.html', {
+        'book': book,
+        'author_str': author_str,
+        'with_link': with_link,
+        'show_lang': book.language_code() != settings.LANGUAGE_CODE,
+    })
+
+
+@ssi_included(get_ssi_vars=lambda pk: (lambda ipk: (
+        ('ssify.get_csrf_token',),
+        ('social_tags.likes_book', (ipk,)),
+        ('social_tags.book_shelf_tags', (ipk,)),
+    ))(ssi_expect(pk, int)))
+def book_short(request, pk):
+    book = get_object_or_404(models.Book, pk=pk)
+    stage_note, stage_note_url = book.stage_note()
+
+    return render(request, 'catalogue/book_short.html', {
+        'book': book,
+        'has_audio': book.has_media('mp3'),
+        'main_link': book.get_absolute_url(),
+        'parents': book.parents(),
+        'tags': split_tags(book.tags.exclude(category__in=('set', 'theme'))),
+        'show_lang': book.language_code() != settings.LANGUAGE_CODE,
+        'stage_note': stage_note,
+        'stage_note_url': stage_note_url,
+    })
+
+
+@ssi_included(get_ssi_vars=lambda pk: book_short.get_ssi_vars(pk) +
+    (lambda ipk: (
+        ('social_tags.choose_cite', [ipk]),
+        ('catalogue_tags.choose_fragment', [ipk], {
+            'unless': V('social_tags.choose_cite', [ipk])}),
+    ))(ssi_expect(pk, int)))
+def book_wide(request, pk):
+    book = get_object_or_404(models.Book, pk=pk)
+    stage_note, stage_note_url = book.stage_note()
+    extra_info = book.extra_info
+
+    return render(request, 'catalogue/book_wide.html', {
+        'book': book,
+        'has_audio': book.has_media('mp3'),
+        'parents': book.parents(),
+        'tags': split_tags(book.tags.exclude(category__in=('set', 'theme'))),
+        'show_lang': book.language_code() != settings.LANGUAGE_CODE,
+        'stage_note': stage_note,
+        'stage_note_url': stage_note_url,
+
+        'main_link': reverse('book_text', args=[book.slug]) if book.html_file else None,
+        'extra_info': extra_info,
+        'hide_about': extra_info.get('about', '').startswith('http://wiki.wolnepodreczniki.pl'),
+        'themes': book.related_themes(),
+    })
+
+
+@ssi_included
+def fragment_short(request, pk):
+    fragment = get_object_or_404(models.Fragment, pk=pk)
+    return render(request, 'catalogue/fragment_short.html',
+        {'fragment': fragment})
+
+
+@ssi_included
+def fragment_promo(request, pk):
+    fragment = get_object_or_404(models.Fragment, pk=pk)
+    return render(request, 'catalogue/fragment_promo.html', {
+        'fragment': fragment
+    })
diff --git a/apps/chunks/migrations/0002_auto_20140911_1253.py b/apps/chunks/migrations/0002_auto_20140911_1253.py
new file mode 100644 (file)
index 0000000..09d541c
--- /dev/null
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+def null_to_blank(apps, schema_editor):
+    Chunk = apps.get_model("chunks", "Chunk")
+    Chunk.objects.filter(content=None).update(content='')
+    Chunk.objects.filter(description=None).update(description='')
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chunks', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RunPython(null_to_blank),
+        migrations.AlterField(
+            model_name='chunk',
+            name='content',
+            field=models.TextField(verbose_name='content', blank=True),
+        ),
+        migrations.AlterField(
+            model_name='chunk',
+            name='description',
+            field=models.CharField(max_length=255, verbose_name='Description', blank=True),
+        ),
+    ]
index 5cdf8fe..7f5410b 100644 (file)
@@ -1,6 +1,7 @@
-from django.core.cache import cache
+from django.conf import settings
 from django.db import models
-from django.utils.translation import ugettext_lazy as _, get_language
+from django.utils.translation import ugettext_lazy as _
+from ssify import flush_ssi_includes
 
 
 class Chunk(models.Model):
@@ -9,8 +10,8 @@ class Chunk(models.Model):
     any template with the use of a special template tag.
     """
     key = models.CharField(_('key'), help_text=_('A unique name for this chunk of content'), primary_key=True, max_length=255)
-    description = models.CharField(_('description'), blank=True, null=True, max_length=255)
-    content = models.TextField(_('content'), blank=True, null=True)
+    description = models.CharField(_('description'), blank=True, max_length=255)
+    content = models.TextField(_('content'), blank=True)
 
     class Meta:
         ordering = ('key',)
@@ -20,15 +21,17 @@ class Chunk(models.Model):
     def __unicode__(self):
         return self.key
 
-    @staticmethod
-    def cache_key(key):
-        return 'chunk/%s/%s' % (key, get_language())
-
     def save(self, *args, **kwargs):
         ret = super(Chunk, self).save(*args, **kwargs)
-        cache.delete(self.cache_key(self.key))
+        self.flush_includes()
         return ret
 
+    def flush_includes(self):
+        flush_ssi_includes([
+            '/chunks/chunk/%s.%s.html' % (self.key, lang)
+            for lang in [lc for (lc, _ln) in settings.LANGUAGES]])
+
+
 
 class Attachment(models.Model):
     key = models.CharField(_('key'), help_text=_('A unique name for this attachment'), primary_key=True, max_length=255)
@@ -40,4 +43,3 @@ class Attachment(models.Model):
 
     def __unicode__(self):
         return self.key
-
diff --git a/apps/chunks/templatetags/__init__.py b/apps/chunks/templatetags/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/apps/chunks/templatetags/chunks.py b/apps/chunks/templatetags/chunks.py
deleted file mode 100644 (file)
index cc25df7..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-from django import template
-from django.db import models
-from django.core.cache import cache
-
-
-register = template.Library()
-
-Chunk = models.get_model('chunks', 'chunk')
-Attachment = models.get_model('chunks', 'attachment')
-
-
-@register.simple_tag
-def chunk(key, cache_time=0):
-    try:
-        cache_key = Chunk.cache_key(key)
-        c = cache.get(cache_key)
-        if c is None:
-            c = Chunk.objects.get(key=key)
-            cache.set(cache_key, c, int(cache_time))
-        content = c.content
-    except Chunk.DoesNotExist:
-        n = Chunk(key=key)
-        n.save()
-        return ''
-    return content
-
-
-@register.simple_tag
-def attachment(key, cache_time=0):
-    try:
-        cache_key = 'attachment_' + key
-        c = cache.get(cache_key)
-        if c is None:
-            c = Attachment.objects.get(key=key)
-            cache.set(cache_key, c, int(cache_time))
-        return c.attachment.url
-    except Attachment.DoesNotExist:
-        return ''
diff --git a/apps/chunks/urls.py b/apps/chunks/urls.py
new file mode 100644 (file)
index 0000000..005cadd
--- /dev/null
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns('chunks.views',
+    url(r'^chunk/(?P<key>.+)\.(?P<lang>.+)\.html$', 'chunk', name='chunk'),
+)
diff --git a/apps/chunks/views.py b/apps/chunks/views.py
new file mode 100644 (file)
index 0000000..cbcf5bf
--- /dev/null
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.http import HttpResponse
+from ssify import ssi_included
+from .models import Chunk
+
+@ssi_included
+def chunk(request, key):
+    chunk, created = Chunk.objects.get_or_create(key=key)
+    return HttpResponse(chunk.content)
index 7df3d75..2256acf 100644 (file)
@@ -30,6 +30,6 @@ def build_notes(book):
                                html=html_str,
                                sort_key=sortify(text_str).strip()[:128])
 
-def notes_from_book(sender, **kwargs):
-    build_notes.delay(sender)
+def notes_from_book(sender, instance, **kwargs):
+    build_notes.delay(instance)
 Book.html_built.connect(notes_from_book)
index 3051276..1a30ffc 100755 (executable)
@@ -18,12 +18,17 @@ class Command(BaseCommand):
         from datetime import date, timedelta
         from funding.models import Offer
         from funding import app_settings
+        from django.core.cache import caches
+        from django.conf import settings
 
         verbose = options['verbose']
 
         for offer in Offer.past().filter(notified_end=None):
             if verbose:
                 print 'Notify end:', offer
+            # The 'WL fund' list needs to be updated.
+            caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
+            offer.flush_includes()
             offer.notify_end()
 
         current = Offer.current()
index a9c3d87..e36b732 100644 (file)
@@ -4,18 +4,19 @@
 #
 from datetime import date, datetime
 from urllib import urlencode
+from django.conf import settings
+from django.contrib.sites.models import Site
 from django.core.urlresolvers import reverse
 from django.core.mail import send_mail
-from django.conf import settings
-from django.template.loader import render_to_string
 from django.db import models
+from django.template.loader import render_to_string
 from django.utils.timezone import utc
 from django.utils.translation import ugettext_lazy as _, override
 import getpaid
+from ssify import flush_ssi_includes
 from catalogue.models import Book
 from catalogue.utils import get_random_hash
 from polls.models import Poll
-from django.contrib.sites.models import Site
 from . import app_settings
 
 
@@ -58,10 +59,27 @@ class Offer(models.Model):
             self.pk is not None and
             type(self).objects.values('book').get(pk=self.pk)['book'] != self.book_id)
         retval = super(Offer, self).save(*args, **kw)
+        self.flush_includes()
         if published_now:
             self.notify_published()
         return retval
 
+    def flush_includes(self):
+        flush_ssi_includes([
+            template % (self.pk, lang)
+            for template in [
+                '/wesprzyj/o/%d/top-bar.%s.html',
+                '/wesprzyj/o/%d/detail-bar.%s.html',
+                '/wesprzyj/o/%d/list-bar.%s.html',
+                '/wesprzyj/o/%d/status.%s.html',
+                '/wesprzyj/o/%d/status-more.%s.html',
+                ] + [
+                    '/wesprzyj/o/%%d/fundings/%d.%%s.html' % page
+                    for page in range(1, len(self.funding_payed()) // 10 + 2)
+                ]
+            for lang in [lc for (lc, _ln) in settings.LANGUAGES]
+            ])
+
     def is_current(self):
         return self.start <= date.today() <= self.end and self == self.current()
 
@@ -247,7 +265,9 @@ class Funding(models.Model):
     def save(self, *args, **kwargs):
         if self.email and not self.notify_key:
             self.notify_key = get_random_hash(self.email)
-        return super(Funding, self).save(*args, **kwargs)
+        ret = super(Funding, self).save(*args, **kwargs)
+        self.offer.flush_includes()
+        return ret
 
     @classmethod
     def notify_funders(cls, subject, template_name, extra_context=None,
diff --git a/apps/funding/templates/funding/includes/funding.html b/apps/funding/templates/funding/includes/funding.html
new file mode 100644 (file)
index 0000000..66804a4
--- /dev/null
@@ -0,0 +1,53 @@
+{% spaceless %}
+
+{% load i18n %}
+{% load time_tags %}
+
+{% if offer %}
+
+<div class="funding {{ add_class }}" data-offer-id="{{offer.id}}" style="">
+    {% if closeable %}<a href="#" class="close">X</a>{% endif %}
+    {% if link and is_current %}
+        <div class="call-area">
+            <a class="call honking" href="{% url 'funding_current' offer.slug %}">
+                {% trans "Support!" %}</a>
+            <div class="learn-more">
+                <a href="{% url 'infopage' 'wesprzyj' %}">{% trans "Learn more" %}</a>
+            </div>
+        </div>
+    {% endif %}
+    <div class="description {% if link and is_current %}with-button{% endif %}">
+    {% if link %}<a href="{% if is_current %}{% url 'funding_current' offer.slug %}{% else %}{{ offer.get_absolute_url }}{% endif %}">{% endif %}
+    {% if show_title %}
+        {% if is_current and show_title_calling %}<strong style="margin-right: .6em;">{% trans "Help free the book!" %}</strong>{% endif %}
+        <span class="funding-title{% if not is_current %}-strong{% endif %}">{{ offer }}</span>
+    {% endif %}
+
+    <div class="progress"
+        style="text-align: center; background-size: {{ percentage|stringformat:'.2f' }}% 1px;"
+    >
+        {% if sum %}
+            <span class="piece progress-collected">{% trans "collected" %}: {{ sum }} zł</span>
+        {% endif %}
+        {% if not is_win %}
+            <span class="piece progress-target"><span class="{% if sum %}progress-extralabel{% endif %}">{% trans "needed" %}: </span>{{ offer.target }} zł</span>
+        {% endif %}
+        {% if is_current %}
+            <span class="piece progress-until"><span class="progress-extralabel">{% trans "until fundraiser end" %}:</span>
+                <span class="countdown inline" data-until='{{ offer.end|date_to_utc:True|utc_for_js }}'></span>
+            </span>
+        {% else %}
+            <div style="clear: both"></div>
+        {% endif %}
+    </div>
+    {% if link %}</a>{% endif %}
+    </div>
+    <div style="clear: both"></div>
+</div>
+{% if closeable %}
+    <div class="funding-handle">{% trans "Help free the book!" %}</div>
+{% endif %}
+
+{% endif %}
+
+{% endspaceless %}
diff --git a/apps/funding/templates/funding/includes/fundings.html b/apps/funding/templates/funding/includes/fundings.html
new file mode 100644 (file)
index 0000000..e13e1ec
--- /dev/null
@@ -0,0 +1,28 @@
+{% spaceless %}
+
+{% load i18n %}
+{% load pagination_tags %}
+
+<table class="wlfund">
+
+{% for funding in fundings %}
+    <tr class="funding-plus">
+        <td class="oneline">{{ funding.payed_at.date }}</td>
+        <td>
+            {% if funding.name %}
+                {{ funding.name }}
+            {% else %}
+                <em>{% trans "Anonymous" %}</em>
+            {% endif %}
+        </td>
+        <td>{{ funding.amount }}&nbsp;zł</td>
+        <td>&nbsp;
+            {% for perk in funding.perks.all %}
+                {{ perk.name }}{% if not forloop.last %},{% endif %}
+            {% endfor %}
+        </td>
+{% endfor %}
+</table>
+
+{% endspaceless %}{% paginate %}
+
diff --git a/apps/funding/templates/funding/includes/offer_status.html b/apps/funding/templates/funding/includes/offer_status.html
new file mode 100644 (file)
index 0000000..5828ce4
--- /dev/null
@@ -0,0 +1,31 @@
+{% load i18n %}
+
+{% if offer.is_current %}
+    {% if offer.is_win %}
+        <p>
+            {% blocktrans with end=offer.end %}The fundraiser
+            ends on {{ end }}. The full amount has been successfully
+            raised, but you can still contribute and help liberate
+            more books.{% endblocktrans %}
+        </p>
+    {% else %}
+    <p>
+        <strong>{% blocktrans with target=offer.target|floatformat %}W need {{target}} zł to digitize it,
+        compile it and publish for free in multiple formats.{% endblocktrans %}</strong>
+    </p>
+    <p>
+        {% blocktrans with end=offer.end %}If we raise enought money before {{end}} we will
+        publish it and make it available for everyone.{% endblocktrans %}
+    </p>
+    {% endif %}
+{% else %}
+    {% if offer.is_win %}
+        <p>
+            {% trans "Full amount was successfully raised!" %}
+        </p>
+    {% else %}
+        <p>
+            {% trans "The amount needed was not raised." %}
+        </p>
+    {% endif %}
+{% endif %}
diff --git a/apps/funding/templates/funding/includes/offer_status_more.html b/apps/funding/templates/funding/includes/offer_status_more.html
new file mode 100644 (file)
index 0000000..c94c9a8
--- /dev/null
@@ -0,0 +1,26 @@
+{% load i18n %}
+
+{% if offer.is_current %}
+        <p>
+            {% include "funding/snippets/any_remaining.html" %}
+        </p>
+{% else %}
+    <p class="date">{% trans "Fundraiser span" %}: {{ offer.start }} – {{ offer.end }}</p>
+    {% if offer.is_win %}
+        <p>
+            {% if offer.book %}
+                {% blocktrans with bu=offer.book.get_absolute_url bt=offer.book %}The book
+                <a href="{{ bu }}">{{ bt }}</a> has been already published.{% endblocktrans %}
+            {% else %}
+                {% if offer.redakcja_url %}
+                    {% blocktrans with r=offer.redakcja_url %}You can follow
+                    the work on the <a href="{{ r }}">Editorial Platform</a>.{% endblocktrans %}
+                {% endif %}
+            {% endif %}
+        </p>
+    {% endif %}
+
+    {% if offer.remaining %}
+        <p>{% include "funding/snippets/any_remaining.html" %}</p>
+    {% endif %}
+{% endif %}
index 217d5b8..70930c4 100644 (file)
@@ -1,11 +1,11 @@
 {% extends "base.html" %}
 {% load url from future %}
 {% load i18n static %}
-{% load funding_tags %}
 {% load pagination_tags %}
 {% load fnp_share %}
 {% load thumbnail %}
 {% load build_absolute_uri from fnp_common %}
+{% load ssi_include from ssify %}
 
 
 {% block titleextra %}{{ object }}{% endblock %}
@@ -19,7 +19,7 @@
 
 <h1>{{ object }}</h1>
 
-{% funding object show_title=False %}
+{% ssi_include 'funding_detail_bar' pk=object.pk %}
 <div class="white-box">
     <div class="funding-details-intro">
         {% if object.cover %}
@@ -34,9 +34,9 @@
         <div class="normal-text">
         <h3>{% trans "Help free the book!" %}</h3>
         {{ object.description|safe }}
-        {% offer_status object %}
+        {% ssi_include 'funding_status' pk=object.pk %}
         <a href='//nowoczesnapolska.org.pl/pomoz-nam/wesprzyj-nas/' target="_blank" style='float:right;border:1px solid #ddd;padding: 1em;margin:0 0 1em 1em;background:#eee;'><img src='//nowoczesnapolska.org.pl/wp-content/themes/koed/annoy/procent.png' alt='1%' style='float:left;margin-right: 1em;margin-top:.2em;'>Możesz też przekazać<br/>1% podatku na rozwój biblioteki. &rarr;</a>
-        {% offer_status_more object %}
+        {% ssi_include 'funding_status_more' pk=object.pk %}
         <p><a href="{% url 'infopage' 'wesprzyj' %}">{% trans "Learn more" %}</a>.</p>
         </div>
         
 <h2>{% trans "Supporters" %}:</h2>
 
 <div class="white-box normal-text">
-{% with object.funding_payed.all as fundings %}
-    
-    <table class="wlfund">
-    {% autopaginate fundings 10 %}
-    {% for funding in fundings %}
-        <tr class="funding-plus">
-            <td class="oneline">{{ funding.payed_at.date }}</td>
-            <td>
-                {% if funding.name %}
-                    {{ funding.name }}
-                {% else %}
-                    <em>{% trans "Anonymous" %}</em>
-                {% endif %}
-            </td>
-            <td>{{ funding.amount }}&nbsp;zł</td>
-            <td>&nbsp;
-                {% for perk in funding.perks.all %}
-                    {{ perk.name }}{% if not forloop.last %},{% endif %}
-                {% endfor %}
-            </td>
-    {% endfor %}
-    </table>
-    
-    {% paginate %}
-{% endwith %}
+    {% ssi_include 'funding_fundings' pk=object.pk page=page %}
 </div>
 
 {% endblock %}
diff --git a/apps/funding/templates/funding/tags/funding.html b/apps/funding/templates/funding/tags/funding.html
deleted file mode 100755 (executable)
index 4628251..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-{% load i18n %}
-{% load time_tags %}
-{% if offer %}
-{% spaceless %}
-<div class="funding {{ add_class }}" data-offer-id="{{offer.id}}" style="">
-    {% if closeable %}<a href="#" class="close">X</a>{% endif %}
-    {% if link and is_current %}
-        <div class="call-area">
-            <a class="call honking" href="{% url 'funding_current' offer.slug %}">
-                {% trans "Support!" %}</a>
-            <div class="learn-more">
-                <a href="{% url 'infopage' 'wesprzyj' %}">{% trans "Learn more" %}</a>
-            </div>
-        </div>
-    {% endif %}
-    <div class="description {% if link and is_current %}with-button{% endif %}">
-    {% if link %}<a href="{% if is_current %}{% url 'funding_current' offer.slug %}{% else %}{{ offer.get_absolute_url }}{% endif %}">{% endif %}
-    {% if show_title %}
-        {% if is_current and show_title_calling %}<strong style="margin-right: .6em;">{% trans "Help free the book!" %}</strong>{% endif %}
-        <span class="funding-title{% if not is_current %}-strong{% endif %}">{{ offer }}</span>
-    {% endif %}
-
-    <div class="progress"
-        style="text-align: center; background-size: {{ percentage|stringformat:'.2f' }}% 1px;"
-    >
-        {% if sum %}
-            <span class="piece progress-collected">{% trans "collected" %}: {{ sum }} zł</span>
-        {% endif %}
-        {% if not is_win %}
-            <span class="piece progress-target"><span class="{% if sum %}progress-extralabel{% endif %}">{% trans "needed" %}: </span>{{ offer.target }} zł</span>
-        {% endif %}
-        {% if is_current %}
-            <span class="piece progress-until"><span class="progress-extralabel">{% trans "until fundraiser end" %}:</span>
-                <span class="countdown inline" data-until='{{ offer.end|date_to_utc:True|utc_for_js }}'></span>
-            </span>
-        {% else %}
-            <div style="clear: both"></div>
-        {% endif %}
-    </div>
-    {% if link %}</a>{% endif %}
-    </div>
-    <div style="clear: both"></div>
-</div>
-{% if closeable %}
-    <div class="funding-handle">{% trans "Help free the book!" %}</div>
-{% endif %}
-{% endspaceless %}
-{% endif %}
diff --git a/apps/funding/templates/funding/tags/offer_status.html b/apps/funding/templates/funding/tags/offer_status.html
deleted file mode 100755 (executable)
index 5828ce4..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-{% load i18n %}
-
-{% if offer.is_current %}
-    {% if offer.is_win %}
-        <p>
-            {% blocktrans with end=offer.end %}The fundraiser
-            ends on {{ end }}. The full amount has been successfully
-            raised, but you can still contribute and help liberate
-            more books.{% endblocktrans %}
-        </p>
-    {% else %}
-    <p>
-        <strong>{% blocktrans with target=offer.target|floatformat %}W need {{target}} zł to digitize it,
-        compile it and publish for free in multiple formats.{% endblocktrans %}</strong>
-    </p>
-    <p>
-        {% blocktrans with end=offer.end %}If we raise enought money before {{end}} we will
-        publish it and make it available for everyone.{% endblocktrans %}
-    </p>
-    {% endif %}
-{% else %}
-    {% if offer.is_win %}
-        <p>
-            {% trans "Full amount was successfully raised!" %}
-        </p>
-    {% else %}
-        <p>
-            {% trans "The amount needed was not raised." %}
-        </p>
-    {% endif %}
-{% endif %}
diff --git a/apps/funding/templates/funding/tags/offer_status_more.html b/apps/funding/templates/funding/tags/offer_status_more.html
deleted file mode 100755 (executable)
index c94c9a8..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-{% load i18n %}
-
-{% if offer.is_current %}
-        <p>
-            {% include "funding/snippets/any_remaining.html" %}
-        </p>
-{% else %}
-    <p class="date">{% trans "Fundraiser span" %}: {{ offer.start }} – {{ offer.end }}</p>
-    {% if offer.is_win %}
-        <p>
-            {% if offer.book %}
-                {% blocktrans with bu=offer.book.get_absolute_url bt=offer.book %}The book
-                <a href="{{ bu }}">{{ bt }}</a> has been already published.{% endblocktrans %}
-            {% else %}
-                {% if offer.redakcja_url %}
-                    {% blocktrans with r=offer.redakcja_url %}You can follow
-                    the work on the <a href="{{ r }}">Editorial Platform</a>.{% endblocktrans %}
-                {% endif %}
-            {% endif %}
-        </p>
-    {% endif %}
-
-    {% if offer.remaining %}
-        <p>{% include "funding/snippets/any_remaining.html" %}</p>
-    {% endif %}
-{% endif %}
index 1a7c872..5dbeec6 100755 (executable)
@@ -3,49 +3,18 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django import template
+from ssify import ssi_variable
+from ssify.utils import ssi_cache_control
 from ..models import Offer
 from ..utils import sanitize_payment_title
 
 register = template.Library()
 
 
-@register.inclusion_tag("funding/tags/funding.html", takes_context=True)
-def funding(context, offer=None, link=False, closeable=False, show_title=True, show_title_calling=True, add_class=""):
-    if offer is None and context.get('funding_no_show_current') is None:
-        offer = Offer.current()
-        is_current = True
-    elif offer is not None:
-        is_current = offer.is_current()
+@ssi_variable(register, patch_response=[ssi_cache_control(must_revalidate=True, max_age=0)])
+def current_offer(request):
+    offer = Offer.current()
+    return offer.pk if offer is not None else None
 
-    if offer is None:
-        return {}
-
-    offer_sum = offer.sum()
-    return {
-        'offer': offer,
-        'sum': offer_sum,
-        'is_current': is_current,
-        'is_win': offer_sum >= offer.target,
-        'missing': offer.target - offer_sum,
-        'percentage': 100 * offer_sum / offer.target,
-        'link': link,
-        'closeable': closeable,
-        'show_title': show_title,
-        'show_title_calling': show_title_calling,
-        'add_class': add_class,
-    }
-
-
-@register.inclusion_tag("funding/tags/offer_status.html")
-def offer_status(offer):
-    return {
-        'offer': offer,
-    }
-
-@register.inclusion_tag("funding/tags/offer_status_more.html")
-def offer_status_more(offer):
-    return {
-        'offer': offer,
-    }
 
 register.filter(sanitize_payment_title)
index 2b8e5d6..4d806e8 100644 (file)
@@ -8,7 +8,7 @@ from .views import (WLFundView, OfferDetailView, OfferListView,
                 ThanksView, NoThanksView, CurrentView, DisableNotifications)
 
 
-urlpatterns = patterns('',
+urlpatterns = patterns('funding.views',
 
     url(r'^$', CurrentView.as_view(), name='funding_current'),
     url(r'^teraz/$', CurrentView.as_view()),
@@ -23,4 +23,12 @@ urlpatterns = patterns('',
     url(r'^wylacz_email/$', DisableNotifications.as_view(), name='funding_disable_notifications'),
 
     url(r'^getpaid/', include('getpaid.urls')),
+
+    # Includes
+    url(r'^o/(?P<pk>\d+)/top-bar\.(?P<lang>.+)\.html$', 'top_bar', name='funding_top_bar'),
+    url(r'^o/(?P<pk>\d+)/detail-bar\.(?P<lang>.+)\.html$', 'detail_bar', name='funding_detail_bar'),
+    url(r'^o/(?P<pk>\d+)/list-bar\.(?P<lang>.+)\.html$', 'list_bar', name='funding_list_bar'),
+    url(r'^o/(?P<pk>\d+)/status\.(?P<lang>.+)\.html$', 'offer_status', name='funding_status'),
+    url(r'^o/(?P<pk>\d+)/status-more\.(?P<lang>.+)\.html$', 'offer_status_more', name='funding_status_more'),
+    url(r'^o/(?P<pk>\d+)/fundings/(?P<page>\d+)\.(?P<lang>.+)\.html$', 'offer_fundings', name='funding_fundings'),
 )
index 9dd18cc..7c9adef 100644 (file)
@@ -2,12 +2,15 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
+from django.core.paginator import Paginator, InvalidPage
 from django.core.urlresolvers import reverse
 from django.http import Http404
-from django.shortcuts import redirect, get_object_or_404
+from django.shortcuts import get_object_or_404, redirect, render
 from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import TemplateView, FormView, ListView
 from getpaid.models import Payment
+from ssify import ssi_included
+from ssify.utils import ssi_cache_control
 from . import app_settings
 from .forms import FundingForm
 from .models import Offer, Spent, Funding
@@ -94,6 +97,7 @@ class OfferDetailView(FormView):
     def get_context_data(self, *args, **kwargs):
         ctx = super(OfferDetailView, self).get_context_data(*args, **kwargs)
         ctx['object'] = self.object
+        ctx['page'] = self.request.GET.get('page', 1)
         if self.object.is_current():
             ctx['funding_no_show_current'] = True
         return ctx
@@ -153,3 +157,72 @@ class DisableNotifications(TemplateView):
     def post(self, *args, **kwargs):
         self.object.disable_notifications()
         return redirect(self.request.get_full_path())
+
+
+def offer_bar(request, pk, link=False, closeable=False, show_title=True, show_title_calling=True, add_class=""):
+    offer = get_object_or_404(Offer, pk=pk)
+    offer_sum = offer.sum()
+
+    return render(request, "funding/includes/funding.html", {
+        'offer': offer,
+        'sum': offer_sum,
+        'is_current': offer.is_current(),
+        'is_win': offer_sum >= offer.target,
+        'missing': offer.target - offer_sum,
+        'percentage': 100 * offer_sum / offer.target,
+        'link': link,
+        'closeable': closeable,
+        'show_title': show_title,
+        'show_title_calling': show_title_calling,
+        'add_class': add_class,
+    })
+
+
+@ssi_included(patch_response=[ssi_cache_control(must_revalidate=True)])
+def top_bar(request, pk):
+    return offer_bar(request, pk,
+        link=True, closeable=True, add_class="funding-top-header")
+
+
+@ssi_included(patch_response=[ssi_cache_control(must_revalidate=True)])
+def list_bar(request, pk):
+    return offer_bar(request, pk,
+        link=True, show_title_calling=False)
+
+
+@ssi_included(patch_response=[ssi_cache_control(must_revalidate=True)])
+def detail_bar(request, pk):
+    return offer_bar(request, pk,
+        show_title=False)
+
+
+@ssi_included(patch_response=[ssi_cache_control(must_revalidate=True)])
+def offer_status(request, pk):
+    offer = get_object_or_404(Offer, pk=pk)
+    return render(request, "funding/includes/offer_status.html", {
+        'offer': offer,
+    })
+
+
+@ssi_included(patch_response=[ssi_cache_control(must_revalidate=True)])
+def offer_status_more(request, pk):
+    offer = get_object_or_404(Offer, pk=pk)
+    return render(request, "funding/includes/offer_status_more.html", {
+        'offer': offer,
+    })
+
+
+@ssi_included(patch_response=[ssi_cache_control(must_revalidate=True)])
+def offer_fundings(request, pk, page):
+    offer = get_object_or_404(Offer, pk=pk)
+    fundings = offer.funding_payed()
+    paginator = Paginator(fundings, 10, 2)
+    try:
+        page_obj = paginator.page(int(page))
+    except InvalidPage:
+        raise Http404
+    return render(request, "funding/includes/fundings.html", {
+        "paginator": paginator,
+        "page_obj": page_obj,
+        "fundings": page_obj.object_list,
+    })
index db2d49b..75dae08 100755 (executable)
@@ -1,6 +1,5 @@
 {% extends "base.html" %}
 {% load i18n %}
-{% load chunks %}
 
 {% block titleextra %}{{ page.title }}{% endblock %}
 
index 93d605d..754b17e 100644 (file)
@@ -3,12 +3,10 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.conf.urls import patterns, url
-from django.http import HttpResponseRedirect
 
 
 urlpatterns = patterns('libraries.views',
     url(r'^$', 'main_view', name='libraries_main_view'),
-    url(r'^/$', lambda x: HttpResponseRedirect(x.path[:-1])),
-    url(r'^/(?P<slug>[a-zA-Z0-9_-]+)$', 'catalog_view', name='libraries_catalog_view'),
-    url(r'^/(?P<catalog_slug>[a-zA-Z0-9_-]+)/(?P<slug>[a-zA-Z0-9_-]+)$', 'library_view', name='libraries_library_view'),
+    url(r'^(?P<slug>[a-zA-Z0-9_-]+)$', 'catalog_view', name='libraries_catalog_view'),
+    url(r'^(?P<catalog_slug>[a-zA-Z0-9_-]+)/(?P<slug>[a-zA-Z0-9_-]+)$', 'library_view', name='libraries_library_view'),
 )
index ead442f..694f5b8 100644 (file)
@@ -71,7 +71,7 @@ class TagManager(models.Manager):
             if tag not in current_tags:
                 self.intermediary_table_model._default_manager.create(tag=tag, content_object=obj)
 
-        tags_updated.send(sender=obj, affected_tags=tags_to_add + tags_for_removal)
+        tags_updated.send(sender=type(obj), instance=obj, affected_tags=tags_to_add + tags_for_removal)
 
     def remove_tag(self, obj, tag):
         """
@@ -179,7 +179,7 @@ class TaggedItemManager(models.Manager):
         if not tags:
             return queryset.none()
         # TODO: presumes reverse generic relation
-        return queryset.filter(tag_relations__tag__in=tags)
+        return queryset.filter(tag_relations__tag__in=tags).distinct()
 
     def get_related(self, obj, queryset_or_model):
         """
index 3e2092e..e5b4421 100644 (file)
@@ -5,10 +5,12 @@
 from datetime import datetime
 from django.template import RequestContext
 from django.shortcuts import render_to_response, get_object_or_404
-from pdcounter import models
+from django.views.decorators import cache
 from suggest.forms import PublishingSuggestForm
+from . import models
 
 
+@cache.never_cache
 def book_stub_detail(request, slug):
     book = get_object_or_404(models.BookStub, slug=slug)
     if book.pd and not book.in_pd():
@@ -21,6 +23,7 @@ def book_stub_detail(request, slug):
         context_instance=RequestContext(request))
 
 
+@cache.never_cache
 def author_detail(request, slug):
     author = get_object_or_404(models.Author, slug=slug)
     if not author.alive():
index 5fc3722..42a26c3 100644 (file)
@@ -10,11 +10,8 @@ from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
 from django.core.files.storage import FileSystemStorage
 from django.utils.datastructures import SortedDict
-from django.template.loader import render_to_string
-from django.utils.safestring import mark_safe
-from django.core.cache import caches
-from catalogue.utils import split_tags
 from fnpdjango.utils.text.slughifi import slughifi
+from ssify import flush_ssi_includes
 from picture import tasks
 from StringIO import StringIO
 import jsonfield
@@ -23,13 +20,11 @@ import logging
 
 from PIL import Image
 
-from django.utils.translation import get_language, ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _
 from newtagging import managers
 from os import path
 
 
-permanent_cache = caches['permanent']
-
 picture_storage = FileSystemStorage(location=path.join(
         settings.MEDIA_ROOT, 'pictures'),
         base_url=settings.MEDIA_URL + "pictures/")
@@ -48,6 +43,8 @@ class PictureArea(models.Model):
     tags        = managers.TagDescriptor(catalogue.models.Tag)
     tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
 
+    short_html_url_name = 'picture_area_short'
+
     @classmethod
     def rectangle(cls, picture, kind, coords):
         pa = PictureArea()
@@ -56,35 +53,18 @@ class PictureArea(models.Model):
         pa.area = coords
         return pa
 
-    def reset_short_html(self):
-        if self.id is None:
+    def flush_includes(self, languages=True):
+        if not languages:
             return
-
-        cache_key = "PictureArea.short_html/%d/%s"
-        for lang, langname in settings.LANGUAGES:
-            permanent_cache.delete(cache_key % (self.id, lang))
-
-
-    def short_html(self):
-        if self.id:
-            cache_key = "PictureArea.short_html/%d/%s" % (self.id, get_language())
-            short_html = permanent_cache.get(cache_key)
-        else:
-            short_html = None
-
-        if short_html is not None:
-            return mark_safe(short_html)
-        else:
-            theme = self.tags.filter(category='theme')
-            theme = theme and theme[0] or None
-            thing = self.tags.filter(category='thing')
-            thing = thing and thing[0] or None
-            area = self
-            short_html = unicode(render_to_string(
-                    'picture/picturearea_short.html', locals()))
-            if self.id:
-                permanent_cache.set(cache_key, short_html)
-            return mark_safe(short_html)
+        if languages is True:
+            languages = [lc for (lc, _ln) in settings.LANGUAGES]
+        flush_ssi_includes([
+            template % (self.pk, lang)
+            for template in [
+                '/katalog/pa/%d/short.%s.html',
+                ]
+            for lang in languages
+            ])
 
 
 class Picture(models.Model):
@@ -114,6 +94,8 @@ class Picture(models.Model):
     tags        = managers.TagDescriptor(catalogue.models.Tag)
     tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
 
+    short_html_url_name = 'picture_short'
+
     class AlreadyExists(Exception):
         pass
 
@@ -123,15 +105,18 @@ class Picture(models.Model):
         verbose_name = _('picture')
         verbose_name_plural = _('pictures')
 
-    def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
+    def save(self, force_insert=False, force_update=False, **kwargs):
         from sortify import sortify
 
         self.sort_key = sortify(self.title)
 
-        ret = super(Picture, self).save(force_insert, force_update)
+        try:
+            author = self.tags.filter(category='author')[0].sort_key
+        except IndexError:
+            author = u''
+        self.sort_key_author = author
 
-        if reset_short_html:
-            self.reset_short_html()
+        ret = super(Picture, self).save(force_insert, force_update)
 
         return ret
 
@@ -327,48 +312,9 @@ class Picture(models.Model):
             self._info = info
         return self._info
 
-    def reset_short_html(self):
-        if self.id is None:
-            return
-
-        for area in self.areas.all().iterator():
-            area.reset_short_html()
-
-        try:
-            author = self.tags.filter(category='author')[0].sort_key
-        except IndexError:
-            author = u''
-        type(self).objects.filter(pk=self.pk).update(sort_key_author=author)
-
-        cache_key = "Picture.short_html/%d/%s"
-        for lang, langname in settings.LANGUAGES:
-            permanent_cache.delete(cache_key % (self.id, lang))
-
-    def short_html(self):
-        if self.id:
-            cache_key = "Picture.short_html/%d/%s" % (self.id, get_language())
-            short_html = permanent_cache.get(cache_key)
-        else:
-            short_html = None
-
-        if short_html is not None:
-            return mark_safe(short_html)
-        else:
-            tags = self.tags.filter(category__in=('author', 'kind', 'epoch', 'genre'))
-            tags = split_tags(tags)
-
-            short_html = unicode(render_to_string(
-                    'picture/picture_short.html',
-                    {'picture': self, 'tags': tags}))
-
-            if self.id:
-                permanent_cache.set(cache_key, short_html)
-            return mark_safe(short_html)
-
     def pretty_title(self, html_links=False):
         picture = self
-        names = [(tag.name,
-                  catalogue.models.Tag.create_url('author', tag.slug))
+        names = [(tag.name, tag.get_absolute_url())
                  for tag in self.tags.filter(category='author')]
         names.append((self.title, self.get_absolute_url()))
 
@@ -378,7 +324,19 @@ class Picture(models.Model):
             names = [tag[0] for tag in names]
         return ', '.join(names)
 
-    # copied from book.py, figure out
     def related_themes(self):
         return catalogue.models.Tag.objects.usage_for_queryset(
             self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
+
+    def flush_includes(self, languages=True):
+        if not languages:
+            return
+        if languages is True:
+            languages = [lc for (lc, _ln) in settings.LANGUAGES]
+        flush_ssi_includes([
+            template % (self.pk, lang)
+            for template in [
+                '/katalog/p/%d/short.%s.html',
+                ]
+            for lang in languages
+            ])
index a3cbf7d..51bd6b9 100644 (file)
@@ -2,7 +2,7 @@
 {% load i18n %}
 {% load picture_tags catalogue_tags pagination_tags %}
 {% load thumbnail %}
-{% load build_absolute_uri from common_tags %}
+{% load build_absolute_uri from fnp_common %}
 
 
 {% block ogimage %}{% thumbnail picture.image_file "535" upscale="false" as thumb %}{{ thumb.url|build_absolute_uri:request }}{% endthumbnail %}{% endblock %}
index 71cd702..4fce98a 100644 (file)
@@ -1,9 +1,7 @@
 {% extends "base.html" %}
 {% load i18n %}
-{% load chunks %}
-{% load picture_tags %}
-{% load thumbnail %}
 {% load static %}
+{% load ssi_include from ssify %}
 
 {% block bodyid %}picture-list{% endblock %}
 
@@ -15,7 +13,7 @@
 
     <div class="left-column"><div class="normal-text">
         {% block book_list_info %}
-            {% chunk 'picture-list' %}
+            {% ssi_include 'chunk' key='picture-list' %}
         {% endblock %}
     </div></div>
     <div class="right-column" id="logo-space">
@@ -30,7 +28,7 @@
   <ol class="work-list">{% spaceless %}
 {% for picture in book_list %}
    <li class="Picture-item">
- {% picture_short picture %} 
+   {% ssi_include 'picture_short' pk=picture.pk %}
    </li>
 {% endfor %}
   {% endspaceless %}</ol>
index 28cd34f..cb05889 100644 (file)
@@ -1,7 +1,6 @@
 {% extends "picture/picture_short.html" %}
 {% load i18n %}
 {% load picture_tags thumbnail %}
-{% load cite_promo from social_tags %}
 
 
 {% block box-class %}book-wide-box{% endblock %}
index 1767ce2..7e464fa 100644 (file)
@@ -12,16 +12,6 @@ register = template.Library()
 
 cropper = CustomCroppingEngine()
 
-@register.inclusion_tag('picture/picture_short.html', takes_context=True)
-def picture_short(context, picture):
-    context.update({
-        'picture': picture,
-        'main_link': picture.get_absolute_url(),
-        'request': context.get('request'),
-        'tags': split_tags(picture.tags),
-        })
-    return context
-
 @register.inclusion_tag('picture/picture_wide.html', takes_context=True)
 def picture_wide(context, picture):
     context.update({
index 11b08bc..4bc2ab0 100644 (file)
@@ -4,10 +4,11 @@
 #
 from collections import OrderedDict
 from django.contrib.auth.decorators import permission_required
-from django.shortcuts import render_to_response, get_object_or_404
+from django.shortcuts import render_to_response, get_object_or_404, render
 from django.template import RequestContext
-from picture.models import Picture
+from picture.models import Picture, PictureArea
 from catalogue.utils import split_tags
+from ssify import ssi_included
 
 # was picture/picture_list.html list (without thumbs)
 def picture_list(request, filter=None, get_filter=None, template_name='catalogue/picture_list.html', cache_key=None, context=None):
@@ -87,3 +88,23 @@ def import_picture(request):
         return HttpResponse(_("Error importing file: %r") % import_form.errors)
 
 
+@ssi_included
+def picture_short(request, pk):
+    picture = get_object_or_404(Picture, pk=pk)
+
+    return render(request, 'picture/picture_short.html', {
+        'picture': picture,
+        'main_link': picture.get_absolute_url(),
+        'request': request,
+        'tags': split_tags(picture.tags),
+        })
+
+
+@ssi_included
+def picturearea_short(request, pk):
+    area = get_object_or_404(PictureArea, pk=pk)
+    theme = area.tags.filter(category='theme')
+    theme = theme and theme[0] or None
+    thing = area.tags.filter(category='thing')
+    thing = thing and thing[0] or None
+    return render(request, 'picture/picturearea_short.html', locals())
index 8566a9d..4c4d2b5 100644 (file)
@@ -1,4 +1,5 @@
 {% load i18n %}
+{% load ssi_csrf_token from ssify %}
 
 {% if poll %}
     {% if voted_already %}
@@ -18,7 +19,7 @@
     {% else %}
         <div class="poll">
             <p>{{poll.question}}</p>
-            <form action="{{poll.get_absolute_url}}" method="post">{% csrf_token %}
+            <form action="{{poll.get_absolute_url}}" method="post">{% ssi_csrf_token %}
             {{ form.vote }}
             <input type="submit" value="{% trans "Submit" %}" />
             </form>
index ef5f50f..79540c6 100644 (file)
@@ -2,34 +2,36 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
-from django.views.decorators.http import require_http_methods\r
-from django.shortcuts import get_object_or_404, redirect, render_to_response\r
-from django.core.urlresolvers import reverse\r
+from django.core.urlresolvers import reverse
+from django.shortcuts import get_object_or_404, redirect, render_to_response
 from django.template import RequestContext
+from django.views.decorators import cache
+from django.views.decorators.http import require_http_methods
 
-from .models import Poll, PollItem\r
+from .models import Poll, PollItem
 from .forms import PollForm
 
-\r
-@require_http_methods(['GET', 'POST'])\r
+
+@cache.never_cache
+@require_http_methods(['GET', 'POST'])
 def poll(request, slug):
 
     poll = get_object_or_404(Poll, slug=slug, open=True)
 
-    if request.method == 'POST':\r
+    if request.method == 'POST':
         redirect_to = reverse('poll', args = [slug])
-        form = PollForm(request.POST, poll = poll)\r
-        if form.is_valid():\r
-            if not poll.voted(request.session):\r
-                try:\r
+        form = PollForm(request.POST, poll = poll)
+        if form.is_valid():
+            if not poll.voted(request.session):
+                try:
                     poll_item = PollItem.objects.filter(pk=form.cleaned_data['vote'], poll=poll).get()
-                except PollItem.DoesNotExist:\r
-                    pass\r
-                else:\r
+                except PollItem.DoesNotExist:
+                    pass
+                else:
                     poll_item.vote(request.session)
         return redirect(redirect_to)
     elif request.method == 'GET':
         context = RequestContext(request)
-        context['poll'] = poll\r
+        context['poll'] = poll
         context['voted_already'] = poll.voted(request.session)
         return render_to_response('polls/poll.html', context)
index 65d9427..8dbad9d 100644 (file)
@@ -10,7 +10,6 @@ from django import template
 # from django.db.models import Q
 # from django.utils.translation import ugettext as _
 from catalogue.models import Book
-from catalogue.templatetags.catalogue_tags import book_short
 import re
 # from catalogue.forms import SearchForm
 # from catalogue.utils import split_tags
@@ -49,6 +48,8 @@ def book_searched(context, result):
         snip = snip.replace("\n", "<br />").replace('---', '&mdash;')
         hit['snippet'] = snip
 
-    ctx = book_short(context, book)
-    ctx['hits'] = hits and zip(*hits)[1] or []
-    return ctx
+    return {
+        'request': context['request'],
+        'book': book,
+        'hits':  hits and zip(*hits)[1] or []
+    }
index df1b175..8902807 100644 (file)
@@ -4,8 +4,9 @@
 #
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
 from django.core.urlresolvers import reverse
-
+from ssify import flush_ssi_includes
 from catalogue.models import Book
 
 
@@ -44,3 +45,18 @@ class Cite(models.Model):
     def get_absolute_url(self):
         """This is used for testing."""
         return "%s?choose_cite=%d" % (reverse('main_page'), self.id)
+
+    def save(self, *args, **kwargs):
+        ret = super(Cite, self).save(*args, **kwargs)
+        self.flush_includes()
+        return ret
+
+    def flush_includes(self):
+        flush_ssi_includes([
+            template % (self.pk, lang)
+            for template in [
+                '/ludzie/cite/%s.%s.html',
+                '/ludzie/cite_main/%s.%s.html',
+            ]
+            for lang in [lc for (lc, _ln) in settings.LANGUAGES]] +
+            ['/ludzie/cite_info/%s.html' % self.pk])
diff --git a/apps/social/templates/social/cite_info.html b/apps/social/templates/social/cite_info.html
new file mode 100644 (file)
index 0000000..4ee84d3
--- /dev/null
@@ -0,0 +1,18 @@
+{% spaceless %}
+
+{% if cite.image %}
+    {% if cite.image_link %}<a href="{{ cite.image_link }}">{% endif %}
+    {% if cite.image_title %}
+        {{ cite.image_title }}{% else %}
+        untitled{% endif %}{% if cite.image_link %}</a>{% endif %},
+    {% if cite.image_author %}{{ cite.image_author }},{% endif %}
+    {% if cite.image_license_link %}<a href="{{ cite.image_license_link }}">{% endif %}
+    {{ cite.image_license }}
+    {% if cite.image_license_link %}</a>{% endif %}
+{% else %}
+    <a href="http://www.flickr.com/photos/lou/430980641/">books about architecture</a>,
+    saikofish@Flickr,
+    <a href="http://creativecommons.org/licenses/by-nc-sa/2.0/">CC BY NC SA</a>.
+{% endif %}
+
+{% endspaceless %}
\ No newline at end of file
index c3a73e8..15786cf 100755 (executable)
@@ -1,6 +1,13 @@
+{% spaceless %}
+
 {% load i18n %}
 
+{% if main %}
+    <section id="big-cite"{% if cite.image %} style="background-image: url('{{ cite.image.url }}'); background-position: 50% {{ cite.image_shift|default_if_none:50 }}%;"{% endif %} >
+{% endif %}
+
 {% if cite %}
+
 <a href="{{ cite.link }}" class="cite{% if cite.small %} cite-small{% endif %}">
     {% if cite.vip %}
         <p class='vip mono'><span>{{ cite.vip }} {% trans "recommends" %}:</span></p>
     <p class="source mono"><span>{{ cite.book.pretty_title }}</span></p>
     {% endif %}
 </a>
-{% else %}
-    {% if fallback %}
-        {% load fragment_promo from catalogue_tags %}
-        {% fragment_promo ctx %}
-    {% endif %}
+
 {% endif %}
+
+
+{% if main %}
+    </section>
+{% endif %}
+
+{% endspaceless %}
\ No newline at end of file
index 2f4a1d3..5974a2a 100755 (executable)
@@ -1,15 +1,16 @@
 {% load i18n %}
+{% load ssi_csrf_token from ssify %}
 <h1>{{ title }}</h1>
 
 <form action="{% url 'social_unlike_book' view_kwargs.slug %}" method="post" accept-charset="utf-8"
        class="cuteform{% if placeholdize %} hidelabels{% endif %}">
-{% csrf_token %}
+{% ssi_csrf_token %}
     <input type="submit" value="{% trans "Remove from my shelf" %}"/>
 </form>
 
 <form action="{{ request.get_full_path }}" method="post" accept-charset="utf-8"
        class="cuteform{% if placeholdize %} hidelabels{% endif %}">
-{% csrf_token %}
+{% ssi_csrf_token %}
 <ol>
     <div id="id___all__"></div>
     {{ form.as_ul }}
index eabc961..2baa0f0 100755 (executable)
@@ -1,7 +1,9 @@
-{% if tags.exists %}
+{% spaceless %}
+
 <ul class='social-shelf-tags'>
     {% for tag in tags %}
         <li><a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a></li>
     {% endfor %}
 </ul>
-{% endif %}
+
+{% endspaceless %}
\ No newline at end of file
index bb1b4bc..7065467 100755 (executable)
@@ -5,30 +5,36 @@
 from random import randint
 from django.db.models import Q
 from django import template
-from catalogue.models import Book
+from django.utils.functional import lazy
+from django.utils.cache import add_never_cache_headers
+from catalogue.models import Book, Tag
+from ssify import ssi_variable
+from ssify.utils import ssi_vary_on_cookie
 from social.models import Cite
 from social.utils import likes, cites_for_tags
 
 register = template.Library()
 
-register.filter('likes', likes)
 
+@ssi_variable(register, patch_response=[ssi_vary_on_cookie])
+def likes_book(request, book_id):
+    return likes(request.user, Book.objects.get(pk=book_id), request)
 
-@register.assignment_tag(takes_context=True)
-def choose_cite(context, ctx=None):
+
+def choose_cite(request, book_id=None, tag_ids=None):
     """Choose a cite for main page, for book or for set of tags."""
     try:
-        request = context['request']
         assert request.user.is_staff
         assert 'choose_cite' in request.GET
         cite = Cite.objects.get(pk=request.GET['choose_cite'])
     except (AssertionError, Cite.DoesNotExist):
-        if ctx is None:
-            cites = Cite.objects.all()
-        elif isinstance(ctx, Book):
-            cites = Cite.objects.filter(Q(book=ctx) | Q(book__ancestor=ctx))
+        if book_id is not None:
+            cites = Cite.objects.filter(Q(book=book_id) | Q(book__ancestor=book_id))
+        elif tag_ids is not None:
+            tags = Tag.objects.filter(pk__in=tag_ids)
+            cites = cites_for_tags(tags)
         else:
-            cites = cites_for_tags(ctx)
+            cites = Cite.objects.all()
         stickies = cites.filter(sticky=True)
         count = stickies.count()
         if count:
@@ -42,6 +48,12 @@ def choose_cite(context, ctx=None):
     return cite
 
 
+@ssi_variable(register, name='choose_cite', patch_response=[add_never_cache_headers])
+def choose_cite_tag(request, book_id=None, tag_ids=None):
+    cite = choose_cite(request, book_id, tag_ids)
+    return cite.pk if cite is not None else None
+
+
 @register.inclusion_tag('social/cite_promo.html')
 def render_cite(cite):
     return {
@@ -49,20 +61,18 @@ def render_cite(cite):
     }
 
 
-@register.inclusion_tag('social/cite_promo.html', takes_context=True)
-def cite_promo(context, ctx=None, fallback=False):
-    return {
-        'cite': choose_cite(context, ctx),
-        'fallback': fallback,
-        'ctx': ctx,
-    }
-
-
-@register.inclusion_tag('social/shelf_tags.html', takes_context=True)
-def shelf_tags(context, book):
-    user = context['request'].user
-    if not user.is_authenticated():
-        tags = []
-    else:
-        tags = book.tags.filter(category='set', user=user).exclude(name='')
-    return {'tags': tags}
+@ssi_variable(register, patch_response=[ssi_vary_on_cookie])
+def book_shelf_tags(request, book_id):
+    if not request.user.is_authenticated():
+        return None
+    book = Book.objects.get(pk=book_id)
+    lks = likes(request.user, book, request)
+    def get_value():
+        if not lks:
+            return ''
+        tags = book.tags.filter(category='set', user=request.user).exclude(name='')
+        if not tags:
+            return ''
+        ctx = {'tags': tags}
+        return template.loader.render_to_string('social/shelf_tags.html', ctx)
+    return lazy(get_value, unicode)()
index b25ab25..3642d91 100755 (executable)
@@ -3,14 +3,20 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.conf.urls import patterns, url
+from django.views.decorators.cache import never_cache
 from social.views import ObjectSetsFormView
 
 urlpatterns = patterns('social.views',
     url(r'^lektura/(?P<slug>[a-z0-9-]+)/lubie/$', 'like_book', name='social_like_book'),
     url(r'^lektura/(?P<slug>[a-z0-9-]+)/nie_lubie/$', 'unlike_book', name='social_unlike_book'),
-    url(r'^lektura/(?P<slug>[a-z0-9-]+)/polki/$', ObjectSetsFormView(), name='social_book_sets'),
+    url(r'^lektura/(?P<slug>[a-z0-9-]+)/polki/$', never_cache(ObjectSetsFormView()), name='social_book_sets'),
     url(r'^polka/$', 'my_shelf', name='social_my_shelf'),
 
+    # Includes
+    url(r'^cite/(?P<pk>\d+)\.(?P<lang>.+)\.html$', 'cite', name='social_cite'),
+    url(r'^cite_main/(?P<pk>\d+)\.(?P<lang>.+)\.html$', 'cite', {'main': True}, name='social_cite_main'),
+    url(r'^cite_info/(?P<pk>\d+).html$', 'cite_info', name='social_cite_info'),
+
     #~ 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<slug>%s)/usun$' % SLUG, 'remove_from_shelf', name='remove_from_shelf'),
     #~ url(r'^polki/$', 'user_shelves', name='user_shelves'),
index c6a9353..bf1c242 100755 (executable)
@@ -2,15 +2,44 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
+from collections import defaultdict
+from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
+from django.utils.functional import lazy
 from catalogue.models import Book, Tag
 from catalogue import utils
 from catalogue.tasks import touch_tag
 from social.models import Cite
 
 
-def likes(user, work):
-    return user.is_authenticated() and work.tags.filter(category='set', user=user).exists()
+def likes(user, work, request=None):
+    if not user.is_authenticated():
+        return False
+
+    if request is None:
+        return work.tags.filter(category='set', user=user).exists()
+
+    if not hasattr(request, 'social_likes'):
+        # tuple: unchecked, checked, liked
+        request.social_likes = defaultdict(lambda:(set(), set(), set()))
+
+    ct = ContentType.objects.get_for_model(type(work))
+    likes_t = request.social_likes[ct.pk]
+    if work.pk in likes_t[1]:
+        return work.pk in likes_t[2]
+    else:
+        likes_t[0].add(work.pk)
+        def _likes():
+            if likes_t[0]:
+                ids = tuple(likes_t[0])
+                likes_t[0].clear()
+                likes_t[2].update(Tag.intermediary_table_model.objects.filter(
+                    content_type_id=ct.pk, tag__user_id=user.pk,
+                    object_id__in=ids
+                ).distinct().values_list('object_id', flat=True))
+                likes_t[1].update(ids)
+            return work.pk in likes_t[2]
+        return lazy(_likes, bool)()
 
 
 def get_set(user, name):
index 446c5c4..49c9b70 100644 (file)
@@ -10,7 +10,9 @@ from django.views.decorators.http import require_POST
 from ajaxable.utils import AjaxableFormView
 
 from catalogue.models import Book
+from ssify import ssi_included
 from social import forms
+from .models import Cite
 from social.utils import get_set, likes, set_sets
 
 
@@ -69,3 +71,20 @@ def unlike_book(request, slug):
         return JsonResponse({"success": True, "msg": "ok", "like": False})
     else:
         return redirect(book)
+
+
+@ssi_included
+def cite(request, pk, main=False):
+    cite = get_object_or_404(Cite, pk=pk)
+    return render(request, 'social/cite_promo.html', {
+        'main': main,
+        'cite': cite,
+    })
+
+
+@ssi_included(use_lang=False)
+def cite_info(request, pk):
+    cite = get_object_or_404(Cite, pk=pk)
+    return render(request, 'social/cite_info.html', {
+        'cite': cite,
+    })
index fd3c3a9..0565b97 100644 (file)
@@ -12,6 +12,7 @@ from PIL import Image
 
 from jsonfield import JSONField
 from django.core.files.base import ContentFile
+from ssify import flush_ssi_includes
 
 THUMB_WIDTH = 120
 THUMB_HEIGHT = 120
@@ -94,7 +95,12 @@ class SponsorPage(models.Model):
             'sponsors': self.populated_sponsors(),
             'page': self
         })
-        return super(SponsorPage, self).save(*args, **kwargs)
+        ret = super(SponsorPage, self).save(*args, **kwargs)
+        self.flush_includes()
+        return ret
+
+    def flush_includes(self):
+        flush_ssi_includes(['/sponsors/page/%s.html' % self.name])
 
     def __unicode__(self):
         return self.name
diff --git a/apps/sponsors/templatetags/__init__.py b/apps/sponsors/templatetags/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/apps/sponsors/templatetags/sponsor_tags.py b/apps/sponsors/templatetags/sponsor_tags.py
deleted file mode 100644 (file)
index 3670123..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django import template
-from django.utils.safestring import mark_safe
-
-from sponsors import models
-
-
-register = template.Library()
-
-
-def sponsor_page(name):
-    try:
-        page = models.SponsorPage.objects.get(name=name)
-    except:
-        return u''
-    return mark_safe(page.html)
-
-sponsor_page = register.simple_tag(sponsor_page)
diff --git a/apps/sponsors/urls.py b/apps/sponsors/urls.py
new file mode 100644 (file)
index 0000000..6da6186
--- /dev/null
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns('sponsors.views',
+    url(r'^page/(?P<name>.+)\.html$', 'page', name='sponsor_page'),
+)
diff --git a/apps/sponsors/views.py b/apps/sponsors/views.py
new file mode 100644 (file)
index 0000000..9a34089
--- /dev/null
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.http import HttpResponse
+from ssify import ssi_included
+from .models import SponsorPage
+
+
+@ssi_included(use_lang=False)
+def page(request, name):
+    try:
+        page = SponsorPage.objects.get(name=name)
+    except SponsorPage.DoesNotExist:
+        return HttpResponse(u'')
+    return HttpResponse(page.html)
index 6efbb90..2ac0ec2 100755 (executable)
@@ -1,10 +1,11 @@
 {% load i18n %}
 {% load honeypot %}
+{% load ssi_csrf_token from ssify %}
 
 <h1>{% trans "Didn't find a book? Make a suggestion." %}</h1>
 
 <form id='suggest-publishing-form' action="{% url 'suggest_publishing' %}" method="post" accept-charset="utf-8" class="cuteform">
-{% csrf_token %}
+{% ssi_csrf_token %}
 {% render_honeypot_field %}
 <ol>
     <li><span class="error">{{ form.contact.errors }}</span><label for="id_contact">{{ form.contact.label }}</label> {{ form.contact }}</li>
index e69de29..56037a0 100644 (file)
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+default_app_config = 'wolnelektury_core.apps.WLCoreConfig'
diff --git a/apps/wolnelektury_core/apps.py b/apps/wolnelektury_core/apps.py
new file mode 100644 (file)
index 0000000..996ccae
--- /dev/null
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.apps import AppConfig
+
+class WLCoreConfig(AppConfig):
+    name = 'wolnelektury_core'
+
+    def ready(self):
+        from . import signals
diff --git a/apps/wolnelektury_core/models.py b/apps/wolnelektury_core/models.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/apps/wolnelektury_core/signals.py b/apps/wolnelektury_core/signals.py
new file mode 100644 (file)
index 0000000..5eb7e88
--- /dev/null
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf import settings
+from django.core.cache import caches
+from django.db.models.signals import post_save, post_delete
+from django.dispatch import receiver
+
+from funding.models import Spent
+from infopages.models import InfoPage
+from libraries.models import Catalog, Library
+from pdcounter.models import Author, BookStub
+
+
+@receiver([post_save, post_delete])
+def flush_views_after_manual_change(sender, **kwargs):
+    """Flushes views cache after changes with some models.
+
+    Changes to those models happen infrequently, so we can afford
+    to just flush the cache on those instances.
+
+    If changes become too often, relevant bits should be separated
+    as ssi_included views and flushed individually when needed.
+
+    """
+    if sender in (Catalog, Library, InfoPage, Author, BookStub, Spent):
+        caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
index 4b6c5d3..f2a8e26 100644 (file)
@@ -73,7 +73,7 @@
                                                $current = $hidden;
                         if ($(this).hasClass('load-menu') && !menu_loaded) {
                             $.ajax({
-                                url: '/katalog/',
+                                url: '/katalog/' + LANGUAGE_CODE + '.json',
                                 dataType: "json",
                             }).done(function(data) {
                                 $.each(data, function(index, value) {
index 671f32a..756b8ad 100644 (file)
@@ -1,9 +1,11 @@
 {% load i18n %}
+{% load ssi_csrf_token from ssify %}
+
 <h1>{{ title }}</h1>
 
 <form action="{{ request.get_full_path }}" method="post" accept-charset="utf-8"
        class="cuteform{% if placeholdize %} hidelabels{% endif %}">
-{% csrf_token %}
+{% ssi_csrf_token %}
 <ol>
     <div id="id_{% if form_prefix %}{{ form_prefix }}-{% endif %}__all__"></div>
     {{ form.as_ul }}
index 64b0b88..80d3d23 100755 (executable)
@@ -1,6 +1,7 @@
 {% extends "auth/login.html" %}
 {% load i18n %}
 {% load honeypot %}
+{% load ssi_csrf_token from ssify %}
 
 {% block extra %}
 
@@ -10,7 +11,7 @@
 
 <form action="{% url 'register' %}" method="post" accept-charset="utf-8"
        class="cuteform hidelabels">
-{% csrf_token %}
+{% ssi_csrf_token %}
 {% render_honeypot_field %}
 <ol>
     <div id="id_register-__all__"></div>
diff --git a/apps/wolnelektury_core/templates/latest_blog_posts.html b/apps/wolnelektury_core/templates/latest_blog_posts.html
new file mode 100644 (file)
index 0000000..24d486e
--- /dev/null
@@ -0,0 +1,9 @@
+{% spaceless %}
+
+<ol>
+{% for post in posts %}
+    <li><a href="{{ post.link }}">{{ post.title }}</a></li>
+{% endfor %}
+</ol>
+
+{% endspaceless %}
\ No newline at end of file
index fd51cf3..118f3ac 100755 (executable)
@@ -1,60 +1,51 @@
 {% extends "base.html" %}
 {% load static from staticfiles %}
-{% load cache chunks i18n catalogue_tags infopages_tags social_tags %}
+{% load i18n catalogue_tags infopages_tags social_tags %}
+{% load ssi_include from ssify %}
 
 
 {% block title %}{% trans "Wolne Lektury internet library" %}{% endblock %}
 {% block ogtitle %}{% trans "Wolne Lektury internet library" %}{% endblock %}
 
-{% block body %}
-    <section id="big-cite"{% if cite.image %}
-        style="
-            background-image: url('{{ cite.image.url }}');
-            background-position: 50% {{ cite.image_shift|default_if_none:50 }}%;
-        "{% endif %} >
-        {% render_cite cite %}
-    </section>
+{% block body %}{% spaceless %}
+
+    {% choose_cite as cite_pk %}
+    {{ cite_pk.if }}
+        {% ssi_include 'social_cite_main' pk=cite_pk %}
+    {{ cite_pk.endif }}
 
-    {% spaceless %}
 
 
     <section id="promo-box">
         <h1>{% trans "What's new?" %}</h1>
         <div id="promo-box-body">
-            {% chunk "promo" %}
+            {% ssi_include 'chunk' key='promo' %}
         </div>
     </section>
 
 
     <section id="main-last">
         <h1><a href="{% url 'recent_list' %}">{% trans "Recent publications" %}</a></h1>
-        {% cache 60 last-published-on-main LANGUAGE_CODE %}
             {% for book in last_published %}
-                {% book_mini book %}
+                {% ssi_include 'catalogue_book_mini' pk=book.pk %}
             {% endfor %}
-        {% endcache %}
     </section>
 
     <div class="clearboth"></div>
 
     <section class="infopages-box">
         <h1>{% trans "News" %}</h1>
-       {# 135 is the id of new publications category of our master blog. perhaps this URL should go to settings. #}
-        {% cache 1800 latest-blog-posts %}
-            {% latest_blog_posts "http://nowoczesnapolska.org.pl/feed/?cat=-135" %}
-        {% endcache %}
+        {% ssi_include 'latest_blog_posts' %}
     </section>
 
 
     <section class="infopages-box">
-        <h1>{% trans "Utilities" %}</h2>
+        <h1>{% trans "Utilities" %}</h1>
 
         <ul>
             <li><a href="{% url 'suggest' %}" id="suggest" class="ajaxable">{% trans "Report a bug or suggestion" %}</a></li>
             <!--li><a href="http://turniej.wolnelektury.pl">Turniej Elektrybałtów</a></li-->
-            <li><a href="{% url 'reporting_catalogue_pdf' %}">
-                       {% trans "Download the catalogue in PDF format." %}
-               </a></li>
+            <li><a href="{% url 'reporting_catalogue_pdf' %}">{% trans "Download the catalogue in PDF format." %}</a></li>
             <!--li><a href="{% url 'infopage' "widget" %}">{% trans "Widget" %}</a></li-->
             <li><a href="{% url 'suggest_publishing' %}" id="suggest-publishing" class="ajaxable">{% trans "Missing a book?" %}</a></li>
             <li><a href="{% url 'publish_plan' %}">{% trans "Publishing plan" %}</a></li>
     <section class="infopages-box">
         <h1>{% trans "Information" %}</h1>
         <ul>
-            <li><a href="http://nowoczesnapolska.org.pl/prywatnosc/">{% trans "Privacy policy" %}</a></li>
-        {% cache 60 infopages-on-main LANGUAGE_CODE %}
+            <li><a href="https://nowoczesnapolska.org.pl/prywatnosc/">{% trans "Privacy policy" %}</a></li>
             {% infopages_on_main %}
-        {% endcache %}
         </ul>
 
         <div class="social-links">
-            <a href="http://pl-pl.facebook.com/pages/Wolne-Lektury/203084073268"
-                title='Wolne Lektury @ Facebook'>
+            <a href="https://pl-pl.facebook.com/pages/Wolne-Lektury/203084073268" title='Wolne Lektury @ Facebook'>
                 <img src="{% static "img/social/f.png" %}" alt="Wolne Lektury @ Facebook" />
             </a>
-            <a href="http://nk.pl/profile/30441509"
-                title='Wolne Lektury @ NK'>
+            <a href="https://nk.pl/profile/30441509" title='Wolne Lektury @ NK'>
                 <img src="{% static "img/social/nk.png" %}" alt="Wolne Lektury @ NK.pl" />
             </a>
         </div>
     </section>
 
 
-    {% endspaceless %}
-
-{% endblock %}
-
-
-{% block add_footer %}
-<p>{% trans "Image used:" %} 
-{% if cite.image %}
-    {% if cite.image_link %}<a href="{{ cite.image_link }}">{% endif %}
-    {% if cite.image_title %}
-        {{ cite.image_title }}{% else %}
-        untitled{% endif %}{% if cite.image_link %}</a>{% endif %},
-    {% if cite.image_author %}{{ cite.image_author }},{% endif %}
-    {% if cite.image_license_link %}<a href="{{ cite.image_license_link }}">{% endif %}
-    {{ cite.image_license }}
-    {% if cite.image_license_link %}</a>{% endif %}
-{% else %}
-    <a href="http://www.flickr.com/photos/lou/430980641/">books about architecture</a>,
-    saikofish@Flickr,
-    <a href="http://creativecommons.org/licenses/by-nc-sa/2.0/">CC BY NC SA</a>.
-{% endif %}
-</p>
-{% endblock %}
+{% endspaceless %}{% endblock %}
+
+
+{% block add_footer %}{% spaceless %}
+{{ cite_pk.if }}
+    <p>{% trans "Image used:" %}</p>
+    {% ssi_include 'social_cite_info' pk=cite_pk %}
+    </p>
+{{ cite_pk.endif }}
+{% endspaceless %}{% endblock %}
index 7489d4a..432cf69 100644 (file)
@@ -2,25 +2,25 @@
 {% if is_paginated %}
 <div class="pagination">
     {% if page_obj.has_previous %}
-        <a href="?page={{ page_obj.previous_page_number }}{{ getvars }}" class="prev">&lsaquo;&lsaquo; {% trans "previous" %}</a>
+        <a href="?page={{ page_obj.previous_page_number }}{{ getvars }}" class="prev">&lsaquo;&lsaquo; {% trans "previous" %} </a>
     {% else %}
-        <span class="disabled prev">&lsaquo;&lsaquo; {% trans "previous" %}</span>
+        <span class="disabled prev">&lsaquo;&lsaquo; {% trans "previous" %} </span>
     {% endif %}
     {% for page in pages %}
         {% if page %}
             {% ifequal page page_obj.number %}
-                <span class="current page">{{ page }}</span>
+                <span class="current page"> {{ page }} </span>
             {% else %}
-                <a href="?page={{ page }}{{ getvars }}" class="page">{{ page }}</a>
+                <a href="?page={{ page }}{{ getvars }}" class="page"> {{ page }} </a>
             {% endifequal %}
         {% else %}
             ...
         {% endif %}
     {% endfor %}
     {% if page_obj.has_next %}
-        <a href="?page={{ page_obj.next_page_number }}{{ getvars }}" class="next">{% trans "next" %} &rsaquo;&rsaquo;</a>
+        <a href="?page={{ page_obj.next_page_number }}{{ getvars }}" class="next"> {% trans "next" %} &rsaquo;&rsaquo;</a>
     {% else %}
-        <span class="disabled next">{% trans "next" %} &rsaquo;&rsaquo;</span>
+        <span class="disabled next"> {% trans "next" %} &rsaquo;&rsaquo;</span>
     {% endif %}
 </div>
 {% endif %}
index 46afe28..c3259a3 100644 (file)
@@ -1,10 +1,12 @@
 <!DOCTYPE html>
+{% spaceless %}
 <html lang="{{ LANGUAGE_CODE }}" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
-       {% load cache compressed i18n %}
-       {% load static from staticfiles %}
-    {% load catalogue_tags funding_tags reporting_stats sponsor_tags %}
-    {% load chunks %}
+    {% load compressed i18n %}
+    {% load static from staticfiles %}
+    {% load catalogue_tags funding_tags reporting_stats %}
     {% load piwik_tags %}
+    {% load ssi_include ssi_csrf_token from ssify %}
+    {% load user_username user_is_staff from common_tags %}
     <head>
         <meta charset="utf-8">
         <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
         <meta property="og:title" content="{% block ogtitle %}{% endblock %}" />
         <meta property="og:type" content="{% block ogtype %}website{% endblock %}" />
         <meta property="og:image" content="{% block ogimage %}{{ FULL_STATIC_URL }}img/wiatrak.png{% endblock %}" />
-        <meta name="description" 
-            content="{% block metadescription %}Darmowe, opracowane, pełne teksty lektur, e-booki, audiobooki i pliki DAISY na wolnej licencji.{% endblock %}" />
+        <meta name="description" content="{% block metadescription %}Darmowe, opracowane, pełne teksty lektur, e-booki, audiobooki i pliki DAISY na wolnej licencji.{% endblock %}" />
         {% block ogextra %}{% endblock %}
-        
 
-        <title>{% block title %}{% trans "Wolne Lektury" %} :: 
-            {% block titleextra %}{% endblock %}{% endblock %}</title>
+        <title>{% block title %}{% trans "Wolne Lektury" %} :: {% block titleextra %}{% endblock %}{% endblock %}</title>
         <link rel="icon" href="{% static 'img/favicon.png' %}" type="image/png" />
-        <link rel="search" type="application/opensearchdescription+xml" title="Wolne Lektury" 
-            href="{% static 'opensearch.xml' %}" />
+        <link rel="search" type="application/opensearchdescription+xml" title="Wolne Lektury" href="{% static 'opensearch.xml' %}" />
         {% compressed_css "main" %}
         {% block extrahead %}
         {% endblock %}
     <body id="{% block bodyid %}base{% endblock %}">
 
         {% block bodycontent %}
-        {% funding link=1 closeable=1 add_class="funding-top-header" %}
+
+        {% if not funding_no_show_current %}
+            {% current_offer as current_offer %}
+            {{ current_offer.if }}
+                {% ssi_include 'funding_top_bar' pk=current_offer %}
+            {{ current_offer.endif }}
+        {% endif %}
+
         <div id="header-wrapper">
         <header id="main">
             <a href="/" id="logo">
-                <img src="{% static 'img/logo-neon.png' %}"
-                    alt="Wolne Lektury" />
+                <img src="{% static 'img/logo-neon.png' %}" alt="Wolne Lektury" />
             </a>
 
             <p id="user-info">
-                {% if user.is_authenticated %}
-                    {% trans "Welcome" %}, 
-                    <span class="hidden-box-wrapper">
+                {% user_username as user_username %}
+                {% user_is_staff as user_is_staff %}
+                {{ user_username.if }}{% trans "Welcome" %}, <span class="hidden-box-wrapper">
                         <a href="{% url 'user_settings' %}" class="hidden-box-trigger">
-                            <strong>{{ user.username }}</strong>
+                            <strong>{{ user_username }}</strong>
                         </a>
                         <span id="user-menu" class="hidden-box">
                             <a href="{% url 'account_set_password' %}">{% trans "Password" %}</a><br/>
                             <a href="{% url 'account_email' %}">{% trans "E-mail" %}</a><br/>
                             <a href="{% url 'socialaccount_connections' %}">{% trans "Social accounts" %}</a><br/>
                         </span>
-                    </span>
-                    | <a href="{% url 'social_my_shelf' %}" id="user-shelves-link">{% trans "My shelf" %}</a>
-                    {% if user.is_staff %}
-                    | <a href="/admin/">{% trans "Administration" %}</a>
-                    {% endif %}
-                    | <a href="{% url 'logout' %}?next={% block logout %}{{ request.get_full_path }}{% endblock %}">{% trans "Logout" %}</a>
-                {% else %}
-                    <a href="{% url 'login' %}?next={{ request.path }}"
-                        id="login" class="ajaxable">
-                            {% trans "Sign in" %}</a>
-                    /
-                    <a href="{% url 'register' %}?next={{ request.path }}"
-                        id="register" class="ajaxable">
-                            {% trans "Register" %}</a>
-                {% endif %}
+                    </span> | <a href="{% url 'social_my_shelf' %}" id="user-shelves-link">{% trans "My shelf" %}</a>
+                {{ user_username.endif }}
+                {{ user_is_staff.if }} | <a href="/admin/">{% trans "Administration" %}</a>
+                {{ user_is_staff.endif }}
+                {{ user_username.if }} | <a href="{% url 'logout' %}?next={% block logout %}{{ request.get_full_path }}{% endblock %}">{% trans "Logout" %}</a>
+                {{ user_username.else }}
+                    <a href="{% url 'login' %}?next={{ request.path }}" id="login" class="ajaxable">{% trans "Sign in" %}</a> / <a href="{% url 'register' %}?next={{ request.path }}" id="register" class="ajaxable">{% trans "Register" %}</a>
+                {{ user_username.endif }}
             </p>
 
             <p id="tagline">
-                {% cache 60 tagline LANGUAGE_CODE %}
                     {% url 'book_list' as b %}
                     {% url 'infopage' 'prawa' as r %}
                         {% count_books book_count %}
@@ -77,7 +73,6 @@
                     {% plural %}
                     <a href='{{b}}'>{{c}}</a> free readings you have <a href='{{r}}'>right to</a>
                     {% endblocktrans %}
-                {% endcache %}
             </p>
 
             <form id="search-area" action="{% url 'search' %}">
             <div id="lang-menu" class="hoverget">
                 <span id='lang-button' class='hoverclick'>
                     <span class="lang-flag">⚐</span>
-                    <span class="label">{% trans "Language versions" %}</span>
+                    <span class="label"> {% trans "Language versions" %}</span>
                 </span>
                 <div id="lang-menu-items">
                 {% for lang in LANGUAGES %}
                     <form action="{% url 'django.views.i18n.set_language' %}" method="post">
-                    {% csrf_token %}
+                    {% ssi_csrf_token %}
                     <input type="hidden" name="language" value="{{ lang.0 }}" />
-                    <button type="submit"
-                        lang="{{ lang.0 }}"
-                        class="{% ifequal lang.0 LANGUAGE_CODE %}active{% endifequal %}"
-                        >{{ lang.1 }}</button>
+                    <button type="submit" lang="{{ lang.0 }}" class="{% ifequal lang.0 LANGUAGE_CODE %}active{% endifequal %}">{{ lang.1 }}</button>
                     </form>
                 {% endfor %}
                 </div>
 
         <div id="footer-wrapper">
         <footer id="main">
-            {% chunk 'footer' %}
+            {% ssi_include 'chunk' key='footer' %}
             {% block add_footer %}{% endblock %}
-            {% sponsor_page "footer" %}
+            {% ssi_include 'sponsor_page' name='footer' %}
         </footer>
         </div>
 
 
 
         <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
-        <script type="text/javascript">
-            var LANGUAGE_CODE = "{{ LANGUAGE_CODE }}";
-            var STATIC_URL = "{{ STATIC_URL }}";
-        </script>
+        <script type="text/javascript">var LANGUAGE_CODE="{{ LANGUAGE_CODE }}"; var STATIC_URL="{{ STATIC_URL }}";</script>
         {% compressed_js "base" %}
 
         {% tracking_code %}
         <script src="{% static "js/contrib/modernizr.custom.19652.js" %}"></script>
     </body>
 </html>
+{% endspaceless %}
\ No newline at end of file
index eddf9f1..ab08a3e 100644 (file)
@@ -3,9 +3,17 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django import template
+from ssify import ssi_variable
+from ssify.utils import ssi_vary_on_cookie
+
 register = template.Library()
 
 
-@register.filter
-def build_absolute_uri(uri, request):
-    return request.build_absolute_uri(uri)
+@ssi_variable(register, patch_response=[ssi_vary_on_cookie])
+def user_username(request):
+    return request.user.username
+
+
+@ssi_variable(register, patch_response=[ssi_vary_on_cookie])
+def user_is_staff(request):
+    return request.user.is_staff
index 80156bf..1ccd24a 100644 (file)
@@ -2,33 +2,32 @@
 # 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 datetime import date, datetime
 import feedparser
 
+from django.conf import settings
 from django.contrib import auth
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
 from django.core.cache import cache
 from django.http import HttpResponse, HttpResponseRedirect
-from django.shortcuts import render_to_response
-from django.template import RequestContext
+from django.shortcuts import render
 from django.utils.http import urlquote_plus
 from django.utils.translation import ugettext_lazy as _
 from django.views.decorators.cache import never_cache
 
-from django.conf import settings
 from ajaxable.utils import AjaxableFormView
-from catalogue.models import Book
 from ajaxable.utils import placeholdized
-from social.templatetags.social_tags import choose_cite
+from catalogue.models import Book
+from ssify import ssi_included
 
 
 def main_page(request):
     last_published = Book.objects.exclude(cover_thumb='').filter(parent=None).order_by('-created_at')[:4]
-    cite = choose_cite(RequestContext(request))
 
-    return render_to_response("main_page.html", locals(),
-        context_instance=RequestContext(request))
+    return render(request, "main_page.html", {
+        'last_published': last_published,
+    })
 
 
 class LoginFormView(AjaxableFormView):
@@ -119,11 +118,30 @@ def publish_plan(request):
                     })
         cache.set(cache_key, plan, 1800)
 
-    return render_to_response("publish_plan.html", {'plan': plan},
-        context_instance=RequestContext(request))
+    return render(request, "publish_plan.html", {'plan': plan})
 
 
 @login_required
 def user_settings(request):
-    return render_to_response("user.html",
-        context_instance=RequestContext(request))
+    return render(request, "user.html")
+
+
+@ssi_included(use_lang=False, timeout=1800)
+def latest_blog_posts(request, feed_url=None, posts_to_show=5):
+    if feed_url is None:
+        feed_url = settings.LATEST_BLOG_POSTS
+    try:
+        feed = feedparser.parse(str(feed_url))
+        posts = []
+        for i in range(posts_to_show):
+            pub_date = feed['entries'][i].published_parsed
+            published = date(pub_date[0], pub_date[1], pub_date[2])
+            posts.append({
+                'title': feed['entries'][i].title,
+                'summary': feed['entries'][i].summary,
+                'link': feed['entries'][i].link,
+                'date': published,
+                })
+    except:
+        posts = []
+    return render(request, 'latest_blog_posts.html', {'posts': posts})
index cdab158..4d2d4ad 100644 (file)
@@ -1,23 +1,21 @@
--i http://py.mdrn.pl/simple/ 
+-i http://py.mdrn.pl/simple/
 
 # django
 Django>=1.7,<1.8
 fnpdjango>=0.1.15,<0.2
-South>=0.7 # migrations for django
 django-pipeline>=1.3,<1.4
 django-pagination>=1.0
 django-maintenancemode>=0.10
 django-piston==0.2.2.1.2
-jsonfield>=0.9.20
+jsonfield>=0.9.22,<1.0
 django-picklefield
 django-modeltranslation==0.8b2
-django-allauth>=0.16,<0.17
+# django-allauth>=0.17,<0.18
+# django-allauth pre-0.18 version with Django 1.7 migrations
+-e git+git://github.com/pennersr/django-allauth.git@9cc09402d3dd768bc1221e0bb7a438d6295812f5#egg=django-allauth
 
 pytz
 
-# Some contrib apps still need it
-simplejson
-
 django-honeypot
 django-uni-form
 
@@ -54,3 +52,4 @@ sunburnt
 django-getpaid>=1.6,<1.7
 httplib2
 Texml
+django-ssify>=0.2.1,<0.3
index 58fbc42..4f61940 100644 (file)
@@ -26,17 +26,21 @@ TEMPLATE_CONTEXT_PROCESSORS = (
 )
 
 MIDDLEWARE_CLASSES = [
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'ssify.middleware.SsiMiddleware',
+    'django.middleware.cache.UpdateCacheMiddleware',
+    'ssify.middleware.PrepareForCacheMiddleware',
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.admindocs.middleware.XViewMiddleware',
     'pagination.middleware.PaginationMiddleware',
-    'django.middleware.locale.LocaleMiddleware',
+    'ssify.middleware.LocaleMiddleware',
     'maintenancemode.middleware.MaintenanceModeMiddleware',
     'django.middleware.common.CommonMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'fnpdjango.middleware.SetRemoteAddrFromXRealIP',
+    'django.middleware.cache.FetchFromCacheMiddleware',
 ]
 
 ROOT_URLCONF = 'wolnelektury.urls'
@@ -89,15 +93,13 @@ INSTALLED_APPS_CONTRIB = [
     'pipeline',
     'piston',
     'piwik',
-    #'rosetta',
-    #'south',
     'sorl.thumbnail',
     'kombu.transport.django',
     'honeypot',
-    #'django_nose',
     'fnpdjango',
     'getpaid',
     'getpaid.backends.payu',
+    'ssify',
 
     #allauth stuff
     'uni_form',
index d4beab3..a9cc70f 100644 (file)
@@ -1,6 +1,3 @@
-from os import path
-from .paths import PROJECT_DIR
-
 CACHES = {
     'default': {
         'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
@@ -8,17 +5,14 @@ CACHES = {
             '127.0.0.1:11211',
         ]
     },
-    'permanent': {
+    'ssify': {
         'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
         'TIMEOUT': None,
+        'KEY_PREFIX': 'ssify',
         '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_SECONDS = 24 * 60 * 60
index a0bab7a..8830ebe 100644 (file)
@@ -1,6 +1,3 @@
-# seconds until a changes appears in the changes api
-API_WAIT = 10
-
 # limit number of filtering tags
 MAX_TAG_LIST = 6
 
@@ -16,3 +13,5 @@ CATALOGUE_CUSTOMPDF_RATE_LIMIT = '1/m'
 # set to 'new' or 'old' to skip time-consuming test
 # for TeX morefloats library version
 LIBRARIAN_PDF_MOREFLOATS = None
+
+LATEST_BLOG_POSTS = "http://nowoczesnapolska.org.pl/feed/?cat=-135"
index abe872f..2dfd4d4 100644 (file)
@@ -2,8 +2,6 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
-import os
-
 from django.conf.urls import include, patterns, url
 from django.conf import settings
 from django.contrib import admin
@@ -23,6 +21,11 @@ urlpatterns = patterns('wolnelektury_core.views',
     url(r'^uzytkownik/signup/$', wolnelektury_core.views.RegisterFormView(), name='register'),
     url(r'^uzytkownik/logout/$', 'logout_then_redirect', name='logout'),
     url(r'^uzytkownik/zaloguj-utworz/$', wolnelektury_core.views.LoginRegisterFormView(), name='login_register'),
+
+    # Includes.
+    url(r'^latests_blog_posts.html$',
+        wolnelektury_core.views.latest_blog_posts,
+        name='latest_blog_posts'),
 )
 
 urlpatterns += patterns('',
@@ -38,7 +41,9 @@ urlpatterns += patterns('',
     url(r'^czekaj/', include('waiter.urls')),
     url(r'^wesprzyj/', include('funding.urls')),
     url(r'^ankieta/', include('polls.urls')),
-    url(r'^biblioteki', include('libraries.urls')),
+    url(r'^biblioteki/', include('libraries.urls')),
+    url(r'^chunks/', include('chunks.urls')),
+    url(r'^sponsors/', include('sponsors.urls')),
 
     # Admin panel
     url(r'^admin/catalogue/book/import$', 'catalogue.views.import_book', name='import_book'),
@@ -76,9 +81,3 @@ urlpatterns += patterns('',
     url(r'^wolontariat/$', RedirectView.as_view(
         url='/info/mozesz-nam-pomoc/')),
 )
-    
-
-if 'rosetta' in settings.INSTALLED_APPS:
-    urlpatterns += patterns('',
-        url(r'^rosetta/', include('rosetta.urls')),
-    )