From: Radek Czajka Date: Fri, 13 Jan 2012 16:05:36 +0000 (+0100) Subject: Merge branch 'production' into pretty X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/73ce961f14509aabfa26536f847afd28111029c6?hp=325a7f36de2b6f38c53f6e1dc103807f66944364 Merge branch 'production' into pretty Conflicts: apps/catalogue/models.py wolnelektury/settings.py wolnelektury/templates/catalogue/main_page.html wolnelektury/urls.py --- diff --git a/apps/api/handlers.py b/apps/api/handlers.py index 586e921ca..fddff7407 100644 --- a/apps/api/handlers.py +++ b/apps/api/handlers.py @@ -7,6 +7,7 @@ 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 piston.handler import AnonymousBaseHandler, BaseHandler from piston.utils import rc @@ -531,6 +532,16 @@ class CatalogueHandler(BaseHandler): def changes(cls, request=None, since=0, until=None, book_fields=None, tag_fields=None, tag_categories=None): until = cls.until(until) + since = int(since) + + if not since: + cache = get_cache('api') + key = hash((book_fields, tag_fields, tag_categories, + tuple(sorted(request.GET.items())) + )) + value = cache.get(key) + if value is not None: + return value changes = { 'time_checked': timestamp(until) @@ -546,6 +557,10 @@ class CatalogueHandler(BaseHandler): if field == 'time_checked': continue changes.setdefault(field, {})[model] = changes_by_type[model][field] + + if not since: + cache.set(key, changes) + return changes diff --git a/apps/catalogue/admin.py b/apps/catalogue/admin.py index 7ef2aca6f..87ab727e1 100644 --- a/apps/catalogue/admin.py +++ b/apps/catalogue/admin.py @@ -6,7 +6,7 @@ from django.contrib import admin from django import forms from newtagging.admin import TaggableModelAdmin, TaggableModelForm -from catalogue.models import Tag, Book, Fragment, BookMedia +from catalogue.models import Tag, Book, Fragment, BookMedia, Collection class TagAdmin(admin.ModelAdmin): @@ -54,6 +54,11 @@ class FragmentAdmin(TaggableModelAdmin): ordering = ('book', 'anchor',) +class CollectionAdmin(admin.ModelAdmin): + prepopulated_fields = {'slug': ('title',)} + + admin.site.register(Tag, TagAdmin) admin.site.register(Book, BookAdmin) admin.site.register(Fragment, FragmentAdmin) +admin.site.register(Collection, CollectionAdmin) diff --git a/apps/catalogue/fixtures/collection-boy.json b/apps/catalogue/fixtures/collection-boy.json new file mode 100644 index 000000000..9a0e7781a --- /dev/null +++ b/apps/catalogue/fixtures/collection-boy.json @@ -0,0 +1,19 @@ +[ + { + "pk": "promo", + "model": "chunks.chunk", + "fields": { + "content": "

Biblioteczka Boya

\r\n\r\n

T\u0142umaczenia literatury francuskiej i nie tylko.

\r\n\r\n

Biblioteczka Boya \u21d2

", + "description": "boks promocyjny na g\u0142\u00f3wnej" + } + }, + { + "pk": "boy", + "model": "catalogue.collection", + "fields": { + "book_slugs": "http://www.wolnelektury.pl/katalog/lektura/piesn-o-rolandzie/\nhttp://www.wolnelektury.pl/katalog/lektura/wielki-testament/\nhttp://www.wolnelektury.pl/katalog/lektura/skapiec/\nhttp://www.wolnelektury.pl/katalog/lektura/mieszczanin-szlachcicem/\nhttp://www.wolnelektury.pl/katalog/lektura/kandyd/\nhttp://www.wolnelektury.pl/katalog/lektura/rozprawa-o-metodzie/\nhttp://www.wolnelektury.pl/katalog/lektura/listy-perskie/\nhttp://www.wolnelektury.pl/katalog/lektura/spowiedz-dzieciecia-wieku/\nhttp://www.wolnelektury.pl/katalog/lektura/ubu-krol/\nhttp://www.wolnelektury.pl/katalog/lektura/legenda-o-tristanie-i-izoldzie/\nhttp://www.wolnelektury.pl/katalog/lektura/boy-swietoszek/\nhttp://www.wolnelektury.pl/katalog/lektura/mizantrop/\nhttp://www.wolnelektury.pl/katalog/lektura/kubus-fatalista-i-jego-pan/\nhttp://www.wolnelektury.pl/katalog/lektura/mysli/\nhttp://www.wolnelektury.pl/katalog/lektura/o-duchu-praw/\nhttp://www.wolnelektury.pl/katalog/lektura/proby/\nhttp://www.wolnelektury.pl/katalog/lektura/w-strone-swanna/\nhttp://www.wolnelektury.pl/katalog/lektura/gargantua-i-pantagruel/", + "description": "", + "title": "Biblioteczka Boya" + } + } +] \ No newline at end of file diff --git a/apps/catalogue/migrations/0024_auto__add_collection.py b/apps/catalogue/migrations/0024_auto__add_collection.py new file mode 100644 index 000000000..e2e21007d --- /dev/null +++ b/apps/catalogue/migrations/0024_auto__add_collection.py @@ -0,0 +1,138 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'Collection' + db.create_table('catalogue_collection', ( + ('title', self.gf('django.db.models.fields.CharField')(max_length=120, db_index=True)), + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=120, primary_key=True, db_index=True)), + ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('book_slugs', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal('catalogue', ['Collection']) + + + def backwards(self, orm): + + # Deleting model 'Collection' + db.delete_table('catalogue_collection') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'catalogue.book': { + 'Meta': {'ordering': "('sort_key',)", 'object_name': 'Book'}, + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'common_slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'cover': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'epub_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': 'True'}), + 'mobi_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}), + 'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}) + }, + 'catalogue.bookmedia': { + 'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'}, + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'catalogue.collection': { + 'Meta': {'ordering': "('title',)", 'object_name': 'Collection'}, + 'book_slugs': ('django.db.models.fields.TextField', [], {}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'primary_key': 'True', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'}, + 'anchor': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fragments'", 'to': "orm['catalogue.Book']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'short_text': ('django.db.models.fields.TextField', [], {}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + 'catalogue.tag': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'}, + 'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}) + }, + 'catalogue.tagrelation': { + 'Meta': {'unique_together': "(('tag', 'content_type', 'object_id'),)", 'object_name': 'TagRelation', 'db_table': "'catalogue_tag_relation'"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['catalogue.Tag']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['catalogue'] diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py index fbae111ea..9a1e71ad0 100644 --- a/apps/catalogue/models.py +++ b/apps/catalogue/models.py @@ -1022,6 +1022,24 @@ class Fragment(models.Model): return mark_safe(short_html) +class Collection(models.Model): + """A collection of books, which might be defined before publishing them.""" + title = models.CharField(_('title'), max_length=120, db_index=True) + slug = models.SlugField(_('slug'), max_length=120, primary_key=True) + description = models.TextField(_('description'), null=True, blank=True) + + models.SlugField(_('slug'), max_length=120, unique=True, db_index=True) + book_slugs = models.TextField(_('book slugs')) + + class Meta: + ordering = ('title',) + verbose_name = _('collection') + verbose_name_plural = _('collections') + + def __unicode__(self): + return self.title + + ########### # # SIGNALS diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py index 4baf225a5..db044fc1c 100644 --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@ -23,7 +23,8 @@ urlpatterns = patterns('picture.views', url(r'^polki/$', 'user_shelves', name='user_shelves'), url(r'^polki/(?P[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'), url(r'^polki/(?P[a-zA-Z0-9-]+)\.zip$', 'download_shelf', name='download_shelf'), - url(r'^lektury/', 'book_list', name='book_list'), + url(r'^lektury/$', 'book_list', name='book_list'), + url(r'^lektury/(?P[a-zA-Z0-9-]+)/$', 'collection', name='collection'), url(r'^audiobooki/$', 'audiobook_list', name='audiobook_list'), url(r'^daisy/$', 'daisy_list', name='daisy_list'), url(r'^lektura/(?P%s)/polki/' % SLUG, 'book_sets', name='book_shelves'), diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index 34c9c1f1f..f57797da1 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -53,7 +53,8 @@ def catalogue(request): context_instance=RequestContext(request)) -def book_list(request, filter=None, template_name='catalogue/book_list.html'): +def book_list(request, filter=None, template_name='catalogue/book_list.html', + context=None): """ generates a listing of all books, optionally filtered with a test function """ books_by_author, orphans, books_by_parent = models.Book.book_list(filter) @@ -76,6 +77,17 @@ def daisy_list(request): template_name='catalogue/daisy_list.html') +def collection(request, slug): + coll = get_object_or_404(models.Collection, slug=slug) + slugs = coll.book_slugs.split() + # allow URIs + slugs = [slug.rstrip('/').rsplit('/', 1)[-1] if '/' in slug else slug + for slug in slugs] + return book_list(request, Q(slug__in=slugs), + template_name='catalogue/collection.html', + context={'collection': coll}) + + def differentiate_tags(request, tags, ambiguous_slugs): beginning = '/'.join(tag.url_chunk for tag in tags) unparsed = '/'.join(ambiguous_slugs[1:]) diff --git a/apps/chunks/models.py b/apps/chunks/models.py index cd9cf4e32..ef244eb0b 100644 --- a/apps/chunks/models.py +++ b/apps/chunks/models.py @@ -1,3 +1,4 @@ +from django.core.cache import cache from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -19,6 +20,14 @@ class Chunk(models.Model): def __unicode__(self): return self.key + def cache_key(self): + return 'chunk_' + self.key + + def save(self, *args, **kwargs): + ret = super(Chunk, self).save(*args, **kwargs) + cache.delete(self.cache_key()) + return ret + class Attachment(models.Model): key = models.CharField(_('key'), help_text=_('A unique name for this attachment'), primary_key=True, max_length=255) diff --git a/requirements.txt b/requirements.txt index 2b69d7f6f..81cc3936a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ --find-links=http://www.pythonware.com/products/pil/ # django -Django>=1.2.4,<1.3 +Django>=1.3,<1.4 South>=0.7 # migrations for django django-pagination>=1.0 django-rosetta>=0.5.3 diff --git a/wolnelektury/settings.py b/wolnelektury/settings.py index afd42e96c..a69b0508e 100644 --- a/wolnelektury/settings.py +++ b/wolnelektury/settings.py @@ -58,9 +58,9 @@ USE_I18N = True # Absolute path to the directory that holds media. # Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = path.join(PROJECT_DIR, '../media') -STATIC_ROOT = path.join(PROJECT_DIR, 'static') -SEARCH_INDEX = path.join(MEDIA_ROOT, 'search') +MEDIA_ROOT = path.join(PROJECT_DIR, '../media/') +STATIC_ROOT = path.join(PROJECT_DIR, 'static/') +SEARCH_INDEX = path.join(MEDIA_ROOT, 'search/') # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash if there is a path component (optional in other cases). @@ -77,13 +77,13 @@ ADMIN_MEDIA_PREFIX = '/admin-media/' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = [ - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', ] TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.core.context_processors.auth', + 'django.contrib.auth.context_processors.auth', 'django.core.context_processors.debug', 'django.core.context_processors.i18n', 'django.core.context_processors.media', @@ -158,9 +158,20 @@ INSTALLED_APPS = [ 'search', ] -#CACHE_BACKEND = 'locmem:///?max_entries=3000' -CACHE_BACKEND = 'memcached://127.0.0.1:11211/' -#CACHE_BACKEND = None +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': [ + '127.0.0.1:11211', + ] + }, + 'api': { + 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', + 'LOCATION': path.join(PROJECT_DIR, 'django_cache/'), + 'KEY_PREFIX': 'api', + 'TIMEOUT': 86400, + }, +} CACHE_MIDDLEWARE_ANONYMOUS_ONLY=True # CSS and JavaScript file groups diff --git a/wolnelektury/templates/catalogue/collection.html b/wolnelektury/templates/catalogue/collection.html new file mode 100755 index 000000000..4bb12c938 --- /dev/null +++ b/wolnelektury/templates/catalogue/collection.html @@ -0,0 +1,10 @@ +{% extends "catalogue/book_list.html" %} +{% load i18n %} + +{% block titleextra %}{{ context.collection.title }}{% endblock %} + +{% block book_list_header %}{{ context.collection.title }}{% endblock %} + +{% block book_list_info %} +{{ context.collection.description|safe }} +{% endblock %}