From: Radek Czajka Date: Fri, 19 Sep 2014 14:38:11 +0000 (+0200) Subject: Using cache middleware instead of various caching micro-strategies, X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/d69fc5f16ed739c02685e7d695abf6de59c2bf5e?ds=sidebyside Using cache middleware instead of various caching micro-strategies, 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. --- diff --git a/apps/ajaxable/templates/ajaxable/form.html b/apps/ajaxable/templates/ajaxable/form.html index 38113dbb6..13586ac9d 100755 --- a/apps/ajaxable/templates/ajaxable/form.html +++ b/apps/ajaxable/templates/ajaxable/form.html @@ -1,10 +1,11 @@ {% load i18n %} +{% load ssi_csrf_token from ssify %}

{{ title }}

-{% 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 index 000000000..2f6f7e75f --- /dev/null +++ b/apps/api/emitters.py @@ -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 "" % 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 '\n' \ + '%s' % \ + ''.join(self.construct().get_ssis('xml')) + +Emitter.register('xml', SsiXmlEmitter, 'text/xml; charset=utf-8') + diff --git a/apps/api/handlers.py b/apps/api/handlers.py index a3a5ce599..5fe931e69 100644 --- a/apps/api/handlers.py +++ b/apps/api/handlers.py @@ -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') diff --git a/apps/api/templates/api/main.html b/apps/api/templates/api/main.html index 5c81a2d2d..04244dc91 100755 --- a/apps/api/templates/api/main.html +++ b/apps/api/templates/api/main.html @@ -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 %} diff --git a/apps/api/tests.py b/apps/api/tests.py index 87c4f75d8..94abe53b5 100644 --- a/apps/api/tests.py +++ b/apps/api/tests.py @@ -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.') diff --git a/apps/api/urls.py b/apps/api/urls.py index 7c12c017f..e9b106cdc 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -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 header and 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\d*?)\.(?Pxml|json|yaml)$', book_changes_resource), - url(r'^tag_changes/(?P\d*?)\.(?Pxml|json|yaml)$', tag_changes_resource), - # used by mobile app - url(r'^changes/(?P\d*?)\.(?Pxml|json|yaml)$', changes_resource), + url(r'^include/(?Pbook|fragment|tag)/(?P\d+)\.(?P.+)\.(?Pxml|json)$', + incl, name='api_include'), # info boxes (used by mobile app) url(r'book/(?P\d*?)/info\.html$', 'catalogue.views.book_info'), url(r'tag/(?P\d*?)/info\.html$', 'catalogue.views.tag_info'), - # books by collections url(r'^collections/$', collection_list_resource, name="api_collections"), url(r'^collections/(?P[^/]+)/$', collection_resource, name="api_collection"), diff --git a/apps/catalogue/__init__.py b/apps/catalogue/__init__.py index 2e7a89f90..f3d44e104 100644 --- a/apps/catalogue/__init__.py +++ b/apps/catalogue/__init__.py @@ -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 index 000000000..54bfc8f8b --- /dev/null +++ b/apps/catalogue/apps.py @@ -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 diff --git a/apps/catalogue/fields.py b/apps/catalogue/fields.py index d5cec2e62..884ecef8d 100644 --- a/apps/catalogue/fields.py +++ b/apps/catalogue/fields.py @@ -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"]) diff --git a/apps/catalogue/helpers.py b/apps/catalogue/helpers.py index ddfa4828e..7ca2cbd3a 100644 --- a/apps/catalogue/helpers.py +++ b/apps/catalogue/helpers.py @@ -1,253 +1,58 @@ -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)) diff --git a/apps/catalogue/management/commands/checkintegrity.py b/apps/catalogue/management/commands/checkintegrity.py index 51fcd94c7..6ae2b9a2f 100644 --- a/apps/catalogue/management/commands/checkintegrity.py +++ b/apps/catalogue/management/commands/checkintegrity.py @@ -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']: diff --git a/apps/catalogue/migrations/0002_book_ancestor.py b/apps/catalogue/migrations/0002_book_ancestor.py index 4aa582819..9f304c3d7 100644 --- a/apps/catalogue/migrations/0002_book_ancestor.py +++ b/apps/catalogue/migrations/0002_book_ancestor.py @@ -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 index 000000000..b6117570b --- /dev/null +++ b/apps/catalogue/migrations/0003_populate_ancestors.py @@ -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 index 000000000..916224b25 --- /dev/null +++ b/apps/catalogue/migrations/0004_remove_booktags_count_related_info.py @@ -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', + ), + ] diff --git a/apps/catalogue/models/__init__.py b/apps/catalogue/models/__init__.py index 7651a9f87..73b51090c 100644 --- a/apps/catalogue/models/__init__.py +++ b/apps/catalogue/models/__init__.py @@ -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 * diff --git a/apps/catalogue/models/book.py b/apps/catalogue/models/book.py index e499afc7b..3c32481f9 100644 --- a/apps/catalogue/models/book.py +++ b/apps/catalogue/models/book.py @@ -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. diff --git a/apps/catalogue/models/collection.py b/apps/catalogue/models/collection.py index acb01b6a3..098501eba 100644 --- a/apps/catalogue/models/collection.py +++ b/apps/catalogue/models/collection.py @@ -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]) diff --git a/apps/catalogue/models/fragment.py b/apps/catalogue/models/fragment.py index 283a7d943..a3dbdea54 100644 --- a/apps/catalogue/models/fragment.py +++ b/apps/catalogue/models/fragment.py @@ -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 index d414eb27f..000000000 --- a/apps/catalogue/models/listeners.py +++ /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() diff --git a/apps/catalogue/models/source.py b/apps/catalogue/models/source.py index d13161378..9aff4efec 100644 --- a/apps/catalogue/models/source.py +++ b/apps/catalogue/models/source.py @@ -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 diff --git a/apps/catalogue/models/tag.py b/apps/catalogue/models/tag.py index fb2511891..0e9442f7d 100644 --- a/apps/catalogue/models/tag.py +++ b/apps/catalogue/models/tag.py @@ -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 index 000000000..72607213d --- /dev/null +++ b/apps/catalogue/signals.py @@ -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]]) diff --git a/apps/catalogue/tasks.py b/apps/catalogue/tasks.py index 7d180687c..159494e0f 100644 --- a/apps/catalogue/tasks.py +++ b/apps/catalogue/tasks.py @@ -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 diff --git a/apps/catalogue/templates/catalogue/audiobook_list.html b/apps/catalogue/templates/catalogue/audiobook_list.html index 1358025a1..75576f4ad 100644 --- a/apps/catalogue/templates/catalogue/audiobook_list.html +++ b/apps/catalogue/templates/catalogue/audiobook_list.html @@ -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 %} diff --git a/apps/catalogue/templates/catalogue/book_detail.html b/apps/catalogue/templates/catalogue/book_detail.html index 454bce150..398a43c9a 100644 --- a/apps/catalogue/templates/catalogue/book_detail.html +++ b/apps/catalogue/templates/catalogue/book_detail.html @@ -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 @@

