# 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 django.conf import settings
from api.helpers import timestamp
from api.models import Deleted
from catalogue.models import Book, Tag
+
class CatalogueHandler(BaseHandler):
@staticmethod
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',
'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)
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)
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))
+ until = cls.until(until)
+
+ changes = {
+ 'time_checked': timestamp(until)
+ }
+
if not fields:
fields = cls.fields(request, 'book_fields')
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)
+ 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)
- 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',
- 'url',
+ 'url', 'books',
)
if 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:
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))
+ 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', 'theme', 'epoch', 'kind', 'genre')
+ all_categories = ('author', 'epoch', 'kind', 'genre')
if categories:
categories = (c for c in categories if c in all_categories)
else:
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,
- 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)
- return {'updated': updated, 'deleted': deleted}
+ if deleted:
+ changes['deleted'] = deleted
+
+ return changes
@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):
+ until = cls.until(until)
+
changes = {
- 'time_checked': timestamp(datetime.now())
+ 'time_checked': timestamp(until)
}
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]:
+ if field == 'time_checked':
+ continue
changes.setdefault(field, {})[model] = changes_by_type[model][field]
return changes
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):
- return self.tag_changes(since, request)
+ return self.tag_changes(request, since)
class ChangesHandler(CatalogueHandler):
allowed_methods = ('GET',)
def read(self, request, since):
- return self.changes(since, request)
+ return self.changes(request, since)
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 catalogue.views import tagged_object_list # this should be somewhere else
class Command(BaseCommand):
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)
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')
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);
"""
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())
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())
-
- for b in books:
- db.execute(book_tag_sql, {'book': b.id, 'tag': tag.id})
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)
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 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')
- 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')
-class BookChangesTests(TestCase):
+class BookChangesTests(ApiTest):
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)
- 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,
- '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()
- 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
self.assertEqual(self.book.changed_at,
changed_at)
-class TagChangesTests(TestCase):
+class TagChangesTests(ApiTest):
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)
- self.assertEqual(len(changes['added']),
+ self.assertEqual(len(changes['updated']),
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.')
# -*- 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
-#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('',
- #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'),
-)
-"""
+++ /dev/null
-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))
--- /dev/null
+# 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']
--- /dev/null
+# 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']
# 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 librarian import dcparser, html, epub, NoDublinCore
from mutagen import id3
from slughifi import slughifi
+from sortify import sortify
TAG_CATEGORIES = (
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)
- 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
pass
class Meta:
- ordering = ('title',)
+ ordering = ('sort_key',)
verbose_name = _('book')
verbose_name_plural = _('books')
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 = {}
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)
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()
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 \
some_tags_hidden = True
return locals()
+
+@register.inclusion_tag('catalogue/book_info.html')
+def book_info(book):
+ return locals()
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'),
for tag in tags:
result.setdefault(tag.category, []).append(tag)
return result
-
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
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))
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:
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 = {}
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):
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 = []
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)
--- /dev/null
+# -*- 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')
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
--- /dev/null
+{% 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 %}
</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">