From: Radek Czajka Date: Wed, 31 Aug 2011 09:58:29 +0000 (+0200) Subject: Merge branch 'api' X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/3d1fb545dd1f49e0624d3adf20e5568b1c33d8ec?hp=c3b9c1f48873577c3a29f9c67e35a98d0a84330e Merge branch 'api' --- diff --git a/apps/api/handlers.py b/apps/api/handlers.py index 40121d6af..76de8c6a3 100644 --- a/apps/api/handlers.py +++ b/apps/api/handlers.py @@ -1,35 +1,252 @@ # -*- 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.shortcuts import get_object_or_404 -from django.contrib.auth.decorators import login_required, user_passes_test + +from datetime import datetime, timedelta from piston.handler import BaseHandler -from piston.utils import rc, validate -from catalogue.models import Book -from catalogue.forms import BookImportForm +from django.conf import settings + +from api.helpers import timestamp +from api.models import Deleted +from catalogue.models import Book, Tag -staff_required = user_passes_test(lambda user: user.is_staff) +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 -class BookHandler(BaseHandler): - model = Book - fields = ('slug', 'title') + @staticmethod + def until(t=None): + """ Returns time suitable for use as upper time boundary for check. + + Defaults to 'five minutes ago' to avoid issues with time between + change stamp set and model save. + Cuts the microsecond part to avoid issues with DBs where time has + more precision. - @staff_required - def read(self, request, slug=None): - if slug: - return get_object_or_404(Book, slug=slug) + """ + # set to five minutes ago, to avoid concurrency issues + if t is None: + t = datetime.now() - 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', + 'xml', 'epub', 'txt', 'pdf', 'html', + 'mp3', 'ogg', 'daisy', + '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: - return Book.objects.all() - - @staff_required - def create(self, request): - form = BookImportForm(request.POST, request.FILES) - if form.is_valid(): - form.save() - return rc.CREATED + fields = all_fields + + extra_info = book.get_extra_info_value() + + obj = {} + for field in fields: + + if field in ('xml', 'epub', 'txt', 'pdf', 'html'): + f = getattr(book, field+'_file') + if f: + obj[field] = { + 'url': f.url, + 'size': f.size, + } + + elif field in ('mp3', 'ogg', 'daisy'): + media = [] + for m in book.medias.filter(type=''): + files.append({ + 'url': m.file.get_absolute_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__in=('book', 'set'))] + + elif field == 'author': + obj[field] = ", ".join(t.name for t in book.tags.filter(category='author')) + + 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)) + 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): + 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): + 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: - return rc.BAD_REQUEST + 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])] + + 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)) + 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): + # only serve non-empty tags + if tag.get_count(): + tag_d = cls.tag_dict(tag, fields) + updated.append(tag_d) + elif tag.created_at < since: + 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): + 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) + + 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] + return changes + + +class BookChangesHandler(CatalogueHandler): + allowed_methods = ('GET',) + + def read(self, request, since): + return self.book_changes(request, since) + + +class TagChangesHandler(CatalogueHandler): + allowed_methods = ('GET',) + + def read(self, request, since): + return self.tag_changes(request, since) + + +class ChangesHandler(CatalogueHandler): + allowed_methods = ('GET',) + def read(self, request, since): + return self.changes(request, since) diff --git a/apps/api/helpers.py b/apps/api/helpers.py new file mode 100644 index 000000000..aa22465fc --- /dev/null +++ b/apps/api/helpers.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from time import mktime + +def timestamp(dtime): + "converts a datetime.datetime object to a timestamp int" + return int(mktime(dtime.timetuple())) + diff --git a/apps/api/management/__init__.py b/apps/api/management/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/apps/api/management/commands/__init__.py b/apps/api/management/commands/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/apps/api/management/commands/mobileinit.py b/apps/api/management/commands/mobileinit.py new file mode 100755 index 000000000..cfb9fb433 --- /dev/null +++ b/apps/api/management/commands/mobileinit.py @@ -0,0 +1,162 @@ +# -*- 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 datetime import datetime +import os +import os.path +import re +import sqlite3 +from django.core.management.base import BaseCommand + +from api.helpers import timestamp +from api.settings import MOBILE_INIT_DB +from catalogue.models import Book, Tag + + +class Command(BaseCommand): + help = 'Creates an initial SQLite file for the mobile app.' + + def handle(self, **options): + # those should be versioned + last_checked = timestamp(datetime.now()) + db = init_db(last_checked) + for b in Book.objects.all(): + add_book(db, b) + for t in Tag.objects.exclude(category__in=('book', 'set', 'theme')): + # only add non-empty tags + if t.get_count(): + add_tag(db, t) + db.commit() + db.close() + current(last_checked) + + +def pretty_size(size): + """ Turns size in bytes into a prettier string. + + >>> pretty_size(100000) + '97 KiB' + """ + if not size: + return None + units = ['B', 'KiB', 'MiB', 'GiB'] + size = float(size) + unit = units.pop(0) + while size > 1000 and units: + size /= 1024 + unit = units.pop(0) + if size < 10: + return "%.1f %s" % (size, unit) + return "%d %s" % (size, unit) + + + if not isinstance(value, unicode): + value = unicode(value, 'utf-8') + + # try to replace chars + value = re.sub('[^a-zA-Z0-9\\s\\-]{1}', replace_char, value) + value = value.lower() + value = re.sub(r'[^a-z0-9{|}]+', '~', value) + + return value.encode('ascii', 'ignore') + + + +def init_db(last_checked): + if not os.path.isdir(MOBILE_INIT_DB): + os.makedirs(MOBILE_INIT_DB) + db = sqlite3.connect(os.path.join(MOBILE_INIT_DB, 'initial.db-%d' % last_checked)) + + schema = """ +CREATE TABLE book ( + id INTEGER PRIMARY KEY, + title VARCHAR, + html_file VARCHAR, + html_file_size INTEGER, + parent INTEGER, + parent_number INTEGER, + + sort_key VARCHAR, + pretty_size VARCHAR, + authors VARCHAR, + _local BOOLEAN +); +CREATE INDEX IF NOT EXISTS book_title_index ON book (sort_key); +CREATE INDEX IF NOT EXISTS book_title_index ON book (title); +CREATE INDEX IF NOT EXISTS book_parent_index ON book (parent); + +CREATE TABLE tag ( + id INTEGER PRIMARY KEY, + name VARCHAR, + category VARCHAR, + sort_key VARCHAR, + books VARCHAR); +CREATE INDEX IF NOT EXISTS tag_name_index ON tag (name); +CREATE INDEX IF NOT EXISTS tag_category_index ON tag (category); +CREATE INDEX IF NOT EXISTS tag_sort_key_index ON tag (sort_key); + +CREATE TABLE state (last_checked INTEGER); +""" + + db.executescript(schema) + db.execute("INSERT INTO state VALUES (:last_checked)", locals()) + return db + + +def current(last_checked): + target = os.path.join(MOBILE_INIT_DB, 'initial.db') + os.unlink(target) + os.symlink( + 'initial.db-%d' % last_checked, + target, + ) + + + +book_sql = """ + INSERT INTO book + (id, title, html_file, html_file_size, parent, parent_number, sort_key, pretty_size, authors) + VALUES + (:id, :title, :html_file, :html_file_size, :parent, :parent_number, :sort_key, :size_str, :authors); +""" +book_tag_sql = "INSERT INTO book_tag (book, tag) VALUES (:book, :tag);" +tag_sql = """ + INSERT INTO tag + (id, category, name, sort_key, books) + VALUES + (:id, :category, :name, :sort_key, :book_ids); +""" +categories = {'author': 'autor', + 'epoch': 'epoka', + 'genre': 'gatunek', + 'kind': 'rodzaj', + 'theme': 'motyw' + } + + +def add_book(db, book): + id = book.id + title = book.title + if book.html_file: + html_file = book.html_file.url + html_file_size = book.html_file.size + else: + html_file = html_file_size = None + parent = book.parent + parent_number = book.parent_number + sort_key = book.sort_key + size_str = pretty_size(html_file_size) + authors = ", ".join(t.name for t in book.tags.filter(category='author')) + db.execute(book_sql, locals()) + + +def add_tag(db, tag): + id = tag.id + category = categories[tag.category] + name = tag.name + sort_key = tag.sort_key + + books = Book.tagged_top_level([tag]) + book_ids = ','.join(str(b.id) for b in books) + db.execute(tag_sql, locals()) diff --git a/apps/api/migrations/0001_initial.py b/apps/api/migrations/0001_initial.py new file mode 100644 index 000000000..a5677524d --- /dev/null +++ b/apps/api/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# 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 'Deleted' + db.create_table('api_deleted', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('object_id', self.gf('django.db.models.fields.IntegerField')()), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), + ('created_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), + ('deleted_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)), + )) + db.send_create_signal('api', ['Deleted']) + + # Adding unique constraint on 'Deleted', fields ['content_type', 'object_id'] + db.create_unique('api_deleted', ['content_type_id', 'object_id']) + + + def backwards(self, orm): + + # Removing unique constraint on 'Deleted', fields ['content_type', 'object_id'] + db.delete_unique('api_deleted', ['content_type_id', 'object_id']) + + # Deleting model 'Deleted' + db.delete_table('api_deleted') + + + models = { + 'api.deleted': { + 'Meta': {'unique_together': "(('content_type', 'object_id'),)", 'object_name': 'Deleted'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {}) + }, + '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 = ['api'] diff --git a/apps/api/migrations/0002_auto__add_field_deleted_category.py b/apps/api/migrations/0002_auto__add_field_deleted_category.py new file mode 100644 index 000000000..5480e5222 --- /dev/null +++ b/apps/api/migrations/0002_auto__add_field_deleted_category.py @@ -0,0 +1,40 @@ +# 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 field 'Deleted.category' + db.add_column('api_deleted', 'category', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=64, null=True, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Deleted.category' + db.delete_column('api_deleted', 'category') + + + models = { + 'api.deleted': { + 'Meta': {'unique_together': "(('content_type', 'object_id'),)", 'object_name': 'Deleted'}, + 'category': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {}) + }, + '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 = ['api'] diff --git a/apps/api/migrations/__init__.py b/apps/api/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/api/models.py b/apps/api/models.py new file mode 100644 index 000000000..2cdc74a4b --- /dev/null +++ b/apps/api/models.py @@ -0,0 +1,37 @@ +# -*- 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.db import models +from django.db.models.signals import pre_delete + +from catalogue.models import Book, Tag + + +class Deleted(models.Model): + object_id = models.IntegerField() + content_type = models.ForeignKey(ContentType) + category = models.CharField(max_length=64, null=True, blank=True, db_index=True) + created_at = models.DateTimeField(editable=False, db_index=True) + deleted_at = models.DateTimeField(auto_now_add=True, db_index=True) + + class Meta: + unique_together = (('content_type', 'object_id'),) + + +def _pre_delete_handler(sender, instance, **kwargs): + """ save deleted objects for change history purposes """ + + if sender in (Book, Tag): + if sender == Tag: + if instance.category in ('book', 'set'): + return + else: + category = instance.category + else: + category = None + content_type = ContentType.objects.get_for_model(sender) + Deleted.objects.create(content_type=content_type, object_id=instance.id, + created_at=instance.created_at, category=category) +pre_delete.connect(_pre_delete_handler) diff --git a/apps/api/settings.py b/apps/api/settings.py new file mode 100755 index 000000000..2e02e412a --- /dev/null +++ b/apps/api/settings.py @@ -0,0 +1,8 @@ +import os.path +from django.conf import settings + + +try: + MOBILE_INIT_DB = settings.API_MOBILE_INIT_DB +except AttributeError: + MOBILE_INIT_DB = os.path.abspath(os.path.join(settings.MEDIA_ROOT, 'api/mobile/initial/')) diff --git a/apps/api/tests.py b/apps/api/tests.py new file mode 100644 index 000000000..41df4c6b0 --- /dev/null +++ b/apps/api/tests.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime + +from django.test import TestCase +from django.utils import simplejson as json +from django.conf import settings + +from api.helpers import timestamp +from catalogue.models import Book, Tag + + +class ApiTest(TestCase): + + def setUp(self): + self.old_api_wait = settings.API_WAIT + settings.API_WAIT = -1 + + def tearDown(self): + settings.API_WAIT = self.old_api_wait + + +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 + print 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.') diff --git a/apps/api/urls.py b/apps/api/urls.py index 8b1b9b0cb..536454f2f 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -1,17 +1,20 @@ # -*- coding: utf-8 -*- from django.conf.urls.defaults import * from piston.resource import Resource -from piston.authentication import HttpBasicAuthentication -from api.handlers import BookHandler +from api import handlers -auth = HttpBasicAuthentication(realm='My sample API') -book_resource = Resource(handler=BookHandler, authentication=auth) - +book_changes_resource = Resource(handler=handlers.BookChangesHandler) +tag_changes_resource = Resource(handler=handlers.TagChangesHandler) +changes_resource = Resource(handler=handlers.ChangesHandler) urlpatterns = patterns('', - url(r'^books/(?P[^/]+)\.(?Pxml|json|yaml)$', book_resource), - url(r'^books\.(?Pxml|json|yaml)$', book_resource), -) + url(r'^book_changes/(?P\d*?)\.(?Pxml|json|yaml)$', book_changes_resource), + url(r'^tag_changes/(?P\d*?)\.(?Pxml|json|yaml)$', tag_changes_resource), + url(r'^changes/(?P\d*?)\.(?Pxml|json|yaml)$', changes_resource), + + url(r'book/(?P\d*?)/info\.html$', 'catalogue.views.book_info'), + url(r'tag/(?P\d*?)/info\.html$', 'catalogue.views.tag_info'), +) diff --git a/apps/catalogue/migrations/0007_auto__add_field_book_changed_at__add_field_tag_created_at__add_field_t.py b/apps/catalogue/migrations/0007_auto__add_field_book_changed_at__add_field_tag_created_at__add_field_t.py new file mode 100644 index 000000000..b339c0586 --- /dev/null +++ b/apps/catalogue/migrations/0007_auto__add_field_book_changed_at__add_field_tag_created_at__add_field_t.py @@ -0,0 +1,166 @@ +# 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 field 'Book.changed_at' + db.add_column('catalogue_book', 'changed_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, default=datetime.datetime(2011, 2, 25, 15, 19, 36, 525463), db_index=True, blank=True), keep_default=False) + + # Adding index on 'Book', fields ['created_at'] + db.create_index('catalogue_book', ['created_at']) + + # Adding field 'Tag.created_at' + db.add_column('catalogue_tag', 'created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, default=datetime.datetime(2011, 2, 25, 15, 19, 42, 921525), db_index=True, blank=True), keep_default=False) + + # Adding field 'Tag.changed_at' + db.add_column('catalogue_tag', 'changed_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, default=datetime.datetime(2011, 2, 25, 15, 19, 45, 697471), db_index=True, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Removing index on 'Book', fields ['created_at'] + db.delete_index('catalogue_book', ['created_at']) + + # Deleting field 'Book.changed_at' + db.delete_column('catalogue_book', 'changed_at') + + # Deleting field 'Tag.created_at' + db.delete_column('catalogue_tag', 'created_at') + + # Deleting field 'Tag.changed_at' + db.delete_column('catalogue_tag', 'changed_at') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'}), + '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', [], {'max_length': '30', 'unique': 'True'}) + }, + 'catalogue.book': { + 'Meta': {'ordering': "('title',)", 'object_name': 'Book'}, + '_short_html': ('django.db.models.fields.TextField', [], {}), + '_short_html_de': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_en': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_es': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_fr': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_lt': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_pl': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_ru': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_uk': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_tag_counter': ('catalogue.fields.JSONField', [], {'null': 'True'}), + '_theme_counter': ('catalogue.fields.JSONField', [], {'null': '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'}), + 'epub_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'extra_info': ('catalogue.fields.JSONField', [], {}), + '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'}), + 'medias': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.BookMedia']", 'symmetrical': 'False', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'children'", 'blank': 'True', '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', [], {'max_length': '120', 'unique': 'True', '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'}, + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'catalogue.filerecord': { + 'Meta': {'ordering': "('-time', '-slug', '-type')", 'object_name': 'FileRecord'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'sha1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20', 'db_index': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'}, + '_short_html': ('django.db.models.fields.TextField', [], {}), + '_short_html_de': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_en': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_es': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_fr': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_lt': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_pl': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_ru': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_uk': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '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'}), + 'main_page': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': '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/migrations/0008_auto__add_field_book_sort_key.py b/apps/catalogue/migrations/0008_auto__add_field_book_sort_key.py new file mode 100644 index 000000000..22ddb52ca --- /dev/null +++ b/apps/catalogue/migrations/0008_auto__add_field_book_sort_key.py @@ -0,0 +1,149 @@ +# 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 field 'Book.sort_key' + db.add_column('catalogue_book', 'sort_key', self.gf('django.db.models.fields.CharField')(default='', max_length=120, db_index=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Book.sort_key' + db.delete_column('catalogue_book', 'sort_key') + + + 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'}, + '_short_html': ('django.db.models.fields.TextField', [], {}), + '_short_html_de': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_en': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_es': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_fr': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_lt': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_pl': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_ru': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_uk': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_tag_counter': ('catalogue.fields.JSONField', [], {'null': 'True'}), + '_theme_counter': ('catalogue.fields.JSONField', [], {'null': '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'}), + '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'}), + 'medias': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.BookMedia']", 'symmetrical': 'False', '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'}, + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'catalogue.filerecord': { + 'Meta': {'ordering': "('-time', '-slug', '-type')", 'object_name': 'FileRecord'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'sha1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20', 'db_index': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'}, + '_short_html': ('django.db.models.fields.TextField', [], {}), + '_short_html_de': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_en': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_es': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_fr': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_lt': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_pl': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_ru': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_uk': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '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'}), + 'main_page': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': '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/migrations/0009_sortify.py b/apps/catalogue/migrations/0009_sortify.py new file mode 100644 index 000000000..157fac6e2 --- /dev/null +++ b/apps/catalogue/migrations/0009_sortify.py @@ -0,0 +1,157 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +from sortify import sortify + + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + + for b in orm.Book.objects.all(): + b.sort_key = sortify(b.title) + b.save() + + for t in orm.Tag.objects.all(): + t.sort_key = sortify(t.sort_key) + t.save() + + + def backwards(self, orm): + "Write your backwards methods here." + pass + + + 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'}, + '_short_html': ('django.db.models.fields.TextField', [], {}), + '_short_html_de': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_en': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_es': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_fr': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_lt': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_pl': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_ru': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_uk': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_tag_counter': ('catalogue.fields.JSONField', [], {'null': 'True'}), + '_theme_counter': ('catalogue.fields.JSONField', [], {'null': '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'}), + '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'}), + 'medias': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.BookMedia']", 'symmetrical': 'False', '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'}, + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'catalogue.filerecord': { + 'Meta': {'ordering': "('-time', '-slug', '-type')", 'object_name': 'FileRecord'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'sha1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20', 'db_index': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'}, + '_short_html': ('django.db.models.fields.TextField', [], {}), + '_short_html_de': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_en': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_es': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_fr': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_lt': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_pl': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_ru': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '_short_html_uk': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + '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'}), + 'main_page': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': '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 cda5ba532..fd5dc987a 100644 --- a/apps/catalogue/models.py +++ b/apps/catalogue/models.py @@ -2,6 +2,8 @@ # 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 django.db import models from django.db.models import permalink, Q from django.utils.translation import ugettext_lazy as _ @@ -24,6 +26,7 @@ from librarian import dcparser, html, epub, NoDublinCore import mutagen from mutagen import id3 from slughifi import slughifi +from sortify import sortify TAG_CATEGORIES = ( @@ -66,6 +69,9 @@ class Tag(TagBase): gazeta_link = models.CharField(blank=True, max_length=240) wiki_link = models.CharField(blank=True, max_length=240) + 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) + class UrlDeprecationWarning(DeprecationWarning): pass @@ -271,12 +277,14 @@ class BookMedia(models.Model): class Book(models.Model): title = models.CharField(_('title'), max_length=120) + sort_key = models.CharField(_('sort_key'), max_length=120, db_index=True, editable=False) slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True) description = models.TextField(_('description'), blank=True) - created_at = models.DateTimeField(_('creation date'), auto_now_add=True) + created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True) + changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True) _short_html = models.TextField(_('short HTML'), editable=False) parent_number = models.IntegerField(_('parent number'), default=0) - extra_info = JSONField(_('extra information')) + extra_info = JSONField(_('extra information'), default='{}') gazeta_link = models.CharField(blank=True, max_length=240) wiki_link = models.CharField(blank=True, max_length=240) # files generated during publication @@ -298,7 +306,7 @@ class Book(models.Model): pass class Meta: - ordering = ('title',) + ordering = ('sort_key',) verbose_name = _('book') verbose_name_plural = _('books') @@ -306,6 +314,8 @@ class Book(models.Model): return self.title def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs): + self.sort_key = sortify(self.title) + if reset_short_html: # Reset _short_html during save update = {} @@ -605,7 +615,7 @@ class Book(models.Model): tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category) if created: tag.name = tag_name - tag.sort_key = tag_sort_key.lower() + tag.sort_key = sortify(tag_sort_key.lower()) tag.save() book_tags.append(tag) @@ -751,6 +761,24 @@ class Book(models.Model): return ', '.join(names) + @classmethod + def tagged_top_level(cls, tags): + """ Returns top-level books tagged with `tags'. + + It only returns those books which don't have ancestors which are + also tagged with those tags. + + """ + # get relevant books and their tags + objects = cls.tagged.with_all(tags) + # eliminate descendants + l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in objects]) + descendants_keys = [book.pk for book in cls.tagged.with_any(l_tags)] + if descendants_keys: + objects = objects.exclude(pk__in=descendants_keys) + + return objects + class Fragment(models.Model): text = models.TextField() @@ -806,7 +834,8 @@ class FileRecord(models.Model): def _tags_updated_handler(sender, affected_tags, **kwargs): # reset tag global counter - Tag.objects.filter(pk__in=[tag.pk for tag in affected_tags]).update(book_count=None) + # we want Tag.changed_at updated for API to know the tag was touched + Tag.objects.filter(pk__in=[tag.pk for tag in affected_tags]).update(book_count=None, changed_at=datetime.now()) # if book tags changed, reset book tag counter if isinstance(sender, Book) and \ diff --git a/apps/catalogue/templatetags/catalogue_tags.py b/apps/catalogue/templatetags/catalogue_tags.py index 5cfa5fc0c..c931e4880 100644 --- a/apps/catalogue/templatetags/catalogue_tags.py +++ b/apps/catalogue/templatetags/catalogue_tags.py @@ -275,3 +275,7 @@ def folded_tag_list(tags, title='', choices=None): some_tags_hidden = True return locals() + +@register.inclusion_tag('catalogue/book_info.html') +def book_info(book): + return locals() diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py index e864cdba3..7592ca408 100644 --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@ -32,6 +32,7 @@ urlpatterns = patterns('catalogue.views', url(r'^lektura/(?P[a-zA-Z0-9-]+)/$', 'book_detail', name='book_detail'), url(r'^lektura/(?P[a-zA-Z0-9-]+)/motyw/(?P[a-zA-Z0-9-]+)/$', 'book_fragments', name='book_fragments'), + url(r'^(?P[a-zA-Z0-9-/]*)/$', 'tagged_object_list', name='tagged_object_list'), url(r'^audiobooki/(?Pmp3|ogg|daisy|all).xml$', AudiobookFeed(), name='audiobook_feed'), diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index f585aeaa1..d17c349c5 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -28,6 +28,7 @@ from django.utils.functional import Promise from django.utils.encoding import force_unicode from django.utils.http import urlquote_plus from django.views.decorators import cache +from django.utils import translation from django.utils.translation import ugettext as _ from django.views.generic.list_detail import object_list @@ -83,7 +84,7 @@ def book_list(request, filter=None, template_name='catalogue/book_list.html'): form = forms.SearchForm() books_by_parent = {} - books = models.Book.objects.all().order_by('parent_number', 'title').only('title', 'parent', 'slug') + books = models.Book.objects.all().order_by('parent_number', 'sort_key').only('title', 'parent', 'slug') if filter: books = books.filter(filter).distinct() book_ids = set((book.pk for book in books)) @@ -217,14 +218,10 @@ def tagged_object_list(request, tags=''): objects = fragments else: - # get relevant books and their tags - objects = models.Book.tagged.with_all(tags) - if not shelf_is_set: - # eliminate descendants - l_tags = models.Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in objects]) - descendants_keys = [book.pk for book in models.Book.tagged.with_any(l_tags)] - if descendants_keys: - objects = objects.exclude(pk__in=descendants_keys) + if shelf_is_set: + objects = models.Book.tagged.with_all(tags) + else: + objects = models.Book.tagged_top_level(tags) # get related tags from `tag_counter` and `theme_counter` related_counts = {} @@ -256,7 +253,6 @@ def tagged_object_list(request, tags=''): 'only_author': only_author, 'only_my_shelf': only_my_shelf, 'formats_form': forms.DownloadFormatsForm(), - 'tags': tags, } ) @@ -282,7 +278,7 @@ def book_detail(request, slug): book_tag = book.book_tag() tags = list(book.tags.filter(~Q(category='set'))) categories = split_tags(tags) - book_children = book.children.all().order_by('parent_number', 'title') + book_children = book.children.all().order_by('parent_number', 'sort_key') _book = book parents = [] @@ -803,3 +799,18 @@ def xmls(request): temp.seek(0) response.write(temp.read()) return response + + + +# info views for API + +def book_info(request, id, lang='pl'): + book = get_object_or_404(models.Book, id=id) + # set language by hand + translation.activate(lang) + return render_to_response('catalogue/book_info.html', locals(), + context_instance=RequestContext(request)) + +def tag_info(request, id): + tag = get_object_or_404(models.Tag, id=id) + return HttpResponse(tag.description) diff --git a/lib/sortify.py b/lib/sortify.py new file mode 100755 index 000000000..c64668b26 --- /dev/null +++ b/lib/sortify.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +import re +from slughifi import char_map + + +# Specifies diacritics order. +# Default order is zero, max is 9 +char_order = { + u'ż': 1, u'Ż': 1, +} + + +def replace_char(m): + char = m.group() + if char_map.has_key(char): + order = char_order.get(char, 0) + return "%s~%d" % (char_map[char], order) + else: + return char + + +def sortify(value): + """ + Turns Unicode into ASCII-sortable str + + Examples : + + >>> sortify('aa') < sortify('a a') < sortify('ą') < sortify('b') + True + + >>> sortify('ź') < sortify('ż') + True + + """ + + if not isinstance(value, unicode): + value = unicode(value, 'utf-8') + + # try to replace chars + value = re.sub('[^a-zA-Z0-9\\s\\-]{1}', replace_char, value) + value = value.lower() + value = re.sub(r'[^a-z0-9~]+', '|', value) + + return value.encode('ascii', 'ignore') diff --git a/wolnelektury/settings.py b/wolnelektury/settings.py index a47276378..3c5faf8a2 100644 --- a/wolnelektury/settings.py +++ b/wolnelektury/settings.py @@ -206,6 +206,10 @@ THUMBNAIL_PROCESSORS = ( TRANSLATION_REGISTRY = "wolnelektury.translation" + +# seconds until a changes appears in the changes api +API_WAIT = 100 + # limit number of filtering tags MAX_TAG_LIST = 6 diff --git a/wolnelektury/templates/catalogue/book_info.html b/wolnelektury/templates/catalogue/book_info.html new file mode 100755 index 000000000..670679e28 --- /dev/null +++ b/wolnelektury/templates/catalogue/book_info.html @@ -0,0 +1,30 @@ +{% load i18n %} +{% load catalogue_tags %} + +