{% trans "Other versions" %}:

{% for rel in book.other_versions %} - {% book_mini rel %} + {% ssi_include 'book_mini' pk=rel.pk %} {% endfor %}
{% endif %} diff --git a/apps/catalogue/templates/catalogue/book_list.html b/apps/catalogue/templates/catalogue/book_list.html index 260c71fdf..e29cd50ec 100644 --- a/apps/catalogue/templates/catalogue/book_list.html +++ b/apps/catalogue/templates/catalogue/book_list.html @@ -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 %} +

{% block book_list_header %}{% trans "Listing of all works" %}{% endblock %}

{% block book_list_info %} - {% chunk 'book-list' %} + {% ssi_include 'chunk' key='book-list' %} {% endblock %}
@@ -32,4 +34,6 @@ {% endblock %} {% trans "↑ top ↑" %} + +{% endspaceless %} {% endblock %} diff --git a/apps/catalogue/templates/catalogue/book_mini_box.html b/apps/catalogue/templates/catalogue/book_mini_box.html index 49257b1c3..88ec16dfd 100755 --- a/apps/catalogue/templates/catalogue/book_mini_box.html +++ b/apps/catalogue/templates/catalogue/book_mini_box.html @@ -1,11 +1,11 @@ +{% spaceless %} - - +{% endspaceless %} \ No newline at end of file diff --git a/apps/catalogue/templates/catalogue/book_searched.html b/apps/catalogue/templates/catalogue/book_searched.html index 783b14a29..357491ce6 100644 --- a/apps/catalogue/templates/catalogue/book_searched.html +++ b/apps/catalogue/templates/catalogue/book_searched.html @@ -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 %} +
+ +{% ssi_include 'catalogue_book_short' pk=book.pk %} -{% block right-column %}
{% for hit in hits %} {% if hit.snippet %} @@ -28,5 +31,8 @@ {% endfor %}
-{% endblock %} +
+ +
+{% endspaceless %} \ No newline at end of file diff --git a/apps/catalogue/templates/catalogue/book_short.html b/apps/catalogue/templates/catalogue/book_short.html index 58859af38..b069cb585 100644 --- a/apps/catalogue/templates/catalogue/book_short.html +++ b/apps/catalogue/templates/catalogue/book_short.html @@ -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 %}
@@ -10,15 +12,14 @@
-
+{% likes_book book.pk as likes %} +
- {% csrf_token %} + {% ssi_csrf_token %}
@@ -33,17 +34,14 @@ {{ parent.title }}{% endfor %}
- {% if main_link %}{% endif %} - {{ book.title }} - {% if main_link %}{% endif %} + {% if main_link %}{% endif %}{{ book.title }}{% if main_link %}{% endif %}
{% if book.cover_thumb %} {% if main_link %}{% endif %} - Cover + Cover {% if main_link %}{% endif %} {% endif %} {% block cover-area-extra %}{% endblock %} @@ -91,7 +89,7 @@ {% endspaceless %}
- {% shelf_tags book %} + {% book_shelf_tags book.pk %}
  • @@ -134,3 +132,4 @@
+{% endspaceless %} \ No newline at end of file diff --git a/apps/catalogue/templates/catalogue/book_text.html b/apps/catalogue/templates/catalogue/book_text.html index 8dd154646..339cfc7b2 100644 --- a/apps/catalogue/templates/catalogue/book_text.html +++ b/apps/catalogue/templates/catalogue/book_text.html @@ -1,6 +1,6 @@ {% extends "catalogue/viewer_base.html" %} {% load i18n %} -{% load catalogue_tags %} +{% load catalogue_tags ssify %} {% load thumbnail %} @@ -49,7 +49,7 @@
-{{ book.html_file.read|safe }} +
- {% book_short book %} + {% ssi_include 'catalogue_book_short' pk=book.pk %}
{% endblock footer %} diff --git a/apps/catalogue/templates/catalogue/book_wide.html b/apps/catalogue/templates/catalogue/book_wide.html index 5ce66a3cb..b84acdb53 100644 --- a/apps/catalogue/templates/catalogue/book_wide.html +++ b/apps/catalogue/templates/catalogue/book_wide.html @@ -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 %} @@ -36,8 +37,15 @@ {% block right-column %}
-
- {% cite_promo book 1 %} +
+ {% 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 }}
diff --git a/apps/catalogue/templates/catalogue/daisy_list.html b/apps/catalogue/templates/catalogue/daisy_list.html index 4f570b2b6..65d9d6bcd 100644 --- a/apps/catalogue/templates/catalogue/daisy_list.html +++ b/apps/catalogue/templates/catalogue/daisy_list.html @@ -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 index d2c90e182..000000000 --- a/apps/catalogue/templates/catalogue/latest_blog_posts.html +++ /dev/null @@ -1,5 +0,0 @@ -
    -{% for post in posts %} -
  1. {{ post.title }}
  2. -{% endfor %} -
