basic multilingual publications support
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 7 Dec 2011 16:18:36 +0000 (17:18 +0100)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 7 Dec 2011 16:18:36 +0000 (17:18 +0100)
removed the unused FileRecord model

21 files changed:
apps/api/handlers.py
apps/api/urls.py
apps/catalogue/migrations/0017_auto__add_field_book_language__del_unique_book_slug__add_unique_book_s.py [new file with mode: 0644]
apps/catalogue/migrations/0018_auto__del_filerecord.py [new file with mode: 0644]
apps/catalogue/models.py
apps/catalogue/test_utils.py
apps/catalogue/tests/book_import.py
apps/catalogue/tests/files/fraszki.xml
apps/catalogue/urls.py
apps/catalogue/utils.py
apps/catalogue/views.py
apps/dictionary/templates/dictionary/note_list.html
apps/lesmianator/urls.py
apps/lesmianator/views.py
lib/librarian
wolnelektury/settings.py
wolnelektury/templates/catalogue/book_detail.html
wolnelektury/templates/catalogue/book_sets.html
wolnelektury/templates/catalogue/book_short.html
wolnelektury/templates/catalogue/tagged_object_list.html
wolnelektury/templates/lesmianator/poem.html

index 8f06dea..f83a492 100644 (file)
@@ -97,11 +97,14 @@ class BookDetailHandler(BaseHandler):
         'media', 'url'] + category_singular.keys()
 
     @piwik_track
-    def read(self, request, slug):
-        """ Returns details of a book, identified by a slug. """
+    def read(self, request, book):
+        """ Returns details of a book, identified by a slug and lang. """
+        kwargs = Book.split_urlid(book)
+        if not kwargs:
+            return rc.NOT_FOUND
 
         try:
-            return Book.objects.get(slug=slug)
+            return Book.objects.get(**kwargs)
         except Book.DoesNotExist:
             return rc.NOT_FOUND
 
@@ -122,7 +125,7 @@ class AnonymousBooksHandler(AnonymousBaseHandler):
     @classmethod
     def href(cls, book):
         """ Returns an URI for a Book in the API. """
-        return API_BASE + reverse("api_book", args=[book.slug])
+        return API_BASE + reverse("api_book", args=[book.urlid()])
 
     @classmethod
     def url(cls, book):
@@ -265,11 +268,18 @@ class FragmentDetailHandler(BaseHandler):
     fields = ['book', 'anchor', 'text', 'url', 'themes']
 
     @piwik_track
-    def read(self, request, slug, anchor):
+    def read(self, request, book, anchor):
         """ Returns details of a fragment, identified by book slug and anchor. """
+        kwargs = Book.split_urlid(book)
+        if not kwargs:
+            return rc.NOT_FOUND
+
+        fragment_kwargs = {}
+        for field, value in kwargs.items():
+            fragment_kwargs['book__' + field] = value
 
         try:
-            return Fragment.objects.get(book__slug=slug, anchor=anchor)
+            return Fragment.objects.get(anchor=anchor, **fragment_kwargs)
         except Fragment.DoesNotExist:
             return rc.NOT_FOUND
 
@@ -306,7 +316,7 @@ class FragmentsHandler(BaseHandler):
     def href(cls, fragment):
         """ Returns URI in the API for the fragment. """
 
-        return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
+        return API_BASE + reverse("api_fragment", args=[fragment.book.urlid(), fragment.anchor])
 
     @classmethod
     def url(cls, fragment):
index 2d92eba..b6fc760 100644 (file)
@@ -4,6 +4,7 @@ from piston.authentication import OAuthAuthentication
 from piston.resource import Resource
 
 from api import handlers
+from catalogue.models import Book
 
 
 auth = OAuthAuthentication(realm="Wolne Lektury")
