more api changes, preparing for Android app
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 30 Aug 2011 15:49:04 +0000 (17:49 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 30 Aug 2011 15:49:04 +0000 (17:49 +0200)
17 files changed:
apps/api/handlers.py
apps/api/management/commands/mobileinit.py
apps/api/models.py
apps/api/tests.py
apps/api/urls.py
apps/api/views.py [deleted file]
apps/catalogue/migrations/0008_auto__add_field_book_sort_key.py [new file with mode: 0644]
apps/catalogue/migrations/0009_sortify.py [new file with mode: 0644]
apps/catalogue/models.py
apps/catalogue/templatetags/catalogue_tags.py
apps/catalogue/urls.py
apps/catalogue/utils.py
apps/catalogue/views.py
lib/sortify.py [new file with mode: 0755]
wolnelektury/settings.py
wolnelektury/templates/catalogue/book_info.html [new file with mode: 0755]
wolnelektury/templates/catalogue/book_text.html

index a4782dc..76de8c6 100644 (file)
@@ -2,13 +2,15 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 
 # 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 datetime, timedelta
 from piston.handler import BaseHandler
 from piston.handler import BaseHandler
+from django.conf import settings
 
 from api.helpers import timestamp
 from api.models import Deleted
 from catalogue.models import Book, Tag
 
 
 from api.helpers import timestamp
 from api.models import Deleted
 from catalogue.models import Book, Tag
 
+
 class CatalogueHandler(BaseHandler):
 
     @staticmethod
 class CatalogueHandler(BaseHandler):
 
     @staticmethod
@@ -16,6 +18,22 @@ class CatalogueHandler(BaseHandler):
         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
 
         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.
+        
+            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.
+
+        """
+        # 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',
     @staticmethod
     def book_dict(book, fields=None):
         all_fields = ('url', 'title', 'description',
@@ -26,6 +44,7 @@ class CatalogueHandler(BaseHandler):
                       'tags',
                       'license', 'license_description', 'source_name',
                       'technical_editors', 'editors',
                       '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)
                      )
         if fields:
             fields = (f for f in fields if f in all_fields)
@@ -61,6 +80,9 @@ class CatalogueHandler(BaseHandler):
             elif field == 'tags':
                 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
 
             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)
             elif field in ('license', 'license_description', 'source_name',
                       'technical_editors', 'editors'):
                 f = extra_info.get(field)
@@ -76,8 +98,14 @@ class CatalogueHandler(BaseHandler):
         return obj
 
     @classmethod
         return obj
 
     @classmethod
-    def book_changes(cls, since=0, request=None, fields=None):
+    def book_changes(cls, request=None, since=0, until=None, fields=None):
         since = datetime.fromtimestamp(int(since))
         since = datetime.fromtimestamp(int(since))
+        until = cls.until(until)
+
+        changes = {
+            'time_checked': timestamp(until)
+        }
+
         if not fields:
             fields = cls.fields(request, 'book_fields')
 
         if not fields:
             fields = cls.fields(request, 'book_fields')
 
@@ -86,19 +114,28 @@ class CatalogueHandler(BaseHandler):
         deleted = []
 
         last_change = since
         deleted = []
 
         last_change = since
-        for book in Book.objects.filter(changed_at__gte=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)
             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, created_at__lt=since):
+        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)
             deleted.append(book.id)
-        return {'updated': updated, 'deleted': deleted}
+        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',
 
     @staticmethod
     def tag_dict(tag, fields=None):
         all_fields = ('name', 'category', 'sort_key', 'description',
                       'gazeta_link', 'wiki_link',
-                      'url',
+                      'url', 'books',
                      )
 
         if fields:
                      )
 
         if fields:
@@ -112,6 +149,12 @@ class CatalogueHandler(BaseHandler):
             if field == 'url':
                 obj[field] = tag.get_absolute_url()
 
             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:
             else:
                 f = getattr(tag, field)
                 if f:
@@ -121,14 +164,20 @@ class CatalogueHandler(BaseHandler):
         return obj
 
     @classmethod
         return obj
 
     @classmethod
-    def tag_changes(cls, since=0, request=None, fields=None, categories=None):
+    def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
         since = datetime.fromtimestamp(int(since))
         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')
 
         if not fields:
             fields = cls.fields(request, 'tag_fields')
         if not categories:
             categories = cls.fields(request, 'tag_categories')
 
-        all_categories = ('author', 'theme', 'epoch', 'kind', 'genre')
+        all_categories = ('author', 'epoch', 'kind', 'genre')
         if categories:
             categories = (c for c in categories if c in all_categories)
         else:
         if categories:
             categories = (c for c in categories if c in all_categories)
         else:
@@ -137,29 +186,47 @@ class CatalogueHandler(BaseHandler):
         updated = []
         deleted = []
 
         updated = []
         deleted = []
 
-        for tag in Tag.objects.filter(category__in=categories, changed_at__gte=since):
-            tag_d = cls.tag_dict(tag, fields)
-            updated.append(tag_d)
+        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,
 
         for tag in Deleted.objects.filter(category__in=categories,
-                content_type=Tag, deleted_at__gte=since, created_at__lt=since):
+                content_type=Tag, 
+                    deleted_at__gte=since,
+                    deleted_at__lt=until,
+                    created_at__lt=since):
             deleted.append(tag.id)
             deleted.append(tag.id)
-        return {'updated': updated, 'deleted': deleted}
+        if deleted:
+            changes['deleted'] = deleted
+
+        return changes
 
     @classmethod
 
     @classmethod
-    def changes(cls, since=0, request=None, book_fields=None,
+    def changes(cls, request=None, since=0, until=None, book_fields=None,
                 tag_fields=None, tag_categories=None):
                 tag_fields=None, tag_categories=None):
+        until = cls.until(until)
+
         changes = {
         changes = {
-            'time_checked': timestamp(datetime.now())
+            'time_checked': timestamp(until)
         }
 
         changes_by_type = {
         }
 
         changes_by_type = {
-            'books': cls.book_changes(since, request, book_fields),
-            'tags': cls.tag_changes(since, request, tag_fields, tag_categories),
+            '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]:
         }
 
         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
 
                 changes.setdefault(field, {})[model] = changes_by_type[model][field]
         return changes
 
@@ -168,18 +235,18 @@ class BookChangesHandler(CatalogueHandler):
     allowed_methods = ('GET',)
 
     def read(self, request, since):
     allowed_methods = ('GET',)
 
     def read(self, request, since):
-        return self.book_changes(since, request)
+        return self.book_changes(request, since)
 
 
 class TagChangesHandler(CatalogueHandler):
     allowed_methods = ('GET',)
 
     def read(self, request, since):
 
 
 class TagChangesHandler(CatalogueHandler):
     allowed_methods = ('GET',)
 
     def read(self, request, since):
-        return self.tag_changes(since, request)
+        return self.tag_changes(request, since)
 
 
 class ChangesHandler(CatalogueHandler):
     allowed_methods = ('GET',)
 
     def read(self, request, since):
 
 
 class ChangesHandler(CatalogueHandler):
     allowed_methods = ('GET',)
 
     def read(self, request, since):
-        return self.changes(since, request)
+        return self.changes(request, since)
index 8e225f9..cfb9fb4 100755 (executable)
@@ -8,12 +8,10 @@ import os.path
 import re
 import sqlite3
 from django.core.management.base import BaseCommand
 import re
 import sqlite3
 from django.core.management.base import BaseCommand
-from slughifi import char_map
 
 from api.helpers import timestamp
 from api.settings import MOBILE_INIT_DB
 from catalogue.models import Book, Tag
 
 from api.helpers import timestamp
 from api.settings import MOBILE_INIT_DB
 from catalogue.models import Book, Tag
-from catalogue.views import tagged_object_list # this should be somewhere else
 
 
 class Command(BaseCommand):
 
 
 class Command(BaseCommand):
@@ -26,7 +24,9 @@ class Command(BaseCommand):
         for b in Book.objects.all():
             add_book(db, b)
         for t in Tag.objects.exclude(category__in=('book', 'set', 'theme')):
         for b in Book.objects.all():
             add_book(db, b)
         for t in Tag.objects.exclude(category__in=('book', 'set', 'theme')):
-            add_tag(db, t)
+            # only add non-empty tags
+            if t.get_count():
+                add_tag(db, t)
         db.commit()
         db.close()
         current(last_checked)
         db.commit()
         db.close()
         current(last_checked)
@@ -51,26 +51,6 @@ def pretty_size(size):
     return "%d %s" % (size, unit)
 
 
     return "%d %s" % (size, unit)
 
 
-special_marks = {'ż': '|', 'Ż': '|',}
-def replace_char(m):
-    char = m.group()
-    if char_map.has_key(char):
-        special = special_marks.get(char, '{')
-        return char_map[char] + special
-    else:
-        return char
-
-def sortify(value):
-    """
-        Turns Unicode into ASCII-sortable str
-
-        Examples :
-
-        >>> slughifi('aa') < slughifi('a a') < slughifi('ą') < slughifi('b')
-        True
-
-    """
-
     if not isinstance(value, unicode):
         value = unicode(value, 'utf-8')
 
     if not isinstance(value, unicode):
         value = unicode(value, 'utf-8')
 
@@ -116,10 +96,6 @@ 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 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 book_tag (book INTEGER, tag INTEGER);
-CREATE INDEX IF NOT EXISTS book_tag_book ON book_tag (book);
-CREATE INDEX IF NOT EXISTS book_tag_tag_index ON book_tag (tag);
-
 CREATE TABLE state (last_checked INTEGER);
 """
 
 CREATE TABLE state (last_checked INTEGER);
 """
 
@@ -169,7 +145,7 @@ def add_book(db, book):
         html_file = html_file_size = None
     parent = book.parent
     parent_number = book.parent_number
         html_file = html_file_size = None
     parent = book.parent
     parent_number = book.parent_number
-    sort_key = sortify(title)
+    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())
     size_str = pretty_size(html_file_size)
     authors = ", ".join(t.name for t in book.tags.filter(category='author'))
     db.execute(book_sql, locals())
@@ -179,11 +155,8 @@ def add_tag(db, tag):
     id = tag.id
     category = categories[tag.category]
     name = tag.name
     id = tag.id
     category = categories[tag.category]
     name = tag.name
-    sort_key = sortify(tag.sort_key)
+    sort_key = tag.sort_key
 
 
-    books = list(tagged_object_list(None, [tag], api=True))
+    books = Book.tagged_top_level([tag])
     book_ids = ','.join(str(b.id) for b in books)
     db.execute(tag_sql, locals())
     book_ids = ','.join(str(b.id) for b in books)
     db.execute(tag_sql, locals())
-
-    for b in books:
-        db.execute(book_tag_sql, {'book': b.id, 'tag': tag.id})
index 22e5648..2cdc74a 100644 (file)
@@ -31,6 +31,7 @@ def _pre_delete_handler(sender, instance, **kwargs):
                 category = instance.category
         else:
             category = None
                 category = instance.category
         else:
             category = None
-        Deleted.objects.create(type=sender, object_id=instance.id, 
+        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)
             created_at=instance.created_at, category=category)
 pre_delete.connect(_pre_delete_handler)
index e3dd71a..41df4c6 100644 (file)
@@ -4,62 +4,59 @@ from datetime import datetime
 
 from django.test import TestCase
 from django.utils import simplejson as json
 
 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
 
 
 
 from api.helpers import timestamp
 from catalogue.models import Book, Tag
 
 
-class ChangesTests(TestCase):
+class ApiTest(TestCase):
 
 
-    def test_basic(self):
-        book = Book.objects.create(slug='a-book', title='A Book')
-        tag = Tag.objects.create(category='author', slug='author', name='Author')
+    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):
 
 
-        print self.client.get('/api/changes/0.json?book_fields=slug&tag_fields=slug').content
-        changes = json.loads(self.client.get('/api/changes/0.json?book_fields=slug&tag_fields=slug').content)
-        self.assertEqual(changes['added']['books'], 
-                         [{'id': book.id, 'slug': book.slug}],
+    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')
                          'Invalid book format in changes')
-        self.assertEqual(changes['added']['tags'], 
-                         [{'id': tag.id, 'slug': tag.slug}],
+        self.assertEqual(changes['updated']['tags'], 
+                         [{'id': tag.id, 'name': tag.name}],
                          'Invalid tag format in changes')
 
 
                          'Invalid tag format in changes')
 
 
-class BookChangesTests(TestCase):
+class BookChangesTests(ApiTest):
 
     def setUp(self):
 
     def setUp(self):
-        self.book = Book.objects.create()
+        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)
 
     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['added']),
-                         1,
-                         'Added book not in book_changes.added')
-
-        # test changed book in changed
-        self.book.slug = 'a-book'
-        self.book.save()
-        changes = json.loads(self.client.get('/api/book_changes/%f.json' % timestamp(self.book.created_at)).content)
-        self.assertEqual(changes['added'],
-                         [],
-                         'Changed book in book_changes.added instead of book_changes.changed.')
-        self.assertEqual(len(changes['changed']),
+        self.assertEqual(len(changes['updated']),
                          1,
                          1,
-                         'Changed book not in book_changes.changed.')
+                         'Added book not in book_changes.updated')
 
 
-        # test deleted book in deleted
+    def test_deleted_disappears(self):
+        # test deleted book disappears
         Book.objects.all().delete()
         Book.objects.all().delete()
-        changes = json.loads(self.client.get('/api/book_changes/%f.json' % timestamp(self.book.changed_at)).content)
-        self.assertEqual(changes['added'],
-                         [],
-                         'Deleted book still in book_changes.added.')
-        self.assertEqual(changes['changed'],
-                         [],
-                         'Deleted book still in book_changes.changed.')
-        self.assertEqual(len(changes['deleted']),
-                         1,
-                         'Deleted book not in book_changes.deleted.')
+        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
 
     def test_shelf(self):
         changed_at = self.book.changed_at
@@ -71,38 +68,25 @@ class BookChangesTests(TestCase):
         self.assertEqual(self.book.changed_at,
                          changed_at)
 
         self.assertEqual(self.book.changed_at,
                          changed_at)
 
-class TagChangesTests(TestCase):
+class TagChangesTests(ApiTest):
 
     def setUp(self):
 
     def setUp(self):
-        self.tag = Tag.objects.create()
+        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_basic(self):
+    def test_added(self):
         # test tag in tag_changes.added
         changes = json.loads(self.client.get('/api/tag_changes/0.json').content)
         # test tag in tag_changes.added
         changes = json.loads(self.client.get('/api/tag_changes/0.json').content)
-        self.assertEqual(len(changes['added']),
+        self.assertEqual(len(changes['updated']),
                          1,
                          1,
-                         'Added tag not in tag_changes.added')
-
-        # test changed tag in changed
-        self.tag.slug = 'a-tag'
-        self.tag.save()
-        changes = json.loads(self.client.get('/api/tag_changes/%f.json' % timestamp(self.tag.created_at)).content)
-        self.assertEqual(changes['added'],
-                         [],
-                         'Changed tag in tag_changes.added instead of tag_changes.changed.')
-        self.assertEqual(len(changes['changed']),
-                         1,
-                         'Changed tag not in tag_changes.changed.')
-
-        # test deleted book in deleted
-        Tag.objects.all().delete()
-        changes = json.loads(self.client.get('/api/tag_changes/%f.json' % timestamp(self.tag.changed_at)).content)
-        self.assertEqual(changes['added'],
-                         [],
-                         'Deleted tag still in tag_changes.added.')
-        self.assertEqual(changes['changed'],
-                         [],
-                         'Deleted tag still in tag_changes.changed.')
-        self.assertEqual(len(changes['deleted']),
-                         1,
-                         'Deleted tag not in tag_changes.deleted.')
+                         '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.')
index eb2df8c..536454f 100644 (file)
@@ -1,36 +1,20 @@
 # -*- coding: utf-8 -*-
 from django.conf.urls.defaults import *
 from piston.resource import Resource
 # -*- coding: utf-8 -*-
 from django.conf.urls.defaults import *
 from piston.resource import Resource
-#from piston.authentication import HttpBasicAuthentication
 
 
-#from api.handlers import BookHandler, TagHandler
 from api import handlers
 
 
 from api import handlers
 
 
-#auth = OAuthAuthentication(realm='API')
-#book_changes_resource = Resource(handler=handlers.BookChangesHandler)
-#tag_changes_resource = Resource(handler=handlers.TagChangesHandler)#, authentication=auth)
+book_changes_resource = Resource(handler=handlers.BookChangesHandler)
+tag_changes_resource = Resource(handler=handlers.TagChangesHandler)
 changes_resource = Resource(handler=handlers.ChangesHandler)
 
 urlpatterns = patterns('',
 changes_resource = Resource(handler=handlers.ChangesHandler)
 
 urlpatterns = patterns('',
-    #url(r'^book_changes/(?P<since>\d*(\.\d*)?)\.(?P<emitter_format>xml|json|yaml)$', book_changes_resource),
-    #url(r'^tag_changes/(?P<since>\d*(\.\d*)?)\.(?P<emitter_format>xml|json|yaml)$', tag_changes_resource),
-    url(r'^changes/(?P<since>\d*(\.\d*)?)\.(?P<emitter_format>xml|json|yaml)$', changes_resource),
+    url(r'^book_changes/(?P<since>\d*?)\.(?P<emitter_format>xml|json|yaml)$', book_changes_resource),
+    url(r'^tag_changes/(?P<since>\d*?)\.(?P<emitter_format>xml|json|yaml)$', tag_changes_resource),
+    url(r'^changes/(?P<since>\d*?)\.(?P<emitter_format>xml|json|yaml)$', changes_resource),
 
 
-    #url(r'^books/(?P<id>[\d,]+)\.(?P<emitter_format>xml|json|yaml)$', book_resource),
-    #url(r'^books\.(?P<emitter_format>xml|json|yaml)$', book_resource),
 
 
-    #url(r'^tags/(?P<tags>[a-zA-Z0-9-/]*)\.(?P<emitter_format>xml|json|yaml)$', tag_resource),
-    #url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)\.(?P<emitter_format>xml|json|yaml)$', book_resource), #detail
-    #url(r'^lektura/(?P<book_slug>[a-zA-Z0-9-]+)/motyw/(?P<theme_slug>[a-zA-Z0-9-]+)\.(?P<emitter_format>xml|json|yaml)$', book_resource), #fragments
-    #url(r'^oauth/callback/$','oauth_callback'),
+    url(r'book/(?P<id>\d*?)/info\.html$', 'catalogue.views.book_info'),
+    url(r'tag/(?P<id>\d*?)/info\.html$', 'catalogue.views.tag_info'),
 )
 )
-
-"""
-urlpatterns += patterns(
-'piston.authentication',
-    url(r'^oauth/request_token/$','oauth_request_token'),
-    url(r'^oauth/authorize/$','oauth_user_auth'),
-    url(r'^oauth/access_token/$','oauth_access_token'),
-)
-"""
diff --git a/apps/api/views.py b/apps/api/views.py
deleted file mode 100644 (file)
index 1c5e848..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-from django.http import HttpResponseRedirect
-
-def oauth_callback(request, data):
-    key       = data.key
-    secret    = data.secret
-    verifier  = data.verifier
-    timestamp = data.timestamp
-    print data
-    return HttpResponseRedirect("wl://")
-    #return HttpResponseRedirect("wl://%s/%s/%s/%s/" % (key, secret, verifier, timestamp))
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 (file)
index 0000000..22ddb52
--- /dev/null
@@ -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 (file)
index 0000000..157fac6
--- /dev/null
@@ -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']
index 34c1d0b..c1290fb 100644 (file)
@@ -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.
 #
 # 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 _
 from django.db import models
 from django.db.models import permalink, Q
 from django.utils.translation import ugettext_lazy as _
@@ -22,6 +24,7 @@ from catalogue.fields import JSONField
 from librarian import dcparser, html, epub, NoDublinCore
 from mutagen import id3
 from slughifi import slughifi
 from librarian import dcparser, html, epub, NoDublinCore
 from mutagen import id3
 from slughifi import slughifi
+from sortify import sortify
 
 
 TAG_CATEGORIES = (
 
 
 TAG_CATEGORIES = (
@@ -221,13 +224,14 @@ class BookMedia(models.Model):
 
 class Book(models.Model):
     title         = models.CharField(_('title'), max_length=120)
 
 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, 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)
     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, 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
     gazeta_link   = models.CharField(blank=True, max_length=240)
     wiki_link     = models.CharField(blank=True, max_length=240)
     # files generated during publication
@@ -251,7 +255,7 @@ class Book(models.Model):
         pass
 
     class Meta:
         pass
 
     class Meta:
-        ordering = ('title',)
+        ordering = ('sort_key',)
         verbose_name = _('book')
         verbose_name_plural = _('books')
 
         verbose_name = _('book')
         verbose_name_plural = _('books')
 
@@ -259,6 +263,8 @@ class Book(models.Model):
         return self.title
 
     def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
         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 = {}
         if reset_short_html:
             # Reset _short_html during save
             update = {}
@@ -558,7 +564,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, 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)
 
                     tag.save()
                 book_tags.append(tag)
 
@@ -704,6 +710,24 @@ class Book(models.Model):
 
         return ', '.join(names)
 
 
         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()
 
 class Fragment(models.Model):
     text = models.TextField()
@@ -759,7 +783,8 @@ class FileRecord(models.Model):
 
 def _tags_updated_handler(sender, affected_tags, **kwargs):
     # reset tag global counter
 
 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 \
 
     # if book tags changed, reset book tag counter
     if isinstance(sender, Book) and \
index 5cfa5fc..c931e48 100644 (file)
@@ -275,3 +275,7 @@ def folded_tag_list(tags, title='', choices=None):
             some_tags_hidden = True
     return locals()
 
             some_tags_hidden = True
     return locals()
 
+
+@register.inclusion_tag('catalogue/book_info.html')
+def book_info(book):
+    return locals()
index cc97e92..2ae2457 100644 (file)
@@ -31,6 +31,7 @@ urlpatterns = patterns('catalogue.views',
     url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)/$', 'book_detail', name='book_detail'),
     url(r'^lektura/(?P<book_slug>[a-zA-Z0-9-]+)/motyw/(?P<theme_slug>[a-zA-Z0-9-]+)/$',
         'book_fragments', name='book_fragments'),
     url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)/$', 'book_detail', name='book_detail'),
     url(r'^lektura/(?P<book_slug>[a-zA-Z0-9-]+)/motyw/(?P<theme_slug>[a-zA-Z0-9-]+)/$',
         'book_fragments', name='book_fragments'),
+
     url(r'^(?P<tags>[a-zA-Z0-9-/]*)/$', 'tagged_object_list', name='tagged_object_list'),
 
     url(r'^audiobooki/(?P<type>mp3|ogg|daisy|all).xml$', AudiobookFeed(), name='audiobook_feed'),
     url(r'^(?P<tags>[a-zA-Z0-9-/]*)/$', 'tagged_object_list', name='tagged_object_list'),
 
     url(r'^audiobooki/(?P<type>mp3|ogg|daisy|all).xml$', AudiobookFeed(), name='audiobook_feed'),
index e44b379..7a405ba 100644 (file)
@@ -30,4 +30,3 @@ def split_tags(tags):
     for tag in tags:
         result.setdefault(tag.category, []).append(tag)
     return result
     for tag in tags:
         result.setdefault(tag.category, []).append(tag)
     return result
-
index 2b4e75e..aedc4ca 100644 (file)
@@ -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.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
 
 from django.utils.translation import ugettext as _
 from django.views.generic.list_detail import object_list
 
@@ -82,7 +83,7 @@ def book_list(request, filter=None, template_name='catalogue/book_list.html'):
     form = forms.SearchForm()
 
     books_by_parent = {}
     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))
     if filter:
         books = books.filter(filter).distinct()
         book_ids = set((book.pk for book in books))
@@ -141,7 +142,7 @@ def differentiate_tags(request, tags, ambiguous_slugs):
                 context_instance=RequestContext(request))
 
 
                 context_instance=RequestContext(request))
 
 
-def tagged_object_list(request, tags='', api=False):
+def tagged_object_list(request, tags=''):
     try:
         tags = models.Tag.get_tag_list(tags)
     except models.Tag.DoesNotExist:
     try:
         tags = models.Tag.get_tag_list(tags)
     except models.Tag.DoesNotExist:
@@ -193,14 +194,10 @@ def tagged_object_list(request, tags='', api=False):
 
             objects = fragments
     else:
 
             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 = {}
 
         # get related tags from `tag_counter` and `theme_counter`
         related_counts = {}
@@ -222,22 +219,19 @@ def tagged_object_list(request, tags='', api=False):
         only_author = len(tags) == 1 and tags[0].category == 'author'
         objects = models.Book.objects.none()
 
         only_author = len(tags) == 1 and tags[0].category == 'author'
         objects = models.Book.objects.none()
 
-    if api:
-        return objects    
-    else:
-        return object_list(
-            request,
-            objects,
-            template_name='catalogue/tagged_object_list.html',
-            extra_context={
-                'categories': categories,
-                'only_shelf': only_shelf,
-                'only_author': only_author,
-                'only_my_shelf': only_my_shelf,
-                'formats_form': forms.DownloadFormatsForm(),
-                'tags': tags,
-            }
-        )
+    return object_list(
+        request,
+        objects,
+        template_name='catalogue/tagged_object_list.html',
+        extra_context={
+            'categories': categories,
+            'only_shelf': only_shelf,
+            'only_author': only_author,
+            'only_my_shelf': only_my_shelf,
+            'formats_form': forms.DownloadFormatsForm(),
+            'tags': tags,
+        }
+    )
 
 
 def book_fragments(request, book_slug, theme_slug):
 
 
 def book_fragments(request, book_slug, theme_slug):
@@ -260,7 +254,7 @@ def book_detail(request, slug):
     book_tag = book.book_tag()
     tags = list(book.tags.filter(~Q(category='set')))
     categories = split_tags(tags)
     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 = []
     
     _book = book
     parents = []
@@ -788,3 +782,18 @@ def xmls(request):
     temp.seek(0)
     response.write(temp.read())
     return response
     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 (executable)
index 0000000..c64668b
--- /dev/null
@@ -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')
index 49073a1..dac4e6a 100644 (file)
@@ -204,6 +204,10 @@ THUMBNAIL_PROCESSORS = (
 
 TRANSLATION_REGISTRY = "wolnelektury.translation"
 
 
 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
 
 # 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 (executable)
index 0000000..670679e
--- /dev/null
@@ -0,0 +1,30 @@
+{% load i18n %}
+{% load catalogue_tags %}
+
+<p>
+    {% if book.get_extra_info_value.license %}
+        {% trans "This work is licensed under:" %}
+        <a href="{{ book.get_extra_info_value.license }}">{{ book.get_extra_info_value.license_description }}</a>
+    {% 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 
+        <a href="http://creativecommons.org/licenses/by-sa/3.0/">Creative Commons Attribution-ShareAlike 3.0</a>
+        license.{% endblocktrans %}
+    {% endif %}
+</p>
+
+{% if book.get_extra_info_value.source_name %}
+  <p>{% trans "Text prepared based on:" %} {{ book.get_extra_info_value.source_name }}</p>
+{% endif %}
+
+{% if book.get_extra_info_value.description %}
+  <p>{{ book.get_extra_info_value.description }}</p>
+{% endif %}
+
+{% if book.get_extra_info_value.editor or book.get_extra_info_value.technical_editor %}
+  <p>{% trans "Edited and annotated by:" %}
+      {% all_editors book.get_extra_info_value %}.</p>
+{% endif %}
index cb00818..4dc6cb9 100644 (file)
             </ul>
         </div>
         <div id="info">
             </ul>
         </div>
         <div id="info">
-            <p>
-                {% if book.get_extra_info_value.license %}
-                    {% trans "This work is licensed under:" %}
-                    <a href="{{ book.get_extra_info_value.license }}">{{ book.get_extra_info_value.license_description }}</a>
-                {% 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 
-                    <a href="http://creativecommons.org/licenses/by-sa/3.0/">Creative Commons Attribution-ShareAlike 3.0</a>
-                    license.{% endblocktrans %}
-                {% endif %}
-            </p>
-    
-            {% if book.get_extra_info_value.source_name %}
-              <p>{% trans "Text prepared based on:" %} {{ book.get_extra_info_value.source_name }}</p>
-            {% endif %}
-    
-            {% if book.get_extra_info_value.description %}
-              <p>{{ book.get_extra_info_value.description }}</p>
-            {% endif %}
-
-            {% if book.get_extra_info_value.editor or book.get_extra_info_value.technical_editor %}
-              <p>{% trans "Edited and annotated by:" %}
-                  {% all_editors book.get_extra_info_value %}.</p>
-            {% endif %}
-
+            {% book_info book %}
         </div>
         <div id="header">
             <div id="logo">
         </div>
         <div id="header">
             <div id="logo">