\ No newline at end of file diff --git a/apps/catalogue/templates/catalogue/menu.html b/apps/catalogue/templates/catalogue/menu.html index b6bc172a0..5ddbd3b23 100644 --- a/apps/catalogue/templates/catalogue/menu.html +++ b/apps/catalogue/templates/catalogue/menu.html @@ -1,3 +1,4 @@ +{% spaceless %} {% load i18n static %} @@ -7,8 +8,7 @@ + +{% endspaceless %} \ No newline at end of file diff --git a/apps/catalogue/templates/catalogue/related_books.html b/apps/catalogue/templates/catalogue/related_books.html index 219876e95..48fb2eeaf 100755 --- a/apps/catalogue/templates/catalogue/related_books.html +++ b/apps/catalogue/templates/catalogue/related_books.html @@ -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 diff --git a/apps/catalogue/templates/catalogue/search_multiple_hits.html b/apps/catalogue/templates/catalogue/search_multiple_hits.html index 667de9cc4..d1e1dedf7 100644 --- a/apps/catalogue/templates/catalogue/search_multiple_hits.html +++ b/apps/catalogue/templates/catalogue/search_multiple_hits.html @@ -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 @@
    {% for result in results.title %}
  1. - {% book_short result.book %} + {% ssi_include 'catalogue_book_short' pk=result.book.pk %}
  2. {% endfor %}
@@ -69,7 +72,7 @@
    - {% for author in results.author %}
  1. {% book_short author.book %}
  2. {% endfor %} + {% for author in results.author %}
  3. {% ssi_include 'catalogue_book_short' pk=author.book.pk %}
  4. {% endfor %}
{% endif %} @@ -82,7 +85,7 @@
    - {% for translator in results.translator %}
  1. {% book_short translator.book %}
  2. {% endfor %} + {% for translator in results.translator %}
  3. {% ssi_include 'catalogue_book_short' pk=translator.book.pk %}
  4. {% endfor %}
{% endif %} diff --git a/apps/catalogue/templates/catalogue/tag_list.html b/apps/catalogue/templates/catalogue/tag_list.html index 21430480c..e0fecc020 100644 --- a/apps/catalogue/templates/catalogue/tag_list.html +++ b/apps/catalogue/templates/catalogue/tag_list.html @@ -1,10 +1,12 @@ +{% spaceless %} + {% load i18n %} {% load catalogue_tags %} {% if one_tag %}

{% trans "See full category" %} {{ one_tag }}

{% else %} {% endif %} + +{% endspaceless %} \ No newline at end of file diff --git a/apps/catalogue/templates/catalogue/tagged_object_list.html b/apps/catalogue/templates/catalogue/tagged_object_list.html index f2c430845..9027027a7 100644 --- a/apps/catalogue/templates/catalogue/tagged_object_list.html +++ b/apps/catalogue/templates/catalogue/tagged_object_list.html @@ -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 %} @@ -102,7 +103,15 @@ {% 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 }}
{% if last_tag.gazeta_link or last_tag.wiki_link %} diff --git a/apps/catalogue/templates/catalogue/work-list.html b/apps/catalogue/templates/catalogue/work-list.html index fa33557d8..30265255a 100755 --- a/apps/catalogue/templates/catalogue/work-list.html +++ b/apps/catalogue/templates/catalogue/work-list.html @@ -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 %} +
    {% for item in object_list %}
  1. - {% 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 %}
  2. {% endfor %}