@@ -46,10 +47,10 @@ urlpatterns = patterns(
 
 
     # objects details
-    url(r'^books/(?P<slug>[a-z0-9-]+)/$', book_resource, name="api_book"),
+    url(r'^books/(?P<book>%s)/$' % Book.URLID_RE, book_resource, name="api_book"),
     url(r'^(?P<category>[a-z0-9-]+)/(?P<slug>[a-z0-9-]+)/$',
         tag_resource, name="api_tag"),
-    url(r'^books/(?P<slug>[a-z0-9-]+)/fragments/(?P<anchor>[a-z0-9-]+)/$',
+    url(r'^books/(?P<book>%s)/fragments/(?P<anchor>[a-z0-9-]+)/$' % Book.URLID_RE,
         fragment_resource, name="api_fragment"),
 
     # books by tags
diff --git a/apps/catalogue/migrations/0017_auto__add_field_book_language__del_unique_book_slug__add_unique_book_s.py b/apps/catalogue/migrations/0017_auto__add_field_book_language__del_unique_book_slug__add_unique_book_s.py
new file mode 100644 (file)
index 0000000..6d1edcf
--- /dev/null
@@ -0,0 +1,144 @@
+# 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):
+        
+        # Removing unique constraint on 'Book', fields ['slug']
+        db.delete_unique('catalogue_book', ['slug'])
+
+        # Adding field 'Book.language'
+        db.add_column('catalogue_book', 'language', self.gf('django.db.models.fields.CharField')(default='pol', max_length=3, db_index=True), keep_default=False)
+
+        # Adding unique constraint on 'Book', fields ['slug', 'language']
+        db.create_unique('catalogue_book', ['slug', 'language'])
+
+
+    def backwards(self, orm):
+        
+        # Removing unique constraint on 'Book', fields ['slug', 'language']
+        db.delete_unique('catalogue_book', ['slug', 'language'])
+
+        # Deleting field 'Book.language'
+        db.delete_column('catalogue_book', 'language')
+
+        # Adding unique constraint on 'Book', fields ['slug']
+        db.create_unique('catalogue_book', ['slug'])
+
+
+    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',)", 'unique_together': "[['slug', 'language']]", 'object_name': 'Book'},
+            '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'}),
+            'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': 'True'}),
+            'mobi_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}),
+            'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
+            'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}),
+            'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'})
+        },
+        'catalogue.bookmedia': {
+            'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}),
+            'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}),
+            'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}),
+            'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}),
+            'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+        },
+        'catalogue.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'},
+            '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/0018_auto__del_filerecord.py b/apps/catalogue/migrations/0018_auto__del_filerecord.py
new file mode 100644 (file)
index 0000000..66a6542
--- /dev/null
@@ -0,0 +1,131 @@
+# 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):
+        
+        # Deleting model 'FileRecord'
+        db.delete_table('catalogue_filerecord')
+
+
+    def backwards(self, orm):
+        
+        # Adding model 'FileRecord'
+        db.create_table('catalogue_filerecord', (
+            ('sha1', self.gf('django.db.models.fields.CharField')(max_length=40)),
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('time', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+            ('type', self.gf('django.db.models.fields.CharField')(max_length=20, db_index=True)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=120, db_index=True)),
+        ))
+        db.send_create_signal('catalogue', ['FileRecord'])
+
+
+    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',)", 'unique_together': "[['slug', 'language']]", 'object_name': 'Book'},
+            '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'}),
+            'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': 'True'}),
+            'mobi_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}),
+            'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
+            'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}),
+            'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}),
+            'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'})
+        },
+        'catalogue.bookmedia': {
+            'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}),
+            'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}),
+            'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}),
+            'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}),
+            'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+        },
+        'catalogue.fragment': {
+            'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'},
+            'anchor': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fragments'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'short_text': ('django.db.models.fields.TextField', [], {}),
+            'text': ('django.db.models.fields.TextField', [], {})
+        },
+        'catalogue.tag': {
+            'Meta': {'ordering': "('sort_key',)", 'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'},
+            'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
+            'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            '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 b82879b..2ca78b0 100644 (file)
@@ -202,7 +202,7 @@ def get_customized_pdf_path(book, customizations):
     """
     customizations.sort()
     h = hash(tuple(customizations))
-    pdf_name = '%s-custom-%s' % (book.slug, h)
+    pdf_name = '%s-custom-%s' % (book.fileid(), h)
     pdf_file = models.get_dynamic_path(None, pdf_name, ext='pdf')
     return pdf_file
 
@@ -211,7 +211,7 @@ def get_existing_customized_pdf(book):
     """
     Returns a list of paths to generated customized pdf of a book
     """
-    pdf_glob = '%s-custom-' % (book.slug,)
+    pdf_glob = '%s-custom-' % (book.fileid(),)
     pdf_glob = get_dynamic_path(None, pdf_glob, ext='pdf')
     pdf_glob = re.sub(r"[.]([a-z0-9]+)$", "*.\\1", pdf_glob)
     return glob(path.join(settings.MEDIA_ROOT, pdf_glob))
@@ -250,7 +250,7 @@ class BookMedia(models.Model):
         super(BookMedia, self).save(*args, **kwargs)
 
         # remove the zip package for book with modified media
-        remove_zip(self.book.slug)
+        remove_zip(self.book.fileid())
 
         extra_info = self.get_extra_info_value()
         extra_info.update(self.read_meta())
@@ -319,7 +319,9 @@ class BookMedia(models.Model):
 class Book(models.Model):
     title         = models.CharField(_('title'), max_length=120)
     sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
-    slug          = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
+    slug          = models.SlugField(_('slug'), max_length=120, db_index=True)
+    language = models.CharField(_('language code'), max_length=3, db_index=True,
+                    default=settings.CATALOGUE_DEFAULT_LANGUAGE)
     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)
@@ -339,10 +341,14 @@ class Book(models.Model):
     html_built = django.dispatch.Signal()
     published = django.dispatch.Signal()
 
+    URLID_RE = r'[a-z0-9-]+(?:/[a-z]{3})?'
+    FILEID_RE = r'[a-z0-9-]+(?:_[a-z]{3})?'
+
     class AlreadyExists(Exception):
         pass
 
     class Meta:
+        unique_together = [['slug', 'language']]
         ordering = ('sort_key',)
         verbose_name = _('book')
         verbose_name_plural = _('books')
@@ -350,6 +356,42 @@ class Book(models.Model):
     def __unicode__(self):
         return self.title
 
+    def urlid(self, sep='/'):
+        stem = self.slug
+        if self.language != settings.CATALOGUE_DEFAULT_LANGUAGE:
+            stem += sep + self.language
+        return stem
+
+    def fileid(self):
+        return self.urlid('_')
+
+    @staticmethod
+    def split_urlid(urlid, sep='/', default_lang=settings.CATALOGUE_DEFAULT_LANGUAGE):
+        """Splits a URL book id into slug and language code.
+        
+        Returns a dictionary usable i.e. for object lookup, or None.
+
+        >>> Book.split_urlid("a-slug/pol", default_lang="eng")
+        {'slug': 'a-slug', 'language': 'pol'}
+        >>> Book.split_urlid("a-slug", default_lang="eng")
+        {'slug': 'a-slug', 'language': 'eng'}
+        >>> Book.split_urlid("a-slug_pol", "_", default_lang="eng")
+        {'slug': 'a-slug', 'language': 'pol'}
+        >>> Book.split_urlid("a-slug/eng", default_lang="eng")
+
+        """
+        parts = urlid.rsplit(sep, 1)
+        if len(parts) == 2:
+            if parts[1] == default_lang:
+                return None
+            return {'slug': parts[0], 'language': parts[1]}
+        else:
+            return {'slug': urlid, 'language': default_lang}
+
+    @classmethod
+    def split_fileid(cls, fileid):
+        return cls.split_urlid(fileid, '_')
+
     def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
         from sortify import sortify
 
@@ -364,14 +406,18 @@ class Book(models.Model):
 
     @permalink
     def get_absolute_url(self):
-        return ('catalogue.views.book_detail', [self.slug])
+        return ('catalogue.views.book_detail', [self.urlid()])
 
     @property
     def name(self):
         return self.title
 
     def book_tag_slug(self):
-        return ('l-' + self.slug)[:120]
+        stem = 'l-' + self.slug
+        if self.language != settings.CATALOGUE_DEFAULT_LANGUAGE:
+            return stem[:116] + ' ' + self.language
+        else:
+            return stem[:120]
 
     def book_tag(self):
         slug = self.book_tag_slug()
@@ -433,7 +479,7 @@ class Book(models.Model):
             formats = []
             # files generated during publication
             if self.has_media("html"):
-                formats.append(u'<a href="%s">%s</a>' % (reverse('book_text', kwargs={'slug': self.slug}), _('Read online')))
+                formats.append(u'<a href="%s">%s</a>' % (reverse('book_text', [self.fileid()]), _('Read online')))
             if self.has_media("pdf"):
                 formats.append(u'<a href="%s">PDF</a>' % self.get_media('pdf').url)
             if self.has_media("mobi"):
@@ -493,37 +539,36 @@ class Book(models.Model):
     has_daisy_file.short_description = 'DAISY'
     has_daisy_file.boolean = True
 
+    def wldocument(self, parse_dublincore=True):
+        from catalogue.utils import ORMDocProvider
+        from librarian.parser import WLDocument
+
+        return WLDocument.from_file(self.xml_file.path,
+                provider=ORMDocProvider(self),
+                parse_dublincore=parse_dublincore)
+
     def build_pdf(self, customizations=None, file_name=None):
         """ (Re)builds the pdf file.
         customizations - customizations which are passed to LaTeX class file.
         file_name - save the pdf file under a different name and DO NOT save it in db.
         """
-        from tempfile import NamedTemporaryFile
         from os import unlink
         from django.core.files import File
-        from librarian import pdf
-        from catalogue.utils import ORMDocProvider, remove_zip
+        from catalogue.utils import remove_zip
 
-        try:
-            pdf_file = NamedTemporaryFile(delete=False)
-            pdf.transform(ORMDocProvider(self),
-                      file_path=str(self.xml_file.path),
-                      output_file=pdf_file,
-                      customizations=customizations
-                      )
-
-            if file_name is None:
-                # we'd like to be sure not to overwrite changes happening while
-                # (timely) pdf generation is taking place (async celery scenario)
-                current_self = Book.objects.get(id=self.id)
-                current_self.pdf_file.save('%s.pdf' % self.slug, File(open(pdf_file.name)))
-                self.pdf_file = current_self.pdf_file
-            else:
-                print "safing %s" % file_name
-                print "to: %s" % DefaultStorage().path(file_name)
-                DefaultStorage().save(file_name, File(open(pdf_file.name)))
-        finally:
-            unlink(pdf_file.name)
+        pdf = self.wldocument().as_pdf(customizations=customizations)
+
+        if file_name is None:
+            # we'd like to be sure not to overwrite changes happening while
+            # (timely) pdf generation is taking place (async celery scenario)
+            current_self = Book.objects.get(id=self.id)
+            current_self.pdf_file.save('%s.pdf' % self.fileid(),
+                    File(open(pdf.get_filename())))
+            self.pdf_file = current_self.pdf_file
+        else:
+            print "saving %s" % file_name
+            print "to: %s" % DefaultStorage().path(file_name)
+            DefaultStorage().save(file_name, File(open(pdf.get_filename())))
 
         # remove cached downloadables
         remove_zip(settings.ALL_PDF_ZIP)
@@ -534,22 +579,12 @@ class Book(models.Model):
         """ (Re)builds the MOBI file.
 
         """
-        from tempfile import NamedTemporaryFile
-        from os import unlink
         from django.core.files import File
-        from librarian import mobi
-        from catalogue.utils import ORMDocProvider, remove_zip
+        from catalogue.utils import remove_zip
 
-        try:
-            mobi_file = NamedTemporaryFile(suffix='.mobi', delete=False)
-            mobi.transform(ORMDocProvider(self), verbose=1,
-                      file_path=str(self.xml_file.path),
-                      output_file=mobi_file.name,
-                      )
+        mobi = self.wldocument().as_mobi()
 
-            self.mobi_file.save('%s.mobi' % self.slug, File(open(mobi_file.name)))
-        finally:
-            unlink(mobi_file.name)
+        self.mobi_file.save('%s.mobi' % self.fileid(), File(open(mobi.get_filename())))
 
         # remove zip with all mobi files
         remove_zip(settings.ALL_MOBI_ZIP)
@@ -559,21 +594,18 @@ class Book(models.Model):
             If book has a parent, does nothing.
             Unless remove_descendants is False, descendants' epubs are removed.
         """
-        from StringIO import StringIO
-        from hashlib import sha1
-        from django.core.files.base import ContentFile
-        from librarian import epub, NoDublinCore
-        from catalogue.utils import ORMDocProvider, remove_zip
+        from django.core.files import File
+        from catalogue.utils import remove_zip
 
         if self.parent:
             # don't need an epub
             return
 
-        epub_file = StringIO()
+        epub = self.wldocument().as_epub()
+
         try:
-            epub.transform(ORMDocProvider(self), self.slug, output_file=epub_file)
-            self.epub_file.save('%s.epub' % self.slug, ContentFile(epub_file.getvalue()))
-            FileRecord(slug=self.slug, type='epub', sha1=sha1(epub_file.getvalue()).hexdigest()).save()
+            epub.transform(ORMDocProvider(self), self.fileid(), output_file=epub_file)
+            self.epub_file.save('%s.epub' % self.fileid(), File(open(epub.get_filename())))
         except NoDublinCore:
             pass
 
@@ -590,19 +622,15 @@ class Book(models.Model):
         remove_zip(settings.ALL_EPUB_ZIP)
 
     def build_txt(self):
-        from StringIO import StringIO
         from django.core.files.base import ContentFile
-        from librarian import text
 
-        out = StringIO()
-        text.transform(open(self.xml_file.path), out)
-        self.txt_file.save('%s.txt' % self.slug, ContentFile(out.getvalue()))
+        text = self.wldocument().as_text()
+        self.txt_file.save('%s.txt' % self.fileid(), ContentFile(text.get_string()))
 
 
     def build_html(self):
-        from tempfile import NamedTemporaryFile
         from markupstring import MarkupString
-        from django.core.files import File
+        from django.core.files.base import ContentFile
         from slughifi import slughifi
         from librarian import html
 
@@ -610,9 +638,10 @@ class Book(models.Model):
             category__in=('author', 'epoch', 'genre', 'kind')))
         book_tag = self.book_tag()
 