+ {% if book.get_extra_info_value.license %} + {% trans "This work is licensed under:" %} + {{ book.get_extra_info_value.license_description }} + {% else %} + {% blocktrans %}This work isn't covered by copyright and is part of the + public domain, which means it can be freely used, published and + distributed. If there are any additional copyrighted materials + provided with this work (such as annotations, motifs etc.), those + materials are licensed under the + Creative Commons Attribution-ShareAlike 3.0 + license.{% endblocktrans %} + {% endif %} +

+ +{% if book.get_extra_info_value.source_name %} +

{% trans "Text prepared based on:" %} {{ book.get_extra_info_value.source_name }}

+{% endif %} + +{% if book.get_extra_info_value.description %} +

{{ book.get_extra_info_value.description }}

+{% endif %} + +{% if book.get_extra_info_value.editor or book.get_extra_info_value.technical_editor %} +

{% trans "Edited and annotated by:" %} + {% all_editors book.get_extra_info_value %}.

+{% endif %} diff --git a/wolnelektury/templates/catalogue/book_text.html b/wolnelektury/templates/catalogue/book_text.html index 6c8edba6d..935cdf392 100644 --- a/wolnelektury/templates/catalogue/book_text.html +++ b/wolnelektury/templates/catalogue/book_text.html @@ -25,34 +25,7 @@
-

- {% if book.get_extra_info_value.license %} - {% trans "This work is licensed under:" %} - {{ book.get_extra_info_value.license_description }} - {% else %} - {% blocktrans %}This work isn't covered by copyright and is part of the - public domain, which means it can be freely used, published and - distributed. If there are any additional copyrighted materials - provided with this work (such as annotations, motifs etc.), those - materials are licensed under the - Creative Commons Attribution-ShareAlike 3.0 - license.{% endblocktrans %} - {% endif %} -

- - {% if book.get_extra_info_value.source_name %} -

{% trans "Text prepared based on:" %} {{ book.get_extra_info_value.source_name }}

- {% endif %} - - {% if book.get_extra_info_value.description %} -

{{ book.get_extra_info_value.description }}

- {% endif %} - - {% if book.get_extra_info_value.editor or book.get_extra_info_value.technical_editor %} -

{% trans "Edited and annotated by:" %} - {% all_editors book.get_extra_info_value %}.

- {% endif %} - + {% book_info book %}