-{% endspaceless %} + {% paginate %} + +{% endspaceless %} diff --git a/apps/catalogue/templatetags/catalogue_tags.py b/apps/catalogue/templatetags/catalogue_tags.py index 2edae9b21..54fcf5ace 100644 --- a/apps/catalogue/templatetags/catalogue_tags.py +++ b/apps/catalogue/templatetags/catalogue_tags.py @@ -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 diff --git a/apps/catalogue/test_utils.py b/apps/catalogue/test_utils.py index dd11e934f..101a508b8 100644 --- a/apps/catalogue/test_utils.py +++ b/apps/catalogue/test_utils.py @@ -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): """ diff --git a/apps/catalogue/tests/__init__.py b/apps/catalogue/tests/__init__.py index 9c7a77c76..79aed52a7 100644 --- a/apps/catalogue/tests/__init__.py +++ b/apps/catalogue/tests/__init__.py @@ -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 * diff --git a/apps/catalogue/tests/tags.py b/apps/catalogue/tests/tags.py index f10780c0a..42ea6e245 100644 --- a/apps/catalogue/tests/tags.py +++ b/apps/catalogue/tests/tags.py @@ -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 index 000000000..1bdbdbd74 --- /dev/null +++ b/apps/catalogue/tests/test_visit.py @@ -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(''' + + + + + Theme + Test + + + + + '''), 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)) diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py index 7b1fb3d50..05f876607 100644 --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@ -18,6 +18,8 @@ urlpatterns = patterns('picture.views', url(r'^obraz/(?P%s).html$' % SLUG, 'picture_viewer', name='picture_viewer'), url(r'^obraz/(?P%s)/$' % SLUG, 'picture_detail'), + url(r'^p/(?P\d+)/short\.(?P.+)\.html', 'picture_short', name='picture_short'), + url(r'^pa/(?P\d+)/short\.(?P.+)\.html', 'picturearea_short', name='picture_area_short'), ) urlpatterns += patterns('', @@ -64,6 +66,15 @@ urlpatterns += patterns('catalogue.views', url(r'^lektura/(?P%s)/motyw/(?P[a-zA-Z0-9-]+)/$' % SLUG, 'book_fragments', name='book_fragments'), + # Includes. + url(r'^(?P[^/]+)\.json$', 'catalogue_json'), + url(r'^b/(?P\d+)/mini\.(?P.+)\.html', 'book_mini', name='catalogue_book_mini'), + url(r'^b/(?P\d+)/mini_nolink\.(?P.+)\.html', 'book_mini', {'with_link': False}, name='catalogue_book_mini_nolink'), + url(r'^b/(?P\d+)/short\.(?P.+)\.html', 'book_short', name='catalogue_book_short'), + url(r'^b/(?P\d+)/wide\.(?P.+)\.html', 'book_wide', name='catalogue_book_wide'), + url(r'^f/(?P\d+)/promo\.(?P.+)\.html', 'fragment_promo', name='catalogue_fragment_promo'), + url(r'^f/(?P\d+)/short\.(?P.+)\.html', 'fragment_short', name='catalogue_fragment_short'), + # This should be the last pattern. url(r'^(?P[a-zA-Z0-9-/]*)/$', 'tagged_object_list', name='tagged_object_list'), ) diff --git a/apps/catalogue/utils.py b/apps/catalogue/utils.py index 91e782efe..bcc5a0b33 100644 --- a/apps/catalogue/utils.py +++ b/apps/catalogue/utils.py @@ -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 diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index f47329a97..e5514f0db 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -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 index 000000000..09d541cd4 --- /dev/null +++ b/apps/chunks/migrations/0002_auto_20140911_1253.py @@ -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), + ), + ] diff --git a/apps/chunks/models.py b/apps/chunks/models.py index 5cdf8fea8..7f5410b24 100644 --- a/apps/chunks/models.py +++ b/apps/chunks/models.py @@ -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 index e69de29bb..000000000 diff --git a/apps/chunks/templatetags/chunks.py b/apps/chunks/templatetags/chunks.py deleted file mode 100644 index cc25df728..000000000 --- a/apps/chunks/templatetags/chunks.py +++ /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 index 000000000..005cadd02 --- /dev/null +++ b/apps/chunks/urls.py @@ -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.+)\.(?P.+)\.html$', 'chunk', name='chunk'), +) diff --git a/apps/chunks/views.py b/apps/chunks/views.py new file mode 100644 index 000000000..cbcf5bf6c --- /dev/null +++ b/apps/chunks/views.py @@ -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) diff --git a/apps/dictionary/models.py b/apps/dictionary/models.py index 7df3d75e3..2256acf71 100644 --- a/apps/dictionary/models.py +++ b/apps/dictionary/models.py @@ -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) diff --git a/apps/funding/management/commands/funding_notify.py b/apps/funding/management/commands/funding_notify.py index 305127629..1a30ffc90 100755 --- a/apps/funding/management/commands/funding_notify.py +++ b/apps/funding/management/commands/funding_notify.py @@ -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() diff --git a/apps/funding/models.py b/apps/funding/models.py index a9c3d8724..e36b73255 100644 --- a/apps/funding/models.py +++ b/apps/funding/models.py @@ -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/disable_notifications.html b/apps/funding/templates/funding/disable_notifications.html old mode 100755 new mode 100644 diff --git a/apps/funding/templates/funding/includes/funding.html b/apps/funding/templates/funding/includes/funding.html new file mode 100644 index 000000000..66804a427 --- /dev/null +++ b/apps/funding/templates/funding/includes/funding.html @@ -0,0 +1,53 @@ +{% spaceless %} + +{% load i18n %} +{% load time_tags %} + +{% if offer %} + + +{% if closeable %} +
{% trans "Help free the book!" %}
+{% 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 index 000000000..e13e1ec18 --- /dev/null +++ b/apps/funding/templates/funding/includes/fundings.html @@ -0,0 +1,28 @@ +{% spaceless %} + +{% load i18n %} +{% load pagination_tags %} + + + +{% for funding in fundings %} + + + + + +{% endfor %} +
{{ funding.payed_at.date }} + {% if funding.name %} + {{ funding.name }} + {% else %} + {% trans "Anonymous" %} + {% endif %} + {{ funding.amount }} zł  + {% for perk in funding.perks.all %} + {{ perk.name }}{% if not forloop.last %},{% endif %} + {% endfor %} +
+ +{% 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 index 000000000..5828ce41c --- /dev/null +++ b/apps/funding/templates/funding/includes/offer_status.html @@ -0,0 +1,31 @@ +{% load i18n %} + +{% if offer.is_current %} + {% if offer.is_win %} +

+ {% 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 %} +

+ {% else %} +

+ {% blocktrans with target=offer.target|floatformat %}W need {{target}} zł to digitize it, + compile it and publish for free in multiple formats.{% endblocktrans %} +

+

+ {% blocktrans with end=offer.end %}If we raise enought money before {{end}} we will + publish it and make it available for everyone.{% endblocktrans %} +

+ {% endif %} +{% else %} + {% if offer.is_win %} +

+ {% trans "Full amount was successfully raised!" %} +

+ {% else %} +

+ {% trans "The amount needed was not raised." %} +

+ {% 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 index 000000000..c94c9a801 --- /dev/null +++ b/apps/funding/templates/funding/includes/offer_status_more.html @@ -0,0 +1,26 @@ +{% load i18n %} + +{% if offer.is_current %} +

+ {% include "funding/snippets/any_remaining.html" %} +

+{% else %} +

{% trans "Fundraiser span" %}: {{ offer.start }} – {{ offer.end }}

+ {% if offer.is_win %} +

+ {% if offer.book %} + {% blocktrans with bu=offer.book.get_absolute_url bt=offer.book %}The book + {{ bt }} has been already published.{% endblocktrans %} + {% else %} + {% if offer.redakcja_url %} + {% blocktrans with r=offer.redakcja_url %}You can follow + the work on the Editorial Platform.{% endblocktrans %} + {% endif %} + {% endif %} +

+ {% endif %} + + {% if offer.remaining %} +

{% include "funding/snippets/any_remaining.html" %}

+ {% endif %} +{% endif %} diff --git a/apps/funding/templates/funding/offer_detail.html b/apps/funding/templates/funding/offer_detail.html index 217d5b882..70930c400 100644 --- a/apps/funding/templates/funding/offer_detail.html +++ b/apps/funding/templates/funding/offer_detail.html @@ -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 @@

{{ object }}

-{% funding object show_title=False %} +{% ssi_include 'funding_detail_bar' pk=object.pk %}
{% if object.cover %} @@ -34,9 +34,9 @@

{% trans "Help free the book!" %}

{{ object.description|safe }} - {% offer_status object %} + {% ssi_include 'funding_status' pk=object.pk %} 1%Możesz też przekazać
1% podatku na rozwój biblioteki. →
- {% offer_status_more object %} + {% ssi_include 'funding_status_more' pk=object.pk %}

{% trans "Learn more" %}.

@@ -71,31 +71,7 @@

{% trans "Supporters" %}:

-{% with object.funding_payed.all as fundings %} - - - {% autopaginate fundings 10 %} - {% for funding in fundings %} - - - - - - {% endfor %} -
{{ funding.payed_at.date }} - {% if funding.name %} - {{ funding.name }} - {% else %} - {% trans "Anonymous" %} - {% endif %} - {{ funding.amount }} zł  - {% for perk in funding.perks.all %} - {{ perk.name }}{% if not forloop.last %},{% endif %} - {% endfor %} -
- - {% paginate %} -{% endwith %} + {% ssi_include 'funding_fundings' pk=object.pk page=page %}
{% endblock %} diff --git a/apps/funding/templates/funding/snippets/any_remaining.html b/apps/funding/templates/funding/snippets/any_remaining.html old mode 100755 new mode 100644 diff --git a/apps/funding/templates/funding/tags/funding.html b/apps/funding/templates/funding/tags/funding.html deleted file mode 100755 index 46282518e..000000000 --- a/apps/funding/templates/funding/tags/funding.html +++ /dev/null @@ -1,48 +0,0 @@ -{% load i18n %} -{% load time_tags %} -{% if offer %} -{% spaceless %} - -{% if closeable %} -
{% trans "Help free the book!" %}
-{% 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 index 5828ce41c..000000000 --- a/apps/funding/templates/funding/tags/offer_status.html +++ /dev/null @@ -1,31 +0,0 @@ -{% load i18n %} - -{% if offer.is_current %} - {% if offer.is_win %} -

- {% 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 %} -

- {% else %} -

- {% blocktrans with target=offer.target|floatformat %}W need {{target}} zł to digitize it, - compile it and publish for free in multiple formats.{% endblocktrans %} -

-

- {% blocktrans with end=offer.end %}If we raise enought money before {{end}} we will - publish it and make it available for everyone.{% endblocktrans %} -

- {% endif %} -{% else %} - {% if offer.is_win %} -

- {% trans "Full amount was successfully raised!" %} -

- {% else %} -

- {% trans "The amount needed was not raised." %} -

- {% 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 index c94c9a801..000000000 --- a/apps/funding/templates/funding/tags/offer_status_more.html +++ /dev/null @@ -1,26 +0,0 @@ -{% load i18n %} - -{% if offer.is_current %} -

- {% include "funding/snippets/any_remaining.html" %} -

-{% else %} -

{% trans "Fundraiser span" %}: {{ offer.start }} – {{ offer.end }}

- {% if offer.is_win %} -

- {% if offer.book %} - {% blocktrans with bu=offer.book.get_absolute_url bt=offer.book %}The book - {{ bt }} has been already published.{% endblocktrans %} - {% else %} - {% if offer.redakcja_url %} - {% blocktrans with r=offer.redakcja_url %}You can follow - the work on the Editorial Platform.{% endblocktrans %} - {% endif %} - {% endif %} -

- {% endif %} - - {% if offer.remaining %} -

{% include "funding/snippets/any_remaining.html" %}

- {% endif %} -{% endif %} diff --git a/apps/funding/templatetags/funding_tags.py b/apps/funding/templatetags/funding_tags.py index 1a7c872a4..5dbeec6eb 100755 --- a/apps/funding/templatetags/funding_tags.py +++ b/apps/funding/templatetags/funding_tags.py @@ -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) diff --git a/apps/funding/urls.py b/apps/funding/urls.py index 2b8e5d672..4d806e84b 100644 --- a/apps/funding/urls.py +++ b/apps/funding/urls.py @@ -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\d+)/top-bar\.(?P.+)\.html$', 'top_bar', name='funding_top_bar'), + url(r'^o/(?P\d+)/detail-bar\.(?P.+)\.html$', 'detail_bar', name='funding_detail_bar'), + url(r'^o/(?P\d+)/list-bar\.(?P.+)\.html$', 'list_bar', name='funding_list_bar'), + url(r'^o/(?P\d+)/status\.(?P.+)\.html$', 'offer_status', name='funding_status'), + url(r'^o/(?P\d+)/status-more\.(?P.+)\.html$', 'offer_status_more', name='funding_status_more'), + url(r'^o/(?P\d+)/fundings/(?P\d+)\.(?P.+)\.html$', 'offer_fundings', name='funding_fundings'), ) diff --git a/apps/funding/views.py b/apps/funding/views.py index 9dd18ccd4..7c9adef46 100644 --- a/apps/funding/views.py +++ b/apps/funding/views.py @@ -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, + }) diff --git a/apps/infopages/templates/infopages/infopage.html b/apps/infopages/templates/infopages/infopage.html index db2d49b29..75dae0850 100755 --- a/apps/infopages/templates/infopages/infopage.html +++ b/apps/infopages/templates/infopages/infopage.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load i18n %} -{% load chunks %} {% block titleextra %}{{ page.title }}{% endblock %} diff --git a/apps/libraries/urls.py b/apps/libraries/urls.py index 93d605d2b..754b17e44 100644 --- a/apps/libraries/urls.py +++ b/apps/libraries/urls.py @@ -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[a-zA-Z0-9_-]+)$', 'catalog_view', name='libraries_catalog_view'), - url(r'^/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)$', 'library_view', name='libraries_library_view'), + url(r'^(?P[a-zA-Z0-9_-]+)$', 'catalog_view', name='libraries_catalog_view'), + url(r'^(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)$', 'library_view', name='libraries_library_view'), ) diff --git a/apps/newtagging/models.py b/apps/newtagging/models.py index ead442f31..694f5b868 100644 --- a/apps/newtagging/models.py +++ b/apps/newtagging/models.py @@ -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): """ diff --git a/apps/pdcounter/views.py b/apps/pdcounter/views.py index 3e2092e47..e5b4421ce 100644 --- a/apps/pdcounter/views.py +++ b/apps/pdcounter/views.py @@ -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(): diff --git a/apps/picture/models.py b/apps/picture/models.py index 5fc37228f..42a26c3c5 100644 --- a/apps/picture/models.py +++ b/apps/picture/models.py @@ -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 + ]) diff --git a/apps/picture/templates/picture/picture_detail.html b/apps/picture/templates/picture/picture_detail.html index a3cbf7d94..51bd6b98f 100644 --- a/apps/picture/templates/picture/picture_detail.html +++ b/apps/picture/templates/picture/picture_detail.html @@ -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 %} diff --git a/apps/picture/templates/picture/picture_list_thumb.html b/apps/picture/templates/picture/picture_list_thumb.html index 71cd7026a..4fce98a68 100644 --- a/apps/picture/templates/picture/picture_list_thumb.html +++ b/apps/picture/templates/picture/picture_list_thumb.html @@ -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 @@
{% block book_list_info %} - {% chunk 'picture-list' %} + {% ssi_include 'chunk' key='picture-list' %} {% endblock %}
@@ -30,7 +28,7 @@
    {% spaceless %} {% for picture in book_list %}
  1. - {% picture_short picture %} + {% ssi_include 'picture_short' pk=picture.pk %}
  2. {% endfor %} {% endspaceless %}