-        html_file = NamedTemporaryFile()
-        if html.transform(self.xml_file.path, html_file, parse_dublincore=False):
-            self.html_file.save('%s.html' % self.slug, File(html_file))
+        html_output = self.wldocument(parse_dublincore=False).as_html()
+        if html_output:
+            self.html_file.save('%s.html' % self.fileid(),
+                    ContentFile(html_output.get_string()))
 
             # get ancestor l-tags for adding to new fragments
             ancestor_tags = []
@@ -662,7 +691,7 @@ class Book(models.Model):
         def pretty_file_name(book):
             return "%s/%s.%s" % (
                 b.get_extra_info_value()['author'],
-                b.slug,
+                b.fileid(),
                 format_)
 
         field_name = "%s_file" % format_
@@ -676,7 +705,7 @@ class Book(models.Model):
     def zip_audiobooks(self):
         bm = BookMedia.objects.filter(book=self, type='mp3')
         paths = map(lambda bm: (None, bm.file.path), bm)
-        result = create_zip.delay(paths, self.slug)
+        result = create_zip.delay(paths, self.fileid())
         return result.wait()
 
     @classmethod
@@ -706,24 +735,27 @@ class Book(models.Model):
         children = []
         if hasattr(book_info, 'parts'):
             for part_url in book_info.parts:
-                base, slug = part_url.rsplit('/', 1)
                 try:
-                    children.append(Book.objects.get(slug=slug))
+                    children.append(Book.objects.get(
+                        slug=part_url.slug, language=part_url.language))
                 except Book.DoesNotExist, e:
-                    raise Book.DoesNotExist(_('Book with slug = "%s" does not exist.') % slug)
+                    raise Book.DoesNotExist(_('Book "%s/%s" does not exist.') %
+                            (part_url.slug, part_url.language))
 
 
         # Read book metadata
-        book_base, book_slug = book_info.url.rsplit('/', 1)
+        book_slug = book_info.url.slug
+        language = book_info.language
         if re.search(r'[^a-zA-Z0-9-]', book_slug):
             raise ValueError('Invalid characters in slug')
-        book, created = Book.objects.get_or_create(slug=book_slug)
+        book, created = Book.objects.get_or_create(slug=book_slug, language=language)
 
         if created:
             book_shelves = []
         else:
             if not overwrite:
-                raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
+                raise Book.AlreadyExists(_('Book %s/%s already exists') % (
+                        book_slug, language))
             # Save shelves for this book
             book_shelves = list(book.tags.filter(category='set'))
 
@@ -898,7 +930,8 @@ class Book(models.Model):
         """
 
         books_by_parent = {}
-        books = cls.objects.all().order_by('parent_number', 'sort_key').only('title', 'parent', 'slug')
+        books = cls.objects.all().order_by('parent_number', 'sort_key').only(
+                'title', 'parent', 'slug', 'language')
         if filter:
             books = books.filter(filter).distinct()
             book_ids = set((book.pk for book in books))
@@ -974,7 +1007,7 @@ class Fragment(models.Model):
         verbose_name_plural = _('fragments')
 
     def get_absolute_url(self):
-        return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor)
+        return '%s#m%s' % (self.book.get_html_url(), self.anchor)
 
     def reset_short_html(self):
         if self.id is None:
@@ -1001,20 +1034,6 @@ class Fragment(models.Model):
             return mark_safe(short_html)
 
 
-class FileRecord(models.Model):
-    slug = models.SlugField(_('slug'), max_length=120, db_index=True)
-    type = models.CharField(_('type'), max_length=20, db_index=True)
-    sha1 = models.CharField(_('sha-1 hash'), max_length=40)
-    time = models.DateTimeField(_('time'), auto_now_add=True)
-
-    class Meta:
-        ordering = ('-time','-slug', '-type')
-        verbose_name = _('file record')
-        verbose_name_plural = _('file records')
-
-    def __unicode__(self):
-        return "%s %s.%s" % (self.sha1,  self.slug, self.type)
-
 ###########
 #
 # SIGNALS
index a5f0b4f..70aae02 100644 (file)
@@ -3,6 +3,7 @@ from django.test import TestCase
 import shutil
 import tempfile
 from slughifi import slughifi
+from librarian import WLURI
 
 class WLTestCase(TestCase):
     """
@@ -41,12 +42,14 @@ class BookInfoStub(object):
         return dict((key, unicode(value)) for key, value in self.__dict.items())
 
 
-def info_args(title):
+def info_args(title, language=None):
     """ generate some keywords for comfortable BookInfoCreation  """
     slug = unicode(slughifi(title))