diff --git a/apps/picture/templates/picture/picture_wide.html b/apps/picture/templates/picture/picture_wide.html index 28cd34f65..cb05889d0 100644 --- a/apps/picture/templates/picture/picture_wide.html +++ b/apps/picture/templates/picture/picture_wide.html @@ -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 %} diff --git a/apps/picture/templatetags/picture_tags.py b/apps/picture/templatetags/picture_tags.py index 1767ce228..7e464fa02 100644 --- a/apps/picture/templatetags/picture_tags.py +++ b/apps/picture/templatetags/picture_tags.py @@ -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({ diff --git a/apps/picture/views.py b/apps/picture/views.py index 11b08bc3c..4bc2ab04b 100644 --- a/apps/picture/views.py +++ b/apps/picture/views.py @@ -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()) diff --git a/apps/polls/templates/polls/tags/poll.html b/apps/polls/templates/polls/tags/poll.html index 8566a9d0e..4c4d2b50d 100644 --- a/apps/polls/templates/polls/tags/poll.html +++ b/apps/polls/templates/polls/tags/poll.html @@ -1,4 +1,5 @@ {% load i18n %} +{% load ssi_csrf_token from ssify %} {% if poll %} {% if voted_already %} @@ -18,7 +19,7 @@ {% else %}

{{poll.question}}

-
{% csrf_token %} + {% ssi_csrf_token %} {{ form.vote }}
diff --git a/apps/polls/views.py b/apps/polls/views.py index ef5f50f5e..79540c66a 100644 --- a/apps/polls/views.py +++ b/apps/polls/views.py @@ -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 -from django.shortcuts import get_object_or_404, redirect, render_to_response -from django.core.urlresolvers import reverse +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 +from .models import Poll, PollItem from .forms import PollForm - -@require_http_methods(['GET', 'POST']) + +@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': + if request.method == 'POST': redirect_to = reverse('poll', args = [slug]) - form = PollForm(request.POST, poll = poll) - if form.is_valid(): - if not poll.voted(request.session): - try: + 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: - pass - else: + except PollItem.DoesNotExist: + pass + else: poll_item.vote(request.session) return redirect(redirect_to) elif request.method == 'GET': context = RequestContext(request) - context['poll'] = poll + context['poll'] = poll context['voted_already'] = poll.voted(request.session) return render_to_response('polls/poll.html', context) diff --git a/apps/search/templatetags/search_tags.py b/apps/search/templatetags/search_tags.py index 65d94270e..8dbad9dae 100644 --- a/apps/search/templatetags/search_tags.py +++ b/apps/search/templatetags/search_tags.py @@ -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", "
").replace('---', '—') 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 [] + } diff --git a/apps/social/models.py b/apps/social/models.py index df1b17599..890280774 100644 --- a/apps/social/models.py +++ b/apps/social/models.py @@ -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 index 000000000..4ee84d3c7 --- /dev/null +++ b/apps/social/templates/social/cite_info.html @@ -0,0 +1,18 @@ +{% spaceless %} + +{% if cite.image %} + {% if cite.image_link %}{% endif %} + {% if cite.image_title %} + {{ cite.image_title }}{% else %} + untitled{% endif %}{% if cite.image_link %}{% endif %}, + {% if cite.image_author %}{{ cite.image_author }},{% endif %} + {% if cite.image_license_link %}{% endif %} + {{ cite.image_license }} + {% if cite.image_license_link %}{% endif %} +{% else %} + books about architecture, + saikofish@Flickr, + CC BY NC SA. +{% endif %} + +{% endspaceless %} \ No newline at end of file diff --git a/apps/social/templates/social/cite_promo.html b/apps/social/templates/social/cite_promo.html index c3a73e89a..15786cf3b 100755 --- a/apps/social/templates/social/cite_promo.html +++ b/apps/social/templates/social/cite_promo.html @@ -1,6 +1,13 @@ +{% spaceless %} + {% load i18n %} +{% if main %} +
+{% endif %} + {% if cite %} + {% if cite.vip %}

{{ cite.vip }} {% trans "recommends" %}:

@@ -12,9 +19,12 @@

{{ cite.book.pretty_title }}

{% endif %}
-{% else %} - {% if fallback %} - {% load fragment_promo from catalogue_tags %} - {% fragment_promo ctx %} - {% endif %} + {% endif %} + + +{% if main %} +
+{% endif %} + +{% endspaceless %} \ No newline at end of file diff --git a/apps/social/templates/social/sets_form.html b/apps/social/templates/social/sets_form.html index 2f4a1d39b..5974a2a71 100755 --- a/apps/social/templates/social/sets_form.html +++ b/apps/social/templates/social/sets_form.html @@ -1,15 +1,16 @@ {% load i18n %} +{% load ssi_csrf_token from ssify %}

{{ title }}

-{% csrf_token %} +{% ssi_csrf_token %}
-{% csrf_token %} +{% ssi_csrf_token %}
    {{ form.as_ul }} diff --git a/apps/social/templates/social/shelf_tags.html b/apps/social/templates/social/shelf_tags.html index eabc961aa..2baa0f007 100755 --- a/apps/social/templates/social/shelf_tags.html +++ b/apps/social/templates/social/shelf_tags.html @@ -1,7 +1,9 @@ -{% if tags.exists %} +{% spaceless %} + -{% endif %} + +{% endspaceless %} \ No newline at end of file diff --git a/apps/social/templatetags/social_tags.py b/apps/social/templatetags/social_tags.py index bb1b4bca0..7065467b4 100755 --- a/apps/social/templatetags/social_tags.py +++ b/apps/social/templatetags/social_tags.py @@ -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)() diff --git a/apps/social/urls.py b/apps/social/urls.py index b25ab2509..3642d91da 100755 --- a/apps/social/urls.py +++ b/apps/social/urls.py @@ -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[a-z0-9-]+)/lubie/$', 'like_book', name='social_like_book'), url(r'^lektura/(?P[a-z0-9-]+)/nie_lubie/$', 'unlike_book', name='social_unlike_book'), - url(r'^lektura/(?P[a-z0-9-]+)/polki/$', ObjectSetsFormView(), name='social_book_sets'), + url(r'^lektura/(?P[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\d+)\.(?P.+)\.html$', 'cite', name='social_cite'), + url(r'^cite_main/(?P\d+)\.(?P.+)\.html$', 'cite', {'main': True}, name='social_cite_main'), + url(r'^cite_info/(?P\d+).html$', 'cite_info', name='social_cite_info'), + #~ url(r'^polki/(?P[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'), #~ url(r'^polki/(?P[a-zA-Z0-9-]+)/(?P%s)/usun$' % SLUG, 'remove_from_shelf', name='remove_from_shelf'), #~ url(r'^polki/$', 'user_shelves', name='user_shelves'), diff --git a/apps/social/utils.py b/apps/social/utils.py index c6a93537b..bf1c24229 100755 --- a/apps/social/utils.py +++ b/apps/social/utils.py @@ -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): diff --git a/apps/social/views.py b/apps/social/views.py index 446c5c4a9..49c9b708a 100644 --- a/apps/social/views.py +++ b/apps/social/views.py @@ -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, + }) diff --git a/apps/sponsors/models.py b/apps/sponsors/models.py index fd3c3a925..0565b9744 100644 --- a/apps/sponsors/models.py +++ b/apps/sponsors/models.py @@ -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 index e69de29bb..000000000 diff --git a/apps/sponsors/templatetags/sponsor_tags.py b/apps/sponsors/templatetags/sponsor_tags.py deleted file mode 100644 index 367012362..000000000 --- a/apps/sponsors/templatetags/sponsor_tags.py +++ /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 index 000000000..6da6186c3 --- /dev/null +++ b/apps/sponsors/urls.py @@ -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.+)\.html$', 'page', name='sponsor_page'), +) diff --git a/apps/sponsors/views.py b/apps/sponsors/views.py new file mode 100644 index 000000000..9a340895c --- /dev/null +++ b/apps/sponsors/views.py @@ -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) diff --git a/apps/suggest/templates/publishing_suggest.html b/apps/suggest/templates/publishing_suggest.html index 6efbb908d..2ac0ec245 100755 --- a/apps/suggest/templates/publishing_suggest.html +++ b/apps/suggest/templates/publishing_suggest.html @@ -1,10 +1,11 @@ {% load i18n %} {% load honeypot %} +{% load ssi_csrf_token from ssify %}

    {% trans "Didn't find a book? Make a suggestion." %}

    -{% csrf_token %} +{% ssi_csrf_token %} {% render_honeypot_field %}
    1. {{ form.contact.errors }} {{ form.contact }}
    2. diff --git a/apps/wolnelektury_core/__init__.py b/apps/wolnelektury_core/__init__.py index e69de29bb..56037a03e 100644 --- a/apps/wolnelektury_core/__init__.py +++ b/apps/wolnelektury_core/__init__.py @@ -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 index 000000000..996ccaec6 --- /dev/null +++ b/apps/wolnelektury_core/apps.py @@ -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 index e69de29bb..000000000 diff --git a/apps/wolnelektury_core/signals.py b/apps/wolnelektury_core/signals.py new file mode 100644 index 000000000..5eb7e8847 --- /dev/null +++ b/apps/wolnelektury_core/signals.py @@ -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() diff --git a/apps/wolnelektury_core/static/js/base.js b/apps/wolnelektury_core/static/js/base.js index 4b6c5d34e..f2a8e262a 100644 --- a/apps/wolnelektury_core/static/js/base.js +++ b/apps/wolnelektury_core/static/js/base.js @@ -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) { diff --git a/apps/wolnelektury_core/templates/auth/login.html b/apps/wolnelektury_core/templates/auth/login.html index 671f32a5a..756b8ad31 100644 --- a/apps/wolnelektury_core/templates/auth/login.html +++ b/apps/wolnelektury_core/templates/auth/login.html @@ -1,9 +1,11 @@ {% load i18n %} +{% load ssi_csrf_token from ssify %} +

      {{ title }}

      -{% csrf_token %} +{% ssi_csrf_token %}
        {{ form.as_ul }} diff --git a/apps/wolnelektury_core/templates/auth/login_register.html b/apps/wolnelektury_core/templates/auth/login_register.html index 64b0b8858..80d3d232a 100755 --- a/apps/wolnelektury_core/templates/auth/login_register.html +++ b/apps/wolnelektury_core/templates/auth/login_register.html @@ -1,6 +1,7 @@ {% extends "auth/login.html" %} {% load i18n %} {% load honeypot %} +{% load ssi_csrf_token from ssify %} {% block extra %} @@ -10,7 +11,7 @@ -{% csrf_token %} +{% ssi_csrf_token %} {% render_honeypot_field %}
          diff --git a/apps/wolnelektury_core/templates/latest_blog_posts.html b/apps/wolnelektury_core/templates/latest_blog_posts.html new file mode 100644 index 000000000..24d486ef1 --- /dev/null +++ b/apps/wolnelektury_core/templates/latest_blog_posts.html @@ -0,0 +1,9 @@ +{% spaceless %} + +
            +{% for post in posts %} +
          1. {{ post.title }}
          2. +{% endfor %} +
          + +{% endspaceless %} \ No newline at end of file diff --git a/apps/wolnelektury_core/templates/main_page.html b/apps/wolnelektury_core/templates/main_page.html index fd51cf371..118f3ac55 100755 --- a/apps/wolnelektury_core/templates/main_page.html +++ b/apps/wolnelektury_core/templates/main_page.html @@ -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 %} -
          - {% render_cite cite %} -
          +{% block body %}{% spaceless %} + + {% choose_cite as cite_pk %} + {{ cite_pk.if }} + {% ssi_include 'social_cite_main' pk=cite_pk %} + {{ cite_pk.endif }} - {% spaceless %}

          {% trans "What's new?" %}

          - {% chunk "promo" %} + {% ssi_include 'chunk' key='promo' %}

          {% trans "Recent publications" %}

          - {% 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 %}

          {% trans "News" %}

          - {# 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' %}
          -

          {% trans "Utilities" %}

          +

          {% trans "Utilities" %}

          • {% trans "Report a bug or suggestion" %}
          • -
          • - {% trans "Download the catalogue in PDF format." %} -
          • +
          • {% trans "Download the catalogue in PDF format." %}
          • {% trans "Missing a book?" %}
          • {% trans "Publishing plan" %}
          • @@ -70,45 +61,28 @@

            {% trans "Information" %}

            - {% endspaceless %} - -{% endblock %} - - -{% block add_footer %} -

            {% trans "Image used:" %} -{% if cite.image %} - {% if cite.image_link %}{% endif %} - {% if cite.image_title %} - {{ cite.image_title }}{% else %} - untitled{% endif %}{% if cite.image_link %}{% endif %}, - {% if cite.image_author %}{{ cite.image_author }},{% endif %} - {% if cite.image_license_link %}{% endif %} - {{ cite.image_license }} - {% if cite.image_license_link %}{% endif %} -{% else %} - books about architecture, - saikofish@Flickr, - CC BY NC SA. -{% endif %} -

            -{% endblock %} +{% endspaceless %}{% endblock %} + + +{% block add_footer %}{% spaceless %} +{{ cite_pk.if }} +

            {% trans "Image used:" %}

            + {% ssi_include 'social_cite_info' pk=cite_pk %} +

            +{{ cite_pk.endif }} +{% endspaceless %}{% endblock %} diff --git a/apps/wolnelektury_core/templates/pagination/pagination.html b/apps/wolnelektury_core/templates/pagination/pagination.html index 7489d4a55..432cf6954 100644 --- a/apps/wolnelektury_core/templates/pagination/pagination.html +++ b/apps/wolnelektury_core/templates/pagination/pagination.html @@ -2,25 +2,25 @@ {% if is_paginated %} {% endif %} diff --git a/apps/wolnelektury_core/templates/superbase.html b/apps/wolnelektury_core/templates/superbase.html index 46afe2875..c3259a30b 100644 --- a/apps/wolnelektury_core/templates/superbase.html +++ b/apps/wolnelektury_core/templates/superbase.html @@ -1,10 +1,12 @@ +{% spaceless %} - {% 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 %} @@ -13,16 +15,12 @@ - + {% block ogextra %}{% endblock %} - - {% block title %}{% trans "Wolne Lektury" %} :: - {% block titleextra %}{% endblock %}{% endblock %} + {% block title %}{% trans "Wolne Lektury" %} :: {% block titleextra %}{% endblock %}{% endblock %} - + {% compressed_css "main" %} {% block extrahead %} {% endblock %} @@ -30,45 +28,43 @@ {% 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 %} +

            - {% if user.is_authenticated %} - {% trans "Welcome" %}, - + {% user_username as user_username %} + {% user_is_staff as user_is_staff %} + {{ user_username.if }}{% trans "Welcome" %}, - {{ user.username }} + {{ user_username }} {% trans "Password" %}
            {% trans "E-mail" %}
            {% trans "Social accounts" %}
            -
            - | {% trans "My shelf" %} - {% if user.is_staff %} - | {% trans "Administration" %} - {% endif %} - | {% trans "Logout" %} - {% else %} - - {% trans "Sign in" %} - / - - {% trans "Register" %} - {% endif %} +
            | {% trans "My shelf" %} + {{ user_username.endif }} + {{ user_is_staff.if }} | {% trans "Administration" %} + {{ user_is_staff.endif }} + {{ user_username.if }} | {% trans "Logout" %} + {{ user_username.else }} + {% trans "Sign in" %} / {% trans "Register" %} + {{ user_username.endif }}

            - {% cache 60 tagline LANGUAGE_CODE %} {% url 'book_list' as b %} {% url 'infopage' 'prawa' as r %} {% count_books book_count %} @@ -77,7 +73,6 @@ {% plural %} {{c}} free readings you have right to {% endblocktrans %} - {% endcache %}

            @@ -97,17 +92,14 @@
            ⚐ - {% trans "Language versions" %} + {% trans "Language versions" %}
            {% for lang in LANGUAGES %} - {% csrf_token %} + {% ssi_csrf_token %} - + {% endfor %}
            @@ -126,9 +118,9 @@ @@ -147,10 +139,7 @@ - + {% compressed_js "base" %} {% tracking_code %} @@ -161,3 +150,4 @@ +{% endspaceless %} \ No newline at end of file diff --git a/apps/wolnelektury_core/templatetags/common_tags.py b/apps/wolnelektury_core/templatetags/common_tags.py index eddf9f13f..ab08a3ee6 100644 --- a/apps/wolnelektury_core/templatetags/common_tags.py +++ b/apps/wolnelektury_core/templatetags/common_tags.py @@ -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 diff --git a/apps/wolnelektury_core/views.py b/apps/wolnelektury_core/views.py index 80156bfd8..1ccd24a59 100644 --- a/apps/wolnelektury_core/views.py +++ b/apps/wolnelektury_core/views.py @@ -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}) diff --git a/requirements.txt b/requirements.txt index cdab158ea..4d2d4add8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/wolnelektury/settings/__init__.py b/wolnelektury/settings/__init__.py index 58fbc4225..4f6194096 100644 --- a/wolnelektury/settings/__init__.py +++ b/wolnelektury/settings/__init__.py @@ -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', diff --git a/wolnelektury/settings/cache.py b/wolnelektury/settings/cache.py index d4beab389..a9cc70f3b 100644 --- a/wolnelektury/settings/cache.py +++ b/wolnelektury/settings/cache.py @@ -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 diff --git a/wolnelektury/settings/custom.py b/wolnelektury/settings/custom.py index a0bab7a64..8830ebe6f 100644 --- a/wolnelektury/settings/custom.py +++ b/wolnelektury/settings/custom.py @@ -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" diff --git a/wolnelektury/urls.py b/wolnelektury/urls.py index abe872fa7..2dfd4d410 100644 --- a/wolnelektury/urls.py +++ b/wolnelektury/urls.py @@ -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')), - )