+    if language is None:
+        language = u'pol'
     return {
         'title': unicode(title),
-        'slug': slug,
-        'url': u"http://wolnelektury.pl/example/%s" % slug,
+        'url': WLURI.from_slug_and_lang(slug, language),
         'about': u"http://wolnelektury.pl/example/URI/%s" % slug,
+        'language': language,
     }
index f4588d1..a97c417 100644 (file)
@@ -4,6 +4,7 @@ from __future__ import with_statement
 from django.core.files.base import ContentFile, File
 from catalogue.test_utils import *
 from catalogue import models
+from librarian import WLURI
 
 from nose.tools import raises
 import tempfile
@@ -14,13 +15,14 @@ class BookImportLogicTests(WLTestCase):
     def setUp(self):
         WLTestCase.setUp(self)
         self.book_info = BookInfoStub(
-            url=u"http://wolnelektury.pl/example/default-book",
+            url=WLURI.from_slug_and_lang(u"default-book", None),
             about=u"http://wolnelektury.pl/example/URI/default_book",
             title=u"Default Book",
             author=PersonStub(("Jim",), "Lazy"),
             kind="X-Kind",
             genre="X-Genre",
             epoch="X-Epoch",
+            language=u"pol",
         )
 
         self.expected_tags = [
@@ -112,7 +114,7 @@ class BookImportLogicTests(WLTestCase):
     @raises(ValueError)
     def test_book_with_invalid_slug(self):
         """ Book with invalid characters in slug shouldn't be imported """
-        self.book_info.url = "http://wolnelektury.pl/example/default_book"
+        self.book_info.url = WLURI.from_slug_and_lang(u"default_book", None)
         BOOK_TEXT = "<utwor />"
         book = models.Book.from_text_and_meta(ContentFile(BOOK_TEXT), self.book_info)
 
@@ -242,6 +244,38 @@ class ChildImportTests(WLTestCase):
                         'wrong related theme list')
 
 
+class MultilingualBookImportTest(WLTestCase):
+    def setUp(self):
+        WLTestCase.setUp(self)
+        self.pol_info = BookInfoStub(
+            genre='X-Genre',
+            epoch='X-Epoch',
+            kind='X-Kind',
+            author=PersonStub(("Joe",), "Doe"),
+            **info_args("A book")
+        )
+
+        self.eng_info = BookInfoStub(
+            genre='X-Genre',
+            epoch='X-Epoch',
+            kind='X-Kind',
+            author=PersonStub(("Joe",), "Doe"),
+            **info_args("A book", "eng")
+        )
+
+    def test_multilingual_import(self):
+        BOOK_TEXT = """<utwor><opowiadanie><akap>A</akap></opowiadanie></utwor>"""
+
+        book1 = models.Book.from_text_and_meta(ContentFile(BOOK_TEXT), self.pol_info)
+        book2 = models.Book.from_text_and_meta(ContentFile(BOOK_TEXT), self.eng_info)
+
+        self.assertEqual(
+                set([b.language for b in models.Book.objects.all()]),
+                set(['pol', 'eng']),
+                'Books imported in wrong languages.'
+            )
+
+
 class BookImportGenerateTest(WLTestCase):
     def setUp(self):
         WLTestCase.setUp(self)
index edb29ab..90e7c12 100755 (executable)
@@ -12,7 +12,7 @@
 <dc:subject.genre xml:lang="pl">Fraszka</dc:subject.genre>
 
 <dc:description xml:lang="pl"></dc:description>
-<dc:identifier.url xml:lang="pl">http://wolnelektury.pl/lektura/fraszki</dc:identifier.url>
+<dc:identifier.url xml:lang="pl">http://wolnelektury.pl/katalog/lektura/fraszki</dc:identifier.url>
 <dc:source xml:lang="pl"></dc:source>
 <dc:rights xml:lang="pl">Domena publiczna - Jan Kochanowski zm. 1584</dc:rights>
 <dc:date.pd xml:lang="pl">1584</dc:date.pd>
index 324217a..ada2681 100644 (file)
@@ -4,19 +4,20 @@
 #
 from django.conf.urls.defaults import *
 from catalogue.feeds import AudiobookFeed
+from catalogue.models import Book
 
 
 urlpatterns = patterns('catalogue.views',
     url(r'^$', 'main_page', name='main_page'),
     url(r'^polki/(?P<shelf>[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'),
-    url(r'^polki/(?P<shelf>[a-zA-Z0-9-]+)/(?P<book>[a-zA-Z0-9-0-]+)/usun$', 'remove_from_shelf', name='remove_from_shelf'),
+    url(r'^polki/(?P<shelf>[a-zA-Z0-9-]+)/(?P<book>%s)/usun$' % Book.URLID_RE, 'remove_from_shelf', name='remove_from_shelf'),
     url(r'^polki/$', 'user_shelves', name='user_shelves'),
     url(r'^polki/(?P<slug>[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'),
     url(r'^polki/(?P<slug>[a-zA-Z0-9-]+)\.zip$', 'download_shelf', name='download_shelf'),
     url(r'^lektury/', 'book_list', name='book_list'),
     url(r'^audiobooki/$', 'audiobook_list', name='audiobook_list'),
     url(r'^daisy/$', 'daisy_list', name='daisy_list'),
-    url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)/polki/', 'book_sets', name='book_shelves'),
+    url(r'^lektura/(?P<book>%s)/polki/' % Book.URLID_RE, 'book_sets', name='book_shelves'),
     url(r'^polki/nowa/$', 'new_set', name='new_set'),
     url(r'^tags/$', 'tags_starting_with', name='hint'),
     url(r'^jtags/$', 'json_tags_starting_with', name='jhint'),
@@ -26,21 +27,21 @@ urlpatterns = patterns('catalogue.views',
     #url(r'^zip/pdf\.zip$', 'download_zip', {'format': 'pdf', 'slug': None}, 'download_zip_pdf'),
     #url(r'^zip/epub\.zip$', 'download_zip', {'format': 'epub', 'slug': None}, 'download_zip_epub'),
     #url(r'^zip/mobi\.zip$', 'download_zip', {'format': 'mobi', 'slug': None}, 'download_zip_mobi'),
-    #url(r'^zip/audiobook/(?P<slug>[a-zA-Z0-9-]+)\.zip', 'download_zip', {'format': 'audiobook'}, 'download_zip_audiobook'),
+    #url(r'^zip/audiobook/(?P<book>%s)\.zip' % Book.FILEID_RE, 'download_zip', {'format': 'audiobook'}, 'download_zip_audiobook'),
 
     # tools
     url(r'^zegar/$', 'clock', name='clock'),
 
     # Public interface. Do not change this URLs.
-    url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)\.html$', 'book_text', name='book_text'),
-    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-]+)/$',
+    url(r'^lektura/(?P<book>%s)\.html$' % Book.FILEID_RE, 'book_text', name='book_text'),
+    url(r'^lektura/(?P<book>%s)/$' % Book.URLID_RE, 'book_detail', name='book_detail'),
+    url(r'^lektura/(?P<book>%s)/motyw/(?P<theme_slug>[a-zA-Z0-9-]+)/$' % Book.URLID_RE,
         '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'^custompdf/(?P<slug>[a-zA-Z0-9-]+).pdf', 'download_custom_pdf'),
+    url(r'^custompdf/(?P<book_fileid>%s).pdf' % Book.FILEID_RE, 'download_custom_pdf'),
 )
 
index 3ffe9c0..acbd778 100644 (file)
@@ -66,11 +66,12 @@ class ORMDocProvider(DocProvider):
     def __init__(self, book):
         self.book = book
 
-    def by_slug(self, slug):
-        if slug == self.book.slug:
-            return self.book.xml_file
+    def by_slug_and_lang(self, slug, language):
+        if slug == self.book.slug and language == self.language:
+            return open(self.book.xml_file.path)
         else:
-            return type(self.book).objects.get(slug=slug).xml_file
+            return type(self.book).objects.get(
+                    slug=slug, language=language).xml_file
 
 
 class LockFile(object):
index 2bc1f10..0722f39 100644 (file)
@@ -204,9 +204,13 @@ def tagged_object_list(request, tags=''):
     )
 
 
-def book_fragments(request, book_slug, theme_slug):
-    book = get_object_or_404(models.Book, slug=book_slug)
-    book_tag = get_object_or_404(models.Tag, slug='l-' + book_slug, category='book')
+def book_fragments(request, book, theme_slug):
+    kwargs = models.Book.split_urlid(book)
+    if kwargs is None:
+        raise Http404
+    book = get_object_or_404(models.Book, **kwargs)
+
+    book_tag = book.book_tag()
     theme = get_object_or_404(models.Tag, slug=theme_slug, category='theme')
     fragments = models.Fragment.tagged.with_all([book_tag, theme])
 
@@ -215,11 +219,14 @@ def book_fragments(request, book_slug, theme_slug):
         context_instance=RequestContext(request))
 
 
-def book_detail(request, slug):
+def book_detail(request, book):
+    kwargs = models.Book.split_urlid(book)
+    if kwargs is None:
+        raise Http404
     try:
-        book = models.Book.objects.get(slug=slug)
+        book = models.Book.objects.get(**kwargs)
     except models.Book.DoesNotExist:
-        return pdcounter_views.book_stub_detail(request, slug)
+        return pdcounter_views.book_stub_detail(request, kwargs['slug'])
 
     book_tag = book.book_tag()
     tags = list(book.tags.filter(~Q(category='set')))
@@ -259,8 +266,12 @@ def book_detail(request, slug):
         context_instance=RequestContext(request))
 
 
-def book_text(request, slug):
-    book = get_object_or_404(models.Book, slug=slug)
+def book_text(request, book):
+    kwargs = models.Book.split_fileid(book)
+    if kwargs is None:
+        raise Http404
+    book = get_object_or_404(models.Book, **kwargs)
+
     if not book.has_html_file():
         raise Http404
     book_themes = {}
@@ -392,7 +403,7 @@ def books_starting_with(prefix):
 
 
 def find_best_matches(query, user=None):
-    """ Finds a Book, Tag, BookStub or Author best matching a query.
+    """ Finds a models.Book, Tag, models.BookStub or Author best matching a query.
 
     Returns a with:
       - zero elements when nothing is found,
@@ -494,11 +505,15 @@ def user_shelves(request):
             context_instance=RequestContext(request))
 
 @cache.never_cache
-def book_sets(request, slug):
+def book_sets(request, book):
     if not request.user.is_authenticated():
         return HttpResponse(_('<p>To maintain your shelves you need to be logged in.</p>'))
 
-    book = get_object_or_404(models.Book, slug=slug)
+    kwargs = models.Book.split_urlid(book)
+    if kwargs is None:
+        raise Http404
+    book = get_object_or_404(models.Book, **kwargs)
+
     user_sets = models.Tag.objects.filter(category='set', user=request.user)
     book_sets = book.tags.filter(category='set', user=request.user)
 
@@ -533,7 +548,11 @@ def book_sets(request, slug):
 @require_POST
 @cache.never_cache
 def remove_from_shelf(request, shelf, book):
-    book = get_object_or_404(models.Book, slug=book)
+    kwargs = models.Book.split_urlid(book)
+    if kwargs is None:
+        raise Http404
+    book = get_object_or_404(models.Book, **kwargs)
+
     shelf = get_object_or_404(models.Tag, slug=shelf, category='set', user=request.user)
 
     if shelf in book.tags:
@@ -586,15 +605,16 @@ def download_shelf(request, slug):
 
     already = set()
     for book in collect_books(models.Book.tagged.with_all(shelf)):
+        fileid = book.fileid()
         if 'pdf' in formats and book.pdf_file:
             filename = book.pdf_file.path
-            archive.write(filename, str('%s.pdf' % book.slug))
+            archive.write(filename, str('%s.pdf' % fileid))
         if 'mobi' in formats and book.mobi_file:
             filename = book.mobi_file.path
-            archive.write(filename, str('%s.mobi' % book.slug))
+            archive.write(filename, str('%s.mobi' % fileid))
         if book.root_ancestor not in already and 'epub' in formats and book.root_ancestor.epub_file:
             filename = book.root_ancestor.epub_file.path
-            archive.write(filename, str('%s.epub' % book.root_ancestor.slug))
+            archive.write(filename, str('%s.epub' % book.root_ancestor.fileid()))
             already.add(book.root_ancestor)
         if 'odt' in formats and book.has_media("odt"):
             for file in book.get_media("odt"):
@@ -602,7 +622,7 @@ def download_shelf(request, slug):
                 archive.write(filename, str('%s.odt' % slughifi(file.name)))
         if 'txt' in formats and book.txt_file:
             filename = book.txt_file.path
-            archive.write(filename, str('%s.txt' % book.slug))
+            archive.write(filename, str('%s.txt' % fileid))
     archive.close()
 
     response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed')
@@ -754,20 +774,26 @@ def tag_info(request, id):
     return HttpResponse(tag.description)
 
 
-def download_zip(request, format, slug):
+def download_zip(request, format, book=None):
+    kwargs = models.Book.split_fileid(book)
+
     url = None
     if format in ('pdf', 'epub', 'mobi'):
         url = models.Book.zip_format(format)
-    elif format == 'audiobook' and slug is not None:
-        book = models.Book.objects.get(slug=slug)
+    elif format == 'audiobook' and kwargs is not None:
+        book = get_object_or_404(models.Book, **kwargs)
         url = book.zip_audiobooks()
     else:
         raise Http404('No format specified for zip package')
     return HttpResponseRedirect(urlquote_plus(settings.MEDIA_URL + url, safe='/?='))
 
 
-def download_custom_pdf(request, slug):
-    book = models.Book.objects.get(slug=slug)
+def download_custom_pdf(request, book_fileid):
+    kwargs = models.Book.split_urlid(book)
+    if kwargs is None:
+        raise Http404
+    book = get_object_or_404(models.Book, **kwargs)
+
     if request.method == 'GET':
         form = forms.CustomPDFForm(request.GET)
         if form.is_valid():
@@ -777,7 +803,7 @@ def download_custom_pdf(request, slug):
             if not path.exists(pdf_file):
                 result = async_build_pdf.delay(book.id, cust, pdf_file)
                 result.wait()
-            return AttachmentHttpResponse(file_name=("%s.pdf" % book.slug), file_path=pdf_file, mimetype="application/pdf")
+            return AttachmentHttpResponse(file_name=("%s.pdf" % book_fileid), file_path=pdf_file, mimetype="application/pdf")
         else:
             raise Http404(_('Incorrect customization options for PDF'))
     else:
index e0b10f3..fc4ad66 100755 (executable)
@@ -44,7 +44,7 @@
     <div class='dictionary-note'>
     {{ obj.html|safe }}
     <div class='dictionary-note-source'>
-    (<a href='{% url book_text obj.book.slug %}#{{ obj.anchor }}'>{{ obj.book.pretty_title }}</a>)
+    (<a href='{% url book_text obj.book.fileid %}#{{ obj.anchor }}'>{{ obj.book.pretty_title }}</a>)
     </div>
     </div>
 {% endfor %}
index e7bbb48..eeba610 100644 (file)
@@ -3,11 +3,12 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.conf.urls.defaults import *
+from catalogue.models import Book
 
 urlpatterns = patterns('lesmianator.views',
     url(r'^$', 'main_page', name='lesmianator'),
     url(r'^wiersz/$', 'new_poem', name='new_poem'),
-    url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)/$', 'poem_from_book', name='poem_from_book'),
+    url(r'^lektura/(?P<book>%s)/$' % Book.URLID_RE, 'poem_from_book', name='poem_from_book'),
     url(r'^polka/(?P<shelf>[a-zA-Z0-9-]+)/$', 'poem_from_set', name='poem_from_set'),
     url(r'^wiersz/(?P<poem>[a-zA-Z0-9-]+)/$', 'get_poem', name='get_poem'),
 )
index 56acb57..cebcf8b 100644 (file)
@@ -1,5 +1,6 @@
 # Create your views here.
 
+from django.http import Http404
 from django.shortcuts import render_to_response, get_object_or_404
 from django.template import RequestContext
 from django.contrib.auth.decorators import login_required
@@ -34,8 +35,11 @@ def new_poem(request):
 
 
 @cache.never_cache
-def poem_from_book(request, slug):
-    book = get_object_or_404(Book, slug=slug)
+def poem_from_book(request, book):
+    kwargs = Book.split_urlid(book)
+    if kwargs is None:
+        raise Http404
+    book = get_object_or_404(Book, **kwargs)
     user = request.user if request.user.is_authenticated() else None
     text = Poem.write(Continuations.get(book))
     p = Poem(slug=get_random_hash(text), text=text, created_by=user)
index d7ba2c6..4077ffb 160000 (submodule)
@@ -1 +1 @@
-Subproject commit d7ba2c607dacf7a6136b83a1588b5adf2278ad46
+Subproject commit 4077ffb3cbd868df95239898563508b64e6d6ecf
index 45dfe37..5b5d4fe 100644 (file)
@@ -234,6 +234,8 @@ ALL_EPUB_ZIP = 'wolnelektury_pl_epub'
 ALL_PDF_ZIP = 'wolnelektury_pl_pdf'
 ALL_MOBI_ZIP = 'wolnelektury_pl_mobi'
 
+CATALOGUE_DEFAULT_LANGUAGE = 'pol'
+
 PAGINATION_INVALID_PAGE_RAISES_404 = True
 
 import djcelery
index 5708b91..2d83c73 100644 (file)
             <div id="toggle-description"><p></p></div>
         {% endif %}
         <div id="formats">
-            <p class="change-sets">{% trans "Put a book" %} <span><a href="{% url catalogue.views.book_sets book.slug %}" class="jqm-trigger">{% trans "on the shelf!" %}</a></span></p>
+            <p class="change-sets">{% trans "Put a book" %} <span><a href="{% url catalogue.views.book_sets book.urlid %}" class="jqm-trigger">{% trans "on the shelf!" %}</a></span></p>
             <div class="clearboth"></div>
             <div class="wrap">
                 {% if book.has_html_file %}
-                    <a class="online" href="{% url book_text book.slug %}">{% trans "Read online" %}</a>
+                    <a class="online" href="{% url book_text book.fileid %}">{% trans "Read online" %}</a>
                 {% endif %}
                 <div class="download">
                     {% if book.pdf_file %}
@@ -66,7 +66,7 @@
                        <br/><a href="#" id="custom-pdf-link">{% trans "Dowload customized PDF" %}</a>.
                    {% endif %}
                        <div style="display: none" class="custom-pdf">
-                         <form action="{% url catalogue.views.download_custom_pdf book.slug %}" method="GET">
+                         <form action="{% url catalogue.views.download_custom_pdf book.fileid %}" method="GET">
                            {{custom_pdf_form.as_p}}
                            <input type="submit" value="{% trans "Download" %}"/>
                          </form>
                 {% endif %}
             </ul>
             <p><a href="{{ book.xml_file.url }}">{% trans "View XML source" %}</a></p>
-            <p><a href="{% url poem_from_book book.slug %}">Miksuj ten utwór</a></p>
+            <p><a href="{% url poem_from_book book.urlid %}">Miksuj ten utwór</a></p>
         </div>
         <div id="themes-list">
             <h2>{% trans "Work's themes " %}</h2>
             <ul>
             {% for theme in book_themes %}
-                <li><a href="{% url book_fragments book.slug,theme.slug %}">{{ theme }} ({{ theme.count }})</a></li>
+                <li><a href="{% url book_fragments book.urlid theme.slug %}">{{ theme }} ({{ theme.count }})</a></li>
             {% endfor %}
             </ul>
         </div>
index 1eee61d..f151650 100644 (file)
@@ -9,7 +9,7 @@
 {% if not user.tag_set.count %}
     <p>{% trans "You do not have any shelves. You can create one below, if you want to."%}</p>
 {% else %}
-    <form action="{% url catalogue.views.book_sets book.slug %}" method="POST" id="putOnShelf" accept-charset="utf-8" class="cuteform">
+    <form action="{% url catalogue.views.book_sets book.urlid %}" method="POST" id="putOnShelf" accept-charset="utf-8" class="cuteform">
     <ol>
         <li>{{ form.set_ids }}</li>
         <li><input type="submit" value="{% trans "Put on the shelf!" %}"/></li>
index 8b36718..aced8bf 100644 (file)
@@ -1,7 +1,7 @@
 {% load i18n %}
 <div class="book">
     <div class="change-sets">
-        <a href="{% url catalogue.views.book_sets book.slug %}" class="jqm-trigger">{% trans "Put on the shelf!" %}</a>
+        <a href="{% url catalogue.views.book_sets book.urlid %}" class="jqm-trigger">{% trans "Put on the shelf!" %}</a>
     </div>
     {% if book.children.all|length %}
         <div class="book-parent-thumbnail"></div>
index fec9105..f845c62 100644 (file)
@@ -93,7 +93,7 @@
             {% for book in object_list %}
                 <li>
                     {% if user_is_owner %}
-                        <a href="{% url remove_from_shelf last_tag.slug book.slug %}" class="remove-from-shelf">{% trans "Delete" %}</a>
+                        <a href="{% url remove_from_shelf last_tag.slug book.urlid %}" class="remove-from-shelf">{% trans "Delete" %}</a>
                     {% endif %}
                     {{ book.short_html }}</li>
             {% endfor %}
index 03d1e9d..23e9437 100644 (file)
@@ -15,7 +15,7 @@
                 {# shelf or global mixing #}
                 <a href='' class='menu-link'>Twórzże się jeszcze raz!</a>
             {% else %}{% if book %} 
-                <a href='{% url poem_from_book book.slug %}' class='menu-link'>Twórzże się jeszcze raz!</a>
+                <a href='{% url poem_from_book book.urlid %}' class='menu-link'>Twórzże się jeszcze raz!</a>
             {% endif %}{% endif %}
             <span style='float: right'>Wolne Lektury przepuszczone przez mikser.</a>
         </div>
@@ -40,7 +40,7 @@
             {% if book %}
                 <p>Tekst powstał przez zmiksowanie utworu
                 <a href="{{ book.get_absolute_url }}">{{ book.title }}</a>.<br/>
-                <a href="{% url poem_from_book book.slug %}">Zmiksuj go ponownie</a>
+                <a href="{% url poem_from_book book.urlid %}">Zmiksuj go ponownie</a>
                 albo <a href="{% url lesmianator %}">zobacz</a>, co jeszcze możesz zamieszać.</p>
             {% else %}{% if books %}
                 <p>Tekst powstał przez zmiksowanie utworów:</p>