Merge master into img-playground. Image support with new management features. Missing...
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 14 Dec 2011 13:44:29 +0000 (14:44 +0100)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 14 Dec 2011 13:44:29 +0000 (14:44 +0100)
32 files changed:
1  2 
apps/catalogue/admin.py
apps/catalogue/migrations/0009_auto__add_imagechange__add_unique_imagechange_tree_revision__add_image.py
apps/catalogue/models/__init__.py
apps/catalogue/models/image.py
apps/catalogue/models/listeners.py
apps/catalogue/models/publish_log.py
apps/catalogue/templates/catalogue/book_html.html
apps/catalogue/templates/catalogue/image_list.html
apps/catalogue/templates/catalogue/image_short.html
apps/catalogue/templates/catalogue/image_table.html
apps/catalogue/templates/catalogue/upload_pdf.html
apps/catalogue/templatetags/book_list.py
apps/catalogue/templatetags/catalogue.py
apps/catalogue/urls.py
apps/catalogue/views.py
apps/wiki/admin.py
apps/wiki_img/forms.py
apps/wiki_img/models.py
apps/wiki_img/templates/wiki_img/document_details.html
apps/wiki_img/templates/wiki_img/document_details_base.html
apps/wiki_img/templates/wiki_img/document_details_readonly.html
apps/wiki_img/templates/wiki_img/save_dialog.html
apps/wiki_img/templates/wiki_img/tabs/history_view.html
apps/wiki_img/urls.py
apps/wiki_img/views.py
redakcja/settings/common.py
redakcja/settings/compress.py
redakcja/static/css/master.css
redakcja/static/js/wiki_img/loader.js
redakcja/static/js/wiki_img/loader_readonly.js
redakcja/static/js/wiki_img/wikiapi.js
redakcja/urls.py

diff --combined apps/catalogue/admin.py
index 0000000,a3faa98..8ba803e
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,13 +1,15 @@@
 -
+ from django.contrib import admin
+ from catalogue import models
+ class BookAdmin(admin.ModelAdmin):
+     prepopulated_fields = {'slug': ['title']}
+     search_fields = ['title']
+ admin.site.register(models.Book, BookAdmin)
+ admin.site.register(models.Chunk)
+ admin.site.register(models.Chunk.tag_model)
++
++admin.site.register(models.Image)
++admin.site.register(models.Image.tag_model)
index 0000000,0000000..4de5212
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,251 @@@
++# encoding: utf-8
++import datetime
++from south.db import db
++from south.v2 import SchemaMigration
++from django.db import models
++
++class Migration(SchemaMigration):
++
++    def forwards(self, orm):
++        
++        # Adding model 'ImageChange'
++        db.create_table('catalogue_imagechange', (
++            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
++            ('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
++            ('author_name', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
++            ('author_email', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
++            ('revision', self.gf('django.db.models.fields.IntegerField')(db_index=True)),
++            ('parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='children', null=True, blank=True, to=orm['catalogue.ImageChange'])),
++            ('merge_parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='merge_children', null=True, blank=True, to=orm['catalogue.ImageChange'])),
++            ('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
++            ('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, db_index=True)),
++            ('publishable', self.gf('django.db.models.fields.BooleanField')(default=False)),
++            ('tree', self.gf('django.db.models.fields.related.ForeignKey')(related_name='change_set', to=orm['catalogue.Image'])),
++            ('data', self.gf('django.db.models.fields.files.FileField')(max_length=100)),
++        ))
++        db.send_create_signal('catalogue', ['ImageChange'])
++
++        # Adding M2M table for field tags on 'ImageChange'
++        db.create_table('catalogue_imagechange_tags', (
++            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
++            ('imagechange', models.ForeignKey(orm['catalogue.imagechange'], null=False)),
++            ('imagetag', models.ForeignKey(orm['catalogue.imagetag'], null=False))
++        ))
++        db.create_unique('catalogue_imagechange_tags', ['imagechange_id', 'imagetag_id'])
++
++        # Adding unique constraint on 'ImageChange', fields ['tree', 'revision']
++        db.create_unique('catalogue_imagechange', ['tree_id', 'revision'])
++
++        # Adding model 'ImagePublishRecord'
++        db.create_table('catalogue_imagepublishrecord', (
++            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
++            ('image', self.gf('django.db.models.fields.related.ForeignKey')(related_name='publish_log', to=orm['catalogue.Image'])),
++            ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
++            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
++            ('change', self.gf('django.db.models.fields.related.ForeignKey')(related_name='publish_log', to=orm['catalogue.ImageChange'])),
++        ))
++        db.send_create_signal('catalogue', ['ImagePublishRecord'])
++
++        # Adding model 'ImageTag'
++        db.create_table('catalogue_imagetag', (
++            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
++            ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
++            ('slug', self.gf('django.db.models.fields.SlugField')(db_index=True, max_length=64, unique=True, null=True, blank=True)),
++            ('ordering', self.gf('django.db.models.fields.IntegerField')()),
++        ))
++        db.send_create_signal('catalogue', ['ImageTag'])
++
++        # Adding model 'Image'
++        db.create_table('catalogue_image', (
++            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
++            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
++            ('image', self.gf('django.db.models.fields.files.FileField')(max_length=100)),
++            ('title', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
++            ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=50, db_index=True)),
++            ('public', self.gf('django.db.models.fields.BooleanField')(default=True, db_index=True)),
++            ('_short_html', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
++            ('_new_publishable', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)),
++            ('_published', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)),
++            ('_changed', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)),
++            ('stage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ImageTag'], null=True, blank=True)),
++            ('head', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['catalogue.ImageChange'], null=True, blank=True)),
++            ('creator', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='created_image', null=True, to=orm['auth.User'])),
++        ))
++        db.send_create_signal('catalogue', ['Image'])
++
++
++    def backwards(self, orm):
++        
++        # Removing unique constraint on 'ImageChange', fields ['tree', 'revision']
++        db.delete_unique('catalogue_imagechange', ['tree_id', 'revision'])
++
++        # Deleting model 'ImageChange'
++        db.delete_table('catalogue_imagechange')
++
++        # Removing M2M table for field tags on 'ImageChange'
++        db.delete_table('catalogue_imagechange_tags')
++
++        # Deleting model 'ImagePublishRecord'
++        db.delete_table('catalogue_imagepublishrecord')
++
++        # Deleting model 'ImageTag'
++        db.delete_table('catalogue_imagetag')
++
++        # Deleting model 'Image'
++        db.delete_table('catalogue_image')
++
++
++    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': "['title', 'slug']", 'object_name': 'Book'},
++            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
++            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
++            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
++            '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
++            'dc_slug': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'}),
++            'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
++            'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
++            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
++            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
++            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
++        },
++        'catalogue.bookpublishrecord': {
++            'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
++            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
++            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
++            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
++        },
++        'catalogue.chunk': {
++            'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
++            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
++            '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
++            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
++            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
++            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_chunk'", 'null': 'True', 'to': "orm['auth.User']"}),
++            'gallery_start': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True', 'blank': 'True'}),
++            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
++            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'number': ('django.db.models.fields.IntegerField', [], {}),
++            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
++            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
++            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
++            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
++        },
++        'catalogue.chunkchange': {
++            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
++            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
++            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
++            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
++            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
++            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
++            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
++            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
++            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
++            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
++            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
++            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
++            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
++        },
++        'catalogue.chunkpublishrecord': {
++            'Meta': {'object_name': 'ChunkPublishRecord'},
++            'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
++            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
++            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
++        },
++        'catalogue.chunktag': {
++            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
++            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
++            'ordering': ('django.db.models.fields.IntegerField', [], {}),
++            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
++        },
++        'catalogue.image': {
++            'Meta': {'ordering': "['title']", 'object_name': 'Image'},
++            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
++            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
++            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
++            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
++            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_image'", 'null': 'True', 'to': "orm['auth.User']"}),
++            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ImageChange']", 'null': 'True', 'blank': 'True'}),
++            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'image': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
++            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
++            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
++            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ImageTag']", 'null': 'True', 'blank': 'True'}),
++            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
++            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
++        },
++        'catalogue.imagechange': {
++            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ImageChange'},
++            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
++            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
++            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
++            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
++            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
++            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
++            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ImageChange']"}),
++            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ImageChange']"}),
++            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
++            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
++            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ImageTag']"}),
++            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Image']"})
++        },
++        'catalogue.imagepublishrecord': {
++            'Meta': {'ordering': "['-timestamp']", 'object_name': 'ImagePublishRecord'},
++            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ImageChange']"}),
++            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'image': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Image']"}),
++            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
++            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
++        },
++        'catalogue.imagetag': {
++            'Meta': {'ordering': "['ordering']", 'object_name': 'ImageTag'},
++            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
++            'ordering': ('django.db.models.fields.IntegerField', [], {}),
++            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
++        },
++        '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 0000000,6161807..82e1c11
mode 000000,100755..100755
--- /dev/null
@@@ -1,0 -1,18 +1,19 @@@
+ # -*- coding: utf-8 -*-
+ #
+ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+ #
+ from catalogue.models.chunk import Chunk
++from catalogue.models.image import Image
+ from catalogue.models.publish_log import BookPublishRecord, ChunkPublishRecord
+ from catalogue.models.book import Book
+ from catalogue.models.listeners import *
+ from django.contrib.auth.models import User as AuthUser
+ class User(AuthUser):
+     class Meta:
+         proxy = True
+     def __unicode__(self):
+         return "%s %s" % (self.first_name, self.last_name)
index 0000000,0000000..53f8830
new file mode 100755 (executable)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,95 @@@
++# -*- coding: utf-8 -*-
++#
++# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
++# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
++#
++from django.conf import settings
++from django.db import models
++from django.template.loader import render_to_string
++from django.utils.translation import ugettext_lazy as _
++from catalogue.helpers import cached_in_field
++from catalogue.tasks import refresh_instance
++from dvcs import models as dvcs_models
++
++
++class Image(dvcs_models.Document):
++    """ An editable chunk of text. Every Book text is divided into chunks. """
++    REPO_PATH = settings.CATALOGUE_IMAGE_REPO_PATH
++
++    image = models.FileField(_('image'), upload_to='catalogue/images')
++    title = models.CharField(_('title'), max_length=255, blank=True)
++    slug = models.SlugField(_('slug'), unique=True)
++    public = models.BooleanField(_('public'), default=True, db_index=True)
++
++    # cache
++    _short_html = models.TextField(null=True, blank=True, editable=False)
++    _new_publishable = models.NullBooleanField(editable=False)
++    _published = models.NullBooleanField(editable=False)
++    _changed = models.NullBooleanField(editable=False)
++
++    class Meta:
++        app_label = 'catalogue'
++        ordering = ['title']
++        verbose_name = _('image')
++        verbose_name_plural = _('images')
++        permissions = [('can_pubmark_image', 'Can mark images for publishing')]
++
++    # Representing
++    # ============
++
++    def __unicode__(self):
++        return self.title
++
++    @models.permalink
++    def get_absolute_url(self):
++        return ("wiki_img_editor", [self.slug])
++
++    # State & cache
++    # =============
++
++    def accessible(self, request):
++        return self.public or request.user.is_authenticated()
++
++    def is_new_publishable(self):
++        change = self.publishable()
++        if not change:
++            return False
++        return change.publish_log.exists()
++    new_publishable = cached_in_field('_new_publishable')(is_new_publishable)
++
++    def is_published(self):
++        return self.publish_log.exists()
++    published = cached_in_field('_published')(is_published)
++
++    def is_changed(self):
++        if self.head is None:
++            return False
++        return not self.head.publishable
++    changed = cached_in_field('_changed')(is_changed)
++
++    @cached_in_field('_short_html')
++    def short_html(self):
++        return render_to_string(
++                    'catalogue/image_short.html', {'image': self})
++
++    def refresh(self):
++        """This should be done offline."""
++        self.short_html
++        self.single
++        self.new_publishable
++        self.published
++
++    def touch(self):
++        update = {
++            "_changed": self.is_changed(),
++            "_short_html": None,
++            "_new_publishable": self.is_new_publishable(),
++            "_published": self.is_published(),
++        }
++        Image.objects.filter(pk=self.pk).update(**update)
++        refresh_instance(self)
++
++    def refresh(self):
++        """This should be done offline."""
++        self.changed
++        self.short_html
index 0000000,532f1e7..de1387e
mode 000000,100755..100755
--- /dev/null
@@@ -1,0 -1,53 +1,58 @@@
 -from catalogue.models import Book, Chunk
+ # -*- coding: utf-8 -*-
+ #
+ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+ #
+ from django.contrib.auth.models import User
+ from django.db import models
++from catalogue.models import Book, Chunk, Image
+ from catalogue.signals import post_publish
+ from dvcs.signals import post_publishable
+ def book_changed(sender, instance, created, **kwargs):
+     instance.touch()
+     for c in instance:
+         c.touch()
+ models.signals.post_save.connect(book_changed, sender=Book)
+ def chunk_changed(sender, instance, created, **kwargs):
+     instance.book.touch()
+     instance.touch()
+ models.signals.post_save.connect(chunk_changed, sender=Chunk)
++def image_changed(sender, instance, created, **kwargs):
++    instance.touch()
++models.signals.post_save.connect(image_changed, sender=Image)
++
++
+ def user_changed(sender, instance, *args, **kwargs):
+     books = set()
+     for c in instance.chunk_set.all():
+         books.add(c.book)
+         c.touch()
+     for b in books:
+         b.touch()
+ models.signals.post_save.connect(user_changed, sender=User)
+ def publish_listener(sender, *args, **kwargs):
+     sender.book.touch()
+     for c in sender.book:
+         c.touch()
+ post_publish.connect(publish_listener)
+ def publishable_listener(sender, *args, **kwargs):
+     sender.tree.touch()
+     sender.tree.book.touch()
+ post_publishable.connect(publishable_listener)
+ def listener_create(sender, instance, created, **kwargs):
+     if created:
+         instance.chunk_set.create(number=1, slug='1')
+ models.signals.post_save.connect(listener_create, sender=Book)
index 0000000,f422e37..6cc86d0
mode 000000,100755..100755
--- /dev/null
@@@ -1,0 -1,39 +1,54 @@@
 -from catalogue.models import Chunk
+ # -*- coding: utf-8 -*-
+ #
+ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+ #
+ from django.contrib.auth.models import User
+ from django.db import models
+ from django.utils.translation import ugettext_lazy as _
++from catalogue.models import Chunk, Image
+ class BookPublishRecord(models.Model):
+     """
+         A record left after publishing a Book.
+     """
+     book = models.ForeignKey('Book', verbose_name=_('book'), related_name='publish_log')
+     timestamp = models.DateTimeField(_('time'), auto_now_add=True)
+     user = models.ForeignKey(User, verbose_name=_('user'))
+     class Meta:
+         app_label = 'catalogue'
+         ordering = ['-timestamp']
+         verbose_name = _('book publish record')
+         verbose_name = _('book publish records')
+ class ChunkPublishRecord(models.Model):
+     """
+         BookPublishRecord details for each Chunk.
+     """
+     book_record = models.ForeignKey(BookPublishRecord, verbose_name=_('book publish record'))
+     change = models.ForeignKey(Chunk.change_model, related_name='publish_log', verbose_name=_('change'))
+     class Meta:
+         app_label = 'catalogue'
+         verbose_name = _('chunk publish record')
+         verbose_name = _('chunk publish records')
++
++
++class ImagePublishRecord(models.Model):
++    """A record left after publishing an Image."""
++
++    image = models.ForeignKey(Image, verbose_name=_('image'), related_name='publish_log')
++    timestamp = models.DateTimeField(_('time'), auto_now_add=True)
++    user = models.ForeignKey(User, verbose_name=_('user'))
++    change = models.ForeignKey(Image.change_model, related_name='publish_log', verbose_name=_('change'))
++
++    class Meta:
++        app_label = 'catalogue'
++        ordering = ['-timestamp']
++        verbose_name = _('image publish record')
++        verbose_name = _('image publish records')
index 0000000,0000000..af4cfa7
new file mode 100755 (executable)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,30 @@@
++{% load i18n %}
++{% load compressed %}
++<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
++    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
++<html xmlns="http://www.w3.org/1999/xhtml">
++    <head>
++        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
++        <title>{{ book.title }}</title>
++    </head>
++    <body>
++        <div id="menu">
++            <ul>
++                <li><a href="#toc">{% trans "Table of contents" %}</a></li>
++                <li><a href="#nota_red">{% trans "Edit. note" %}</a></li>
++                <li><a href="#info">{% trans "Infobox" %}</a></li>
++            </ul>
++        </div>
++        <div id="info">
++            {#% book_info book %#}
++        </div>
++        <div id="header">
++            <div id="logo">
++                <a href="/"><img src="http://static.wolnelektury.pl/img/logo.png" alt="WolneLektury.pl - logo" /></a>
++            </div>
++        </div>
++
++        {{ html|safe }}
++
++    </body>
++</html>
index 0000000,0000000..3ff75bc
new file mode 100755 (executable)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,9 @@@
++{% extends "catalogue/base.html" %}
++
++{% load i18n %}
++{% load catalogue book_list %}
++
++
++{% block content %}
++    {% image_list %}
++{% endblock content %}
index 0000000,0000000..2e2b386
new file mode 100755 (executable)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,18 @@@
++{% load i18n %}
++
++<tr>
++    <td><a href="{% url catalogue_image image.slug %}" title='{% trans "Image settings" %}'>[B]</a></td>
++    <td><a target="_blank"
++                href="{% url wiki_img_editor image.slug %}">
++                {{ image.title }}</a></td>
++    <td>{% if image.stage %}
++        {{ image.stage }}
++    {% else %}–
++    {% endif %}</td>
++    <td class='user-column'>{% if image.user %}<a href="{% url catalogue_user image.user.username %}">{{ image.user.first_name }} {{ image.user.last_name }}</a>{% endif %}</td>
++    <td>
++        {% if image.published %}P{% endif %}
++        {% if image.new_publishable %}p{% endif %}
++        {% if image.changed %}+{% endif %}
++    </td>
++</tr>
index 0000000,0000000..68293e7
new file mode 100755 (executable)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,69 @@@
++{% load i18n %}
++{% load pagination_tags %}
++
++
++<form name='filter' action=''>
++<input type='hidden' name="title" value="{{ request.GET.title }}" />
++<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
++{% if not viewed_user %}
++    <input type='hidden' name="user" value="{{ request.GET.user }}" />
++{% endif %}
++<input type='hidden' name="status" value="{{ request.GET.status }}" />
++</form>
++
++<table id="file-list"{% if viewed_user %} class="book-list-user"{% endif %}>
++    <thead><tr>
++        <th></th>
++        <th class='book-search-column'>
++            <form>
++            <input title='{% trans "Search in book titles" %}' name="title"
++                class='text-filter' value="{{ request.GET.title }}" />
++            </form>
++        </th>
++        <th><select name="stage" class="filter">
++            <option value=''>- {% trans "stage" %} -</option>
++            <option {% if request.GET.stage == '-' %}selected="selected"
++                    {% endif %}value="-">- {% trans "none" %} -</option>
++            {% for stage in stages %}
++                <option {% if request.GET.stage == stage.slug %}selected="selected"
++                    {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
++            {% endfor %}
++        </select></th>
++
++        {% if not viewed_user %}
++            <th><select name="user" class="filter">
++                <option value=''>- {% trans "editor" %} -</option>
++                <option {% if request.GET.user == '-' %}selected="selected"
++                        {% endif %}value="-">- {% trans "none" %} -</option>
++                {% for user in users %}
++                    <option {% if request.GET.user == user.username %}selected="selected"
++                        {% endif %}value="{{ user.username }}">{{ user.first_name }} {{ user.last_name }} ({{ user.count }})</option>
++                {% endfor %}
++            </select></th>
++        {% endif %}
++
++        <th><select name="status" class="filter">
++            <option value=''>- {% trans "status" %} -</option>
++            {% for state, label in states %}
++                <option {% if request.GET.status == state %}selected="selected"
++                        {% endif %}value='{{ state }}'>{{ label }}</option>
++            {% endfor %}
++        </select></th>
++
++    </tr></thead>
++
++    {% with cnt=objects|length %}
++    {% autopaginate objects 100 %}
++    <tbody>
++    {% for item in objects %}
++        {{ item.short_html|safe }}
++    {% endfor %}
++    <tr><th class='paginator' colspan="5">
++        {% paginate %}
++        {% blocktrans count c=cnt %}{{c}} image{% plural %}{{c}} images{% endblocktrans %}</th></tr>
++    </tbody>
++    {% endwith %}
++</table>
++{% if not objects %}
++    <p>{% trans "No images found." %}</p>
++{% endif %}
index 0000000,0000000..a9670e4
new file mode 100755 (executable)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,17 @@@
++{% extends "catalogue/base.html" %}
++{% load i18n %}
++
++
++{% block content %}
++
++
++<h2>{% trans "PDF file upload" %}</h2>
++
++<form enctype="multipart/form-data" method="POST" action="">
++{% csrf_token %}
++{{ form.as_p }}
++<p><button type="submit">{% trans "Upload" %}</button></p>
++</form>
++
++
++{% endblock content %}
index 0000000,f7e7047..5e18b7e
mode 000000,100755..100755
--- /dev/null
@@@ -1,0 -1,140 +1,195 @@@
 -from catalogue.models import Chunk
+ from __future__ import absolute_import
+ from re import split
+ from django.db.models import Q, Count
+ from django import template
+ from django.utils.translation import ugettext_lazy as _
+ from django.contrib.auth.models import User
++from catalogue.models import Chunk, Image
+ register = template.Library()
+ class ChunksList(object):
+     def __init__(self, chunk_qs):
+         #self.chunk_qs = chunk_qs#.annotate(
+             #book_length=Count('book__chunk')).select_related(
+             #'book')#, 'stage__name',
+             #'user')
+         self.chunk_qs = chunk_qs.select_related('book__hidden')
+         self.book_qs = chunk_qs.values('book_id')
+     def __getitem__(self, key):
+         if isinstance(key, slice):
+             return self.get_slice(key)
+         elif isinstance(key, int):
+             return self.get_slice(slice(key, key+1))[0]
+         else:
+             raise TypeError('Unsupported list index. Must be a slice or an int.')
+     def __len__(self):
+         return self.book_qs.count()
+     def get_slice(self, slice_):
+         book_ids = [x['book_id'] for x in self.book_qs[slice_]]
+         chunk_qs = self.chunk_qs.filter(book__in=book_ids)
+         chunks_list = []
+         book = None
+         for chunk in chunk_qs:
+             if chunk.book != book:
+                 book = chunk.book
+                 chunks_list.append(ChoiceChunks(book, [chunk]))
+             else:
+                 chunks_list[-1].chunks.append(chunk)
+         return chunks_list
+ class ChoiceChunks(object):
+     """
+         Associates the given chunks iterable for a book.
+     """
+     chunks = None
+     def __init__(self, book, chunks):
+         self.book = book
+         self.chunks = chunks
+ def foreign_filter(qs, value, filter_field, model, model_field='slug', unset='-'):
+     if value == unset:
+         return qs.filter(**{filter_field: None})
+     if not value:
+         return qs
+     try:
+         obj = model._default_manager.get(**{model_field: value})
+     except model.DoesNotExist:
+         return qs.none()
+     else:
+         return qs.filter(**{filter_field: obj})
+ def search_filter(qs, value, filter_fields):
+     if not value:
+         return qs
+     q = Q(**{"%s__icontains" % filter_fields[0]: value})
+     for field in filter_fields[1:]:
+         q |= Q(**{"%s__icontains" % field: value})
+     return qs.filter(q)
+ _states = [
+         ('publishable', _('publishable'), Q(book___new_publishable=True)),
+         ('changed', _('changed'), Q(_changed=True)),
+         ('published', _('published'), Q(book___published=True)),
+         ('unpublished', _('unpublished'), Q(book___published=False)),
+         ('empty', _('empty'), Q(head=None)),
+     ]
+ _states_options = [s[:2] for s in _states]
+ _states_dict = dict([(s[0], s[2]) for s in _states])
+ def document_list_filter(request, **kwargs):
+     def arg_or_GET(field):
+         return kwargs.get(field, request.GET.get(field))
+     if arg_or_GET('all'):
+         chunks = Chunk.objects.all()
+     else:
+         chunks = Chunk.visible_objects.all()
+     chunks = chunks.order_by('book__title', 'book', 'number')
+     if not request.user.is_authenticated():
+         chunks = chunks.filter(book__public=True)
+     state = arg_or_GET('status')
+     if state in _states_dict:
+         chunks = chunks.filter(_states_dict[state])
+     chunks = foreign_filter(chunks, arg_or_GET('user'), 'user', User, 'username')
+     chunks = foreign_filter(chunks, arg_or_GET('stage'), 'stage', Chunk.tag_model, 'slug')
+     chunks = search_filter(chunks, arg_or_GET('title'), ['book__title', 'title'])
+     return chunks
+ @register.inclusion_tag('catalogue/book_list/book_list.html', takes_context=True)
+ def book_list(context, user=None):
+     request = context['request']
+     if user:
+         filters = {"user": user}
+         new_context = {"viewed_user": user}
+     else:
+         filters = {}
+         new_context = {"users": User.objects.annotate(
+                 count=Count('chunk')).filter(count__gt=0).order_by(
+                 '-count', 'last_name', 'first_name')}
+     new_context.update({
+         "filters": True,
+         "request": request,
+         "books": ChunksList(document_list_filter(request, **filters)),
+         "stages": Chunk.tag_model.objects.all(),
+         "states": _states_options,
+     })
+     return new_context
++
++
++
++_image_states = [
++        ('publishable', _('publishable'), Q(_new_publishable=True)),
++        ('changed', _('changed'), Q(_changed=True)),
++        ('published', _('published'), Q(_published=True)),
++        ('unpublished', _('unpublished'), Q(_published=False)),
++        ('empty', _('empty'), Q(head=None)),
++    ]
++_image_states_options = [s[:2] for s in _states]
++_image_states_dict = dict([(s[0], s[2]) for s in _states])
++
++def image_list_filter(request, **kwargs):
++
++    def arg_or_GET(field):
++        return kwargs.get(field, request.GET.get(field))
++
++    images = Image.objects.all()
++
++    if not request.user.is_authenticated():
++        images = images.filter(public=True)
++
++    state = arg_or_GET('status')
++    if state in _image_states_dict:
++        images = images.filter(_image_states_dict[state])
++
++    images = foreign_filter(images, arg_or_GET('user'), 'user', User, 'username')
++    images = foreign_filter(images, arg_or_GET('stage'), 'stage', Image.tag_model, 'slug')
++    images = search_filter(images, arg_or_GET('title'), ['title', 'title'])
++    return images
++
++
++@register.inclusion_tag('catalogue/image_table.html', takes_context=True)
++def image_list(context, user=None):
++    request = context['request']
++
++    if user:
++        filters = {"user": user}
++        new_context = {"viewed_user": user}
++    else:
++        filters = {}
++        new_context = {"users": User.objects.annotate(
++                count=Count('chunk')).filter(count__gt=0).order_by(
++                '-count', 'last_name', 'first_name')}
++
++    new_context.update({
++        "filters": True,
++        "request": request,
++        "objects": image_list_filter(request, **filters),
++        "stages": Image.tag_model.objects.all(),
++        "states": _image_states_options,
++    })
++
++    return new_context
index 0000000,3cc7210..0b57b49
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,43 +1,44 @@@
+ from __future__ import absolute_import
+ from django.core.urlresolvers import reverse
+ from django import template
+ from django.utils.translation import ugettext as _
+ register = template.Library()
+ class Tab(object):
+     slug = None
+     caption = None
+     url = None
+     def __init__(self, slug, caption, url):
+         self.slug = slug
+         self.caption = caption
+         self.url = url
+ @register.inclusion_tag("catalogue/main_tabs.html", takes_context=True)
+ def main_tabs(context):
+     active = getattr(context['request'], 'catalogue_active_tab', None)
+     tabs = []
+     user = context['user']
+     tabs.append(Tab('my', _('My page'), reverse("catalogue_user")))
+     tabs.append(Tab('activity', _('Activity'), reverse("catalogue_activity")))
+     tabs.append(Tab('all', _('All'), reverse("catalogue_document_list")))
++    tabs.append(Tab('images', _('Images'), reverse("catalogue_image_list")))
+     tabs.append(Tab('users', _('Users'), reverse("catalogue_users")))
+     if user.has_perm('catalogue.add_book'):
+         tabs.append(Tab('create', _('Add'), reverse("catalogue_create_missing")))
+         tabs.append(Tab('upload', _('Upload'), reverse("catalogue_upload")))
+     return {"tabs": tabs, "active_tab": active}
+ @register.filter
+ def nice_name(user):
+     return user.get_full_name() or user.username
diff --combined apps/catalogue/urls.py
index 0000000,ab9b570..621eb12
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,41 +1,44 @@@
+ # -*- coding: utf-8
+ from django.conf.urls.defaults import *
+ from django.views.generic.simple import redirect_to
+ urlpatterns = patterns('catalogue.views',
+     url(r'^$', redirect_to, {'url': 'catalogue/'}),
++    url(r'^images/$', 'image_list', name='catalogue_image_list'),
++    url(r'^image/(?P<slug>[^/]+)/$', 'image', name="catalogue_image"),
++
+     url(r'^catalogue/$', 'document_list', name='catalogue_document_list'),
+     url(r'^user/$', 'my', name='catalogue_user'),
+     url(r'^user/(?P<username>[^/]+)/$', 'user', name='catalogue_user'),
+     url(r'^users/$', 'users', name='catalogue_users'),
+     url(r'^activity/$', 'activity', name='catalogue_activity'),
+     url(r'^activity/(?P<isodate>\d{4}-\d{2}-\d{2})/$', 
+         'activity', name='catalogue_activity'),
+     url(r'^upload/$',
+         'upload', name='catalogue_upload'),
+     url(r'^create/(?P<slug>[^/]*)/',
+         'create_missing', name='catalogue_create_missing'),
+     url(r'^create/',
+         'create_missing', name='catalogue_create_missing'),
+     url(r'^book/(?P<slug>[^/]+)/publish$', 'publish', name="catalogue_publish"),
+     #url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$', 'publish', name="catalogue_publish"),
+     url(r'^book/(?P<slug>[^/]+)/$', 'book', name="catalogue_book"),
+     url(r'^book/(?P<slug>[^/]+)/xml$', 'book_xml', name="catalogue_book_xml"),
+     url(r'^book/(?P<slug>[^/]+)/txt$', 'book_txt', name="catalogue_book_txt"),
+     url(r'^book/(?P<slug>[^/]+)/html$', 'book_html', name="catalogue_book_html"),
+     url(r'^book/(?P<slug>[^/]+)/epub$', 'book_epub', name="catalogue_book_epub"),
+     url(r'^book/(?P<slug>[^/]+)/pdf$', 'book_pdf', name="catalogue_book_pdf"),
+     url(r'^chunk_add/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
+         'chunk_add', name="catalogue_chunk_add"),
+     url(r'^chunk_edit/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
+         'chunk_edit', name="catalogue_chunk_edit"),
+     url(r'^book_append/(?P<slug>[^/]+)/$',
+         'book_append', name="catalogue_book_append"),
+ )
diff --combined apps/catalogue/views.py
index 0000000,3c0e70f..3c37ee6
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,445 +1,482 @@@
+ from datetime import datetime, date, timedelta
+ import logging
+ import os
+ from StringIO import StringIO
+ from urllib import unquote
+ from urlparse import urlsplit, urlunsplit
+ from django.contrib import auth
+ from django.contrib.auth.models import User
+ from django.contrib.auth.decorators import login_required, permission_required
+ from django.core.urlresolvers import reverse
+ from django.db.models import Count, Q
+ from django import http
+ from django.http import Http404, HttpResponse, HttpResponseForbidden
+ from django.shortcuts import get_object_or_404, render
+ from django.utils.encoding import iri_to_uri
+ from django.utils.http import urlquote_plus
+ from django.utils.translation import ugettext_lazy as _
+ from django.views.decorators.http import require_POST
+ from django.views.generic.simple import direct_to_template
+ from apiclient import NotAuthorizedError
+ from catalogue import forms
+ from catalogue import helpers
+ from catalogue.helpers import active_tab
+ from catalogue.models import Book, Chunk, BookPublishRecord, ChunkPublishRecord
+ from catalogue.tasks import publishable_error
+ #
+ # Quick hack around caching problems, TODO: use ETags
+ #
+ from django.views.decorators.cache import never_cache
+ logger = logging.getLogger("fnp.catalogue")
+ @active_tab('all')
+ @never_cache
+ def document_list(request):
+     return render(request, 'catalogue/document_list.html')
++@active_tab('images')
++@never_cache
++def image_list(request, user=None):
++    return render(request, 'catalogue/image_list.html')
++
++
+ @never_cache
+ def user(request, username):
+     user = get_object_or_404(User, username=username)
+     return render(request, 'catalogue/user_page.html', {"viewed_user": user})
+ @login_required
+ @active_tab('my')
+ @never_cache
+ def my(request):
+     return render(request, 'catalogue/my_page.html', {
+         'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
+                         key=lambda x: x[1]['time'], reverse=True),
+         "logout_to": '/',
+         })
+ @active_tab('users')
+ def users(request):
+     return direct_to_template(request, 'catalogue/user_list.html', extra_context={
+         'users': User.objects.all().annotate(count=Count('chunk')).order_by(
+             '-count', 'last_name', 'first_name'),
+     })
+ @active_tab('activity')
+ def activity(request, isodate=None):
+     today = date.today()
+     try:
+         day = helpers.parse_isodate(isodate)
+     except ValueError:
+         day = today
+     if day > today:
+         raise Http404
+     if day != today:
+         next_day = day + timedelta(1)
+     prev_day = day - timedelta(1)
+     return render(request, 'catalogue/activity.html', locals())
+ @never_cache
+ def logout_then_redirect(request):
+     auth.logout(request)
+     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
+ @permission_required('catalogue.add_book')
+ @active_tab('create')
+ def create_missing(request, slug=None):
+     if slug is None:
+         slug = ''
+     slug = slug.replace(' ', '-')
+     if request.method == "POST":
+         form = forms.DocumentCreateForm(request.POST, request.FILES)
+         if form.is_valid():
+             
+             if request.user.is_authenticated():
+                 creator = request.user
+             else:
+                 creator = None
+             book = Book.create(
+                 text=form.cleaned_data['text'],
+                 creator=creator,
+                 slug=form.cleaned_data['slug'],
+                 title=form.cleaned_data['title'],
+                 gallery=form.cleaned_data['gallery'],
+             )
+             return http.HttpResponseRedirect(reverse("catalogue_book", args=[book.slug]))
+     else:
+         form = forms.DocumentCreateForm(initial={
+                 "slug": slug,
+                 "title": slug.replace('-', ' ').title(),
+                 "gallery": slug,
+         })
+     return direct_to_template(request, "catalogue/document_create_missing.html", extra_context={
+         "slug": slug,
+         "form": form,
+         "logout_to": '/',
+     })
+ @permission_required('catalogue.add_book')
+ @active_tab('upload')
+ def upload(request):
+     if request.method == "POST":
+         form = forms.DocumentsUploadForm(request.POST, request.FILES)
+         if form.is_valid():
+             import slughifi
+             if request.user.is_authenticated():
+                 creator = request.user
+             else:
+                 creator = None
+             zip = form.cleaned_data['zip']
+             skipped_list = []
+             ok_list = []
+             error_list = []
+             slugs = {}
+             existing = [book.slug for book in Book.objects.all()]
+             for filename in zip.namelist():
+                 if filename[-1] == '/':
+                     continue
+                 title = os.path.basename(filename)[:-4]
+                 slug = slughifi(title)
+                 if not (slug and filename.endswith('.xml')):
+                     skipped_list.append(filename)
+                 elif slug in slugs:
+                     error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
+                 elif slug in existing:
+                     error_list.append((filename, slug, _('Slug already used in repository.')))
+                 else:
+                     try:
+                         zip.read(filename).decode('utf-8') # test read
+                         ok_list.append((filename, slug, title))
+                     except UnicodeDecodeError:
+                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
+                     slugs[slug] = filename
+             if not error_list:
+                 for filename, slug, title in ok_list:
+                     book = Book.create(
+                         text=zip.read(filename).decode('utf-8'),
+                         creator=creator,
+                         slug=slug,
+                         title=title,
+                     )
+             return direct_to_template(request, "catalogue/document_upload.html", extra_context={
+                 "form": form,
+                 "ok_list": ok_list,
+                 "skipped_list": skipped_list,
+                 "error_list": error_list,
+                 "logout_to": '/',
+             })
+     else:
+         form = forms.DocumentsUploadForm()
+     return direct_to_template(request, "catalogue/document_upload.html", extra_context={
+         "form": form,
+         "logout_to": '/',
+     })
+ @never_cache
+ def book_xml(request, slug):
+     book = get_object_or_404(Book, slug=slug)
+     if not book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     xml = book.materialize()
+     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
+     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
+     return response
+ @never_cache
+ def book_txt(request, slug):
+     book = get_object_or_404(Book, slug=slug)
+     if not book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     xml = book.materialize()
+     output = StringIO()
+     # errors?
+     import librarian.text
+     librarian.text.transform(StringIO(xml), output)
+     text = output.getvalue()
+     response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
+     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
+     return response
+ @never_cache
+ def book_html(request, slug):
+     book = get_object_or_404(Book, slug=slug)
+     if not book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     xml = book.materialize()
+     output = StringIO()
+     # errors?
+     import librarian.html
+     librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
+                              flags=['full-page'])
+     html = output.getvalue()
+     response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
+     return response
+ @never_cache
+ def book_pdf(request, slug):
+     book = get_object_or_404(Book, slug=slug)
+     if not book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     from tempfile import NamedTemporaryFile
+     from os import unlink
+     from librarian import pdf
+     from catalogue.ebook_utils import RedakcjaDocProvider, serve_file
+     xml = book.materialize()
+     xml_file = NamedTemporaryFile()
+     xml_file.write(xml.encode('utf-8'))
+     xml_file.flush()
+     try:
+         pdf_file = NamedTemporaryFile(delete=False)
+         pdf.transform(RedakcjaDocProvider(publishable=True),
+                   file_path=xml_file.name,
+                   output_file=pdf_file,
+                   )
+         return serve_file(pdf_file.name, book.slug + '.pdf', 'application/pdf')
+     finally:
+         unlink(pdf_file.name)
+ @never_cache
+ def book_epub(request, slug):
+     book = get_object_or_404(Book, slug=slug)
+     if not book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     from StringIO import StringIO
+     from tempfile import NamedTemporaryFile
+     from librarian import epub
+     from catalogue.ebook_utils import RedakcjaDocProvider
+     xml = book.materialize()
+     xml_file = NamedTemporaryFile()
+     xml_file.write(xml.encode('utf-8'))
+     xml_file.flush()
+     epub_file = StringIO()
+     epub.transform(RedakcjaDocProvider(publishable=True),
+             file_path=xml_file.name,
+             output_file=epub_file)
+     response = HttpResponse(mimetype='application/epub+zip')
+     response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
+     response.write(epub_file.getvalue())
+     return response
+ @never_cache
+ def revision(request, slug, chunk=None):
+     try:
+         doc = Chunk.get(slug, chunk)
+     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+         raise Http404
+     if not doc.book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     return http.HttpResponse(str(doc.revision()))
+ def book(request, slug):
+     book = get_object_or_404(Book, slug=slug)
+     if not book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     if request.user.has_perm('catalogue.change_book'):
+         if request.method == "POST":
+             form = forms.BookForm(request.POST, instance=book)
+             if form.is_valid():
+                 form.save()
+                 return http.HttpResponseRedirect(book.get_absolute_url())
+         else:
+             form = forms.BookForm(instance=book)
+             editable = True
+     else:
+         form = forms.ReadonlyBookForm(instance=book)
+         editable = False
+     publish_error = publishable_error(book)
+     publishable = publish_error is None
+     return direct_to_template(request, "catalogue/book_detail.html", extra_context={
+         "book": book,
+         "publishable": publishable,
+         "publishable_error": publish_error,
+         "form": form,
+         "editable": editable,
+     })
++def image(request, slug):
++    image = get_object_or_404(Image, slug=slug)
++    if not image.accessible(request):
++        return HttpResponseForbidden("Not authorized.")
++
++    if request.user.has_perm('catalogue.change_image'):
++        if request.method == "POST":
++            form = forms.ImageForm(request.POST, instance=image)
++            if form.is_valid():
++                form.save()
++                return http.HttpResponseRedirect(image.get_absolute_url())
++        else:
++            form = forms.ImageForm(instance=image)
++            editable = True
++    else:
++        form = forms.ReadonlyImageForm(instance=image)
++        editable = False
++
++    #publish_error = publishable_error(book)
++    publish_error = 'Publishing not implemented yet.'
++    publishable = publish_error is None
++
++    return direct_to_template(request, "catalogue/image_detail.html", extra_context={
++        "object": image,
++        "publishable": publishable,
++        "publishable_error": publish_error,
++        "form": form,
++        "editable": editable,
++    })
++
++
+ @permission_required('catalogue.add_chunk')
+ def chunk_add(request, slug, chunk):
+     try:
+         doc = Chunk.get(slug, chunk)
+     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+         raise Http404
+     if not doc.book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     if request.method == "POST":
+         form = forms.ChunkAddForm(request.POST, instance=doc)
+         if form.is_valid():
+             if request.user.is_authenticated():
+                 creator = request.user
+             else:
+                 creator = None
+             doc.split(creator=creator,
+                 slug=form.cleaned_data['slug'],
+                 title=form.cleaned_data['title'],
+                 gallery_start=form.cleaned_data['gallery_start'],
+                 user=form.cleaned_data['user'],
+                 stage=form.cleaned_data['stage']
+             )
+             return http.HttpResponseRedirect(doc.book.get_absolute_url())
+     else:
+         form = forms.ChunkAddForm(initial={
+                 "slug": str(doc.number + 1),
+                 "title": "cz. %d" % (doc.number + 1, ),
+         })
+     return direct_to_template(request, "catalogue/chunk_add.html", extra_context={
+         "chunk": doc,
+         "form": form,
+     })
+ def chunk_edit(request, slug, chunk):
+     try:
+         doc = Chunk.get(slug, chunk)
+     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+         raise Http404
+     if not doc.book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     if request.method == "POST":
+         form = forms.ChunkForm(request.POST, instance=doc)
+         if form.is_valid():
+             form.save()
+             go_next = request.GET.get('next', None)
+             if go_next:
+                 go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
+             else:
+                 go_next = doc.book.get_absolute_url()
+             return http.HttpResponseRedirect(go_next)
+     else:
+         form = forms.ChunkForm(instance=doc)
+     referer = request.META.get('HTTP_REFERER')
+     if referer:
+         parts = urlsplit(referer)
+         parts = ['', ''] + list(parts[2:])
+         go_next = urlquote_plus(urlunsplit(parts))
+     else:
+         go_next = ''
+     return direct_to_template(request, "catalogue/chunk_edit.html", extra_context={
+         "chunk": doc,
+         "form": form,
+         "go_next": go_next,
+     })
+ @permission_required('catalogue.change_book')
+ def book_append(request, slug):
+     book = get_object_or_404(Book, slug=slug)
+     if not book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     if request.method == "POST":
+         form = forms.BookAppendForm(book, request.POST)
+         if form.is_valid():
+             append_to = form.cleaned_data['append_to']
+             append_to.append(book)
+             return http.HttpResponseRedirect(append_to.get_absolute_url())
+     else:
+         form = forms.BookAppendForm(book)
+     return direct_to_template(request, "catalogue/book_append_to.html", extra_context={
+         "book": book,
+         "form": form,
+         "logout_to": '/',
+     })
+ @require_POST
+ @login_required
+ def publish(request, slug):
+     book = get_object_or_404(Book, slug=slug)
+     if not book.accessible(request):
+         return HttpResponseForbidden("Not authorized.")
+     try:
+         book.publish(request.user)
+     except NotAuthorizedError:
+         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
+     except BaseException, e:
+         return http.HttpResponse(e)
+     else:
+         return http.HttpResponseRedirect(book.get_absolute_url())
diff --combined apps/wiki/admin.py
@@@ -2,4 -2,8 +2,7 @@@ from django.contrib import admi
  
  from wiki import models
  
- #admin.site.register(models.Theme)
 -
+ class ThemeAdmin(admin.ModelAdmin):
+     search_fields = ['name']
+ admin.site.register(models.Theme, ThemeAdmin)
diff --combined apps/wiki_img/forms.py
index bc9e2d6,0000000..555f264
mode 100644,000000..100644
--- /dev/null
@@@ -1,97 -1,0 +1,20 @@@
- from wiki.constants import DOCUMENT_TAGS, DOCUMENT_STAGES
 +# -*- coding: utf-8 -*-
 +#
 +# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 +#
 +from django import forms
- class DocumentTagForm(forms.Form):
-     """
-         Form for tagging revisions.
-     """
 +from django.utils.translation import ugettext_lazy as _
++from wiki.forms import DocumentTextSaveForm
++from catalogue.models import Image
 +
 +
-     id = forms.CharField(widget=forms.HiddenInput)
-     tag = forms.ChoiceField(choices=DOCUMENT_TAGS)
-     revision = forms.IntegerField(widget=forms.HiddenInput)
- class DocumentCreateForm(forms.Form):
-     """
-         Form used for creating new documents.
-     """
-     title = forms.CharField()
-     id = forms.RegexField(regex=ur"^[-\wąćęłńóśźżĄĆĘŁŃÓŚŹŻ]+$")
-     file = forms.FileField(required=False)
-     text = forms.CharField(required=False, widget=forms.Textarea)
-     def clean(self):
-         file = self.cleaned_data['file']
-         if file is not None:
-             try:
-                 self.cleaned_data['text'] = file.read().decode('utf-8')
-             except UnicodeDecodeError:
-                 raise forms.ValidationError("Text file must be UTF-8 encoded.")
-         if not self.cleaned_data["text"]:
-             raise forms.ValidationError("You must either enter text or upload a file")
-         return self.cleaned_data
- class DocumentsUploadForm(forms.Form):
-     """
-         Form used for uploading new documents.
-     """
-     file = forms.FileField(required=True, label=_('ZIP file'))
-     def clean(self):
-         file = self.cleaned_data['file']
-         import zipfile
-         try:
-             z = self.cleaned_data['zip'] = zipfile.ZipFile(file)
-         except zipfile.BadZipfile:
-             raise forms.ValidationError("Should be a ZIP file.")
-         if z.testzip():
-             raise forms.ValidationError("ZIP file corrupt.")
-         return self.cleaned_data
- class DocumentTextSaveForm(forms.Form):
-     """
-     Form for saving document's text:
-         * name - document's storage identifier.
-         * parent_revision - revision which the modified text originated from.
-         * comment - user's verbose comment; will be used in commit.
-         * stage_completed - mark this change as end of given stage.
-     """
-     id = forms.CharField(widget=forms.HiddenInput)
-     parent_commit = forms.IntegerField(widget=forms.HiddenInput)
-     text = forms.CharField(widget=forms.HiddenInput)
-     author_name = forms.CharField(
++class ImageSaveForm(DocumentTextSaveForm):
++    """Form for saving document's text."""
 +
-         label=_(u"Author"),
-         help_text=_(u"Your name"),
-     )
-     author_email = forms.EmailField(
-         required=False,
-         label=_(u"Author's email"),
-         help_text=_(u"Your email address, so we can show a gravatar :)"),
-     )
-     comment = forms.CharField(
-         required=True,
-         widget=forms.Textarea,
-         label=_(u"Your comments"),
-         help_text=_(u"Describe changes you made."),
++    stage_completed = forms.ModelChoiceField(
++        queryset=Image.tag_model.objects.all(),
 +        required=False,
++        label=_(u"Completed"),
++        help_text=_(u"If you completed a life cycle stage, select it."),
 +    )
diff --combined apps/wiki_img/models.py
index dd16a87,0000000..b685324
mode 100644,000000..100644
--- /dev/null
@@@ -1,29 -1,0 +1,5 @@@
- from django.db import models
- from django.contrib.auth.models import User
- from django.utils.translation import ugettext_lazy as _
- from dvcs.models import Document
- class ImageDocument(models.Model):
-     slug = models.SlugField(_('slug'), max_length=120)
-     name = models.CharField(_('name'), max_length=120)
-     image = models.ImageField(_('image'), upload_to='wiki_img')
-     doc = models.OneToOneField(Document, null=True, blank=True)
-     creator = models.ForeignKey(User, null=True, blank=True)
-     @staticmethod
-     def listener_initial_commit(sender, instance, created, **kwargs):
-         if created:
-             instance.doc = Document.objects.create(creator=instance.creator)
-             instance.save()
-     def __unicode__(self):
-         return self.name
- models.signals.post_save.connect(ImageDocument.listener_initial_commit, sender=ImageDocument)
 +# -*- coding: utf-8 -*-
 +#
 +# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 +#
index d03a0bf,0000000..fc2e207
mode 100644,000000..100644
--- /dev/null
@@@ -1,32 -1,0 +1,34 @@@
 +{% extends "wiki_img/document_details_base.html" %}
 +{% load i18n %}
 +
 +{% block extrabody %}
 +{{ block.super }}
 +<script src="{{ STATIC_URL }}js/lib/codemirror-0.8/codemirror.js" type="text/javascript" charset="utf-8">
 +</script>
 +<script src="{{ STATIC_URL }}js/wiki_img/loader.js" type="text/javascript" charset="utf-8"> </script>
 +{% endblock %}
 +
 +{% block tabs-menu %}
 +    {% include "wiki_img/tabs/summary_view_item.html" %}
 +    {% include "wiki_img/tabs/motifs_editor_item.html" %}
 +    {% include "wiki_img/tabs/objects_editor_item.html" %}
 +    {% include "wiki_img/tabs/source_editor_item.html" %}
++    {% include "wiki/tabs/history_view_item.html" %}
 +{% endblock %}
 +
 +{% block tabs-content %}
 +    {% include "wiki_img/tabs/summary_view.html" %}
 +    {% include "wiki_img/tabs/motifs_editor.html" %}
 +    {% include "wiki_img/tabs/objects_editor.html" %}
 +    {% include "wiki_img/tabs/source_editor.html" %}
++    {% include "wiki_img/tabs/history_view.html" %}
 +{% endblock %}
 +
 +{% block dialogs %}
 +    {% include "wiki_img/save_dialog.html" %}
 +{% endblock %}
 +
 +{% block editor-class %}
 +    sideless
 +{% endblock %}
 +
index 30accf2,0000000..8cba7bf
mode 100644,000000..100644
--- /dev/null
@@@ -1,57 -1,0 +1,52 @@@
-       data-document-name="{{ document.slug }}" style="display:none">
 +{% extends "base.html" %}
 +{% load toolbar_tags i18n %}
 +
 +{% block title %}{{ document.name }} - {{ block.super }}{% endblock %}
 +{% block extrahead %}
 +{% load compressed %}
 +{% compressed_css 'detail' %}
 +{% endblock %}
 +
 +{% block extrabody %}
 +<script type="text/javascript" charset="utf-8">
 +    var STATIC_URL = '{{STATIC_URL}}';
 +</script>
 +{% compressed_js 'wiki_img' %}
 +{% endblock %}
 +
 +{% block maincontent %}
 +<div id="document-meta"
-       {% for k, v in document_meta.items %}
-               <span data-key="{{ k }}">{{ v }}</span>
-       {% endfor %}
-       {% for k, v in document_info.items %}
-               <span data-key="{{ k }}">{{ v }}</span>
-       {% endfor %}
++      data-object-id="{{ document.pk }}" style="display:none">
 +
-     <h1><a href="{% url wiki_document_list %}"><img src="{{STATIC_URL}}icons/go-home.png"/><a href="{% url wiki_document_list %}">Strona<br>główna</a></h1>
++      <span data-key="revision">{{ revision }}</span>
++    <span data-key="diff">{{ request.GET.diff }}</span>
 +
 +      {% block meta-extra %} {% endblock %}
 +</div>
 +
 +<div id="header">
++    <h1><a href="{% url catalogue_document_list %}"><img src="{{STATIC_URL}}icons/go-home.png"/><a href="{% url catalogue_document_list %}">Strona<br>główna</a></h1>
 +    <div id="tools">
 +        <a href="{{ REDMINE_URL }}projects/wl-publikacje/wiki/Pomoc" target="_blank">
 +        {% trans "Help" %}</a>
 +        | {% include "registration/head_login.html" %}
 +        | {% trans "Version" %}: <span id="document-revision">{% trans "Unknown" %}</span>
 +              {% if not readonly %}
 +            | <button style="margin-left: 6px" id="save-button">{% trans "Save" %}</button>
 +                      <span id='save-attempt-info'>{% trans "Save attempt in progress" %}</span>
 +            <span id='out-of-date-info'>{% trans "There is a newer version of this document!" %}</span>
 +              {% endif %}
 +    </div>
 +    <ol id="tabs" class="tabs">
 +      {% block tabs-menu %} {% endblock %}
 +    </ol>
 +</div>
 +<div id="splitter">
 +    <div id="editor" class="{% block editor-class %} {% endblock %}">
 +      {% block tabs-content %} {% endblock %}
 +    </div>
 +</div>
 +
 +{% block dialogs %} {% endblock %}
 +
 +{% endblock %}
index 71556a1,0000000..ca38838
mode 100644,000000..100644
--- /dev/null
@@@ -1,27 -1,0 +1,26 @@@
- {% extends "wiki/document_details_base.html" %}
++{% extends "wiki_img/document_details_base.html" %}
 +{% load i18n %}
 +
- {% block editor-class %}readonly{% endblock %}
 +{% block extrabody %}
 +{{ block.super }}
- <script src="{{STATIC_URL}}js/lib/codemirror-0.8/codemirror.js" type="text/javascript" charset="utf-8">
++<script src="{{ STATIC_URL }}js/lib/codemirror-0.8/codemirror.js" type="text/javascript" charset="utf-8">
 +</script>
- <script src="{{STATIC_URL}}js/wiki/loader_readonly.js" type="text/javascript" charset="utf-8"> </script>
++<script src="{{ STATIC_URL }}js/wiki_img/loader_readonly.js" type="text/javascript" charset="utf-8"> </script>
 +{% endblock %}
 +
 +{% block tabs-menu %}
-     {% include "wiki/tabs/wysiwyg_editor_item.html" %}
-       {% include "wiki/tabs/source_editor_item.html" %}
++    {% include "wiki_img/tabs/motifs_editor_item.html" %}
++    {% include "wiki_img/tabs/objects_editor_item.html" %}
++    {% include "wiki_img/tabs/source_editor_item.html" %}
 +{% endblock %}
 +
 +{% block tabs-content %}
-     {% include "wiki/tabs/wysiwyg_editor.html" %}
-       {% include "wiki/tabs/source_editor.html" %}
++    {% include "wiki_img/tabs/motifs_editor.html" %}
++    {% include "wiki_img/tabs/objects_editor.html" %}
++    {% include "wiki_img/tabs/source_editor.html" %}
 +{% endblock %}
 +
- {% block splitter-extra %}
++{% block editor-class %}
++    sideless
 +{% endblock %}
 +
- {% block dialogs %}
- {% endblock %}
index fc239d2,0000000..e8f89e6
mode 100644,000000..100644
--- /dev/null
@@@ -1,24 -1,0 +1,25 @@@
 +{% load i18n %}
 +<div id="save_dialog" class="dialog" data-ui-jsclass="SaveDialog">
 +      <form method="POST" action="">
++    {% csrf_token %}
 +      <p>{{ forms.text_save.comment.label }}</p>
 +      <p class="help_text">
 +              {{ forms.text_save.comment.help_text}}
 +              <span data-ui-error-for="{{ forms.text_save.comment.name }}"> </span>
 +      </p>
 +      {{forms.text_save.comment }}
 +
 +
 +
 +      {% for f in forms.text_save.hidden_fields %}
 +              {{ f }}
 +      {% endfor %}
 +
 +      <p data-ui-error-for="__all__"> </p>
 +
 +      <p class="action_area">
 +              <button type="submit" class"ok" data-ui-action="save">Zapisz</button>
 +              <button type="button" class="cancel" data-ui-action="cancel">Anuluj</button>
 +      </p>
 +      </form>
 +</div>
index 0000000,0000000..db49d64
new file mode 100755 (executable)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,40 @@@
++{% load i18n %}
++<div id="history-view-editor" class="editor" style="display: none">
++    <div class="toolbar">
++      <button type="button" id="make-diff-button"
++                      data-enabled-when="2" disabled="disabled">{% trans "Compare versions" %}</button>
++        {% if can_pubmark %}
++              <button type="button" id="pubmark-changeset-button"
++                      data-enabled-when="1" disabled="disabled">{% trans "Mark for publishing" %}</button>
++        {% endif %}
++              <button type="button" id="doc-revert-button"
++                      data-enabled-when="1" disabled="disabled">{% trans "Revert document" %}</button>
++              <button id="open-preview-button" disabled="disabled"
++                      data-enabled-when="1"
++                      data-basehref="{% url wiki_img_editor_readonly document.slug %}">{% trans "View version" %}</button>
++
++      </div>
++    <div id="history-view">
++        <p class="message-box" style="display:none;"></p>
++
++              <table id="changes-list-container">
++        <tbody id="changes-list">
++        </tbody>
++              <tbody style="display: none;">
++                      <tr class="entry row-stub">
++                      <td data-stub-value="version"></td>
++                      <td>
++                <span data-stub-value="date"></span>
++              <br/><span data-stub-value="author"></span>
++                              <br />
++                              <span data-stub-value="description"></span>
++                      </td>
++                      <td>
++                <div data-stub-value="publishable"></div>
++                <div data-stub-value="tag"></div>
++                      </td>
++              </tr>
++              </tbody>
++              </table>
++    </div>
++</div>
diff --combined apps/wiki_img/urls.py
index 075e5ad,0000000..b4396c6
mode 100644,000000..100644
--- /dev/null
@@@ -1,20 -1,0 +1,18 @@@
- from django.conf import settings
- from django.views.generic.list_detail import object_list
 +# -*- coding: utf-8
 +from django.conf.urls.defaults import *
- from wiki_img.models import ImageDocument
- PART = ur"""[ ĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9\w_.-]+"""
 +
-     url(r'^$', object_list, {'queryset': ImageDocument.objects.all(), "template_name": "wiki_img/document_list.html"}),
-     url(r'^edit/(?P<slug>%s)$' % PART,
 +
 +urlpatterns = patterns('wiki_img.views',
-     url(r'^(?P<slug>[^/]+)/text$',
++    url(r'^edit/(?P<slug>[^/]+)/$',
 +        'editor', name="wiki_img_editor"),
 +
++    url(r'^readonly/(?P<slug>[^/]+)/$',
++        'editor_readonly', name="wiki_img_editor_readonly"),
++
++    url(r'^text/(?P<image_id>\d+)/$',
 +        'text', name="wiki_img_text"),
 +
++    url(r'^history/(?P<chunk_id>\d+)/$',
++        'history', name="wiki_history"),
++
 +)
diff --combined apps/wiki_img/views.py
index c4d32b7,0000000..9e87f66
mode 100644,000000..100644
--- /dev/null
@@@ -1,70 -1,0 +1,129 @@@
- logger = logging.getLogger("fnp.wiki")
 +import os
 +import functools
 +import logging
- from wiki_img.models import ImageDocument
++logger = logging.getLogger("fnp.wiki_img")
 +
 +from django.views.generic.simple import direct_to_template
 +from django.core.urlresolvers import reverse
 +from wiki.helpers import JSONResponse
 +from django import http
 +from django.shortcuts import get_object_or_404
++from django.views.decorators.http import require_GET
 +from django.conf import settings
++from django.utils.formats import localize
 +
-     doc = get_object_or_404(ImageDocument, slug=slug)
++from catalogue.models import Image
 +from wiki_img.forms import DocumentTextSaveForm
 +
 +#
 +# Quick hack around caching problems, TODO: use ETags
 +#
 +from django.views.decorators.cache import never_cache
 +
 +
 +@never_cache
 +def editor(request, slug, template_name='wiki_img/document_details.html'):
-             "text_save": DocumentTextSaveForm(prefix="textsave"),
++    doc = get_object_or_404(Image, slug=slug)
 +
 +    return direct_to_template(request, template_name, extra_context={
 +        'document': doc,
 +        'forms': {
- def text(request, slug):
++            "text_save": DocumentTextSaveForm(user=request.user, prefix="textsave"),
 +        },
 +        'REDMINE_URL': settings.REDMINE_URL,
 +    })
 +
 +
++@require_GET
++def editor_readonly(request, slug, template_name='wiki_img/document_details_readonly.html'):
++    doc = get_object_or_404(Image, slug=slug)
++    try:
++        revision = request.GET['revision']
++    except (KeyError):
++        raise Http404
++
++
++
++    return direct_to_template(request, template_name, extra_context={
++        'document': doc,
++        'revision': revision,
++        'readonly': True,
++        'REDMINE_URL': settings.REDMINE_URL,
++    })
++
++
 +@never_cache
-         form = DocumentTextSaveForm(request.POST, prefix="textsave")
++def text(request, image_id):
++    doc = get_object_or_404(Image, pk=image_id)
 +    if request.method == 'POST':
-             document = get_object_or_404(ImageDocument, slug=slug)
-             commit = form.cleaned_data['parent_commit']
-             comment = form.cleaned_data['comment']
++        form = DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
 +        if form.is_valid():
-                 user = request.user
 +            if request.user.is_authenticated():
-                 user = None
-             document.doc.commit(
-                 parent=commit,
-                 text=form.cleaned_data['text'],
-                 author=user,
-                 description=comment
-             )
++                author = request.user
 +            else:
-                 'text': document.doc.materialize(),
-                 'revision': document.doc.change_set.count(),
++                author = None
++            text = form.cleaned_data['text']
++            parent_revision = form.cleaned_data['parent_revision']
++            if parent_revision is not None:
++                parent = doc.at_revision(parent_revision)
++            else:
++                parent = None
++            stage = form.cleaned_data['stage_completed']
++            tags = [stage] if stage else []
++            publishable = (form.cleaned_data['publishable'] and
++                    request.user.has_perm('catalogue.can_pubmark'))
++            doc.commit(author=author,
++                       text=text,
++                       parent=parent,
++                       description=form.cleaned_data['comment'],
++                       tags=tags,
++                       author_name=form.cleaned_data['author_name'],
++                       author_email=form.cleaned_data['author_email'],
++                       publishable=publishable,
++                       )
++            revision = doc.revision()
 +            return JSONResponse({
-         doc = get_object_or_404(ImageDocument, slug=slug).doc
++                'text': doc.materialize() if parent_revision != revision else None,
++                'meta': {},
++                'revision': revision,
 +            })
 +        else:
 +            return JSONFormInvalid(form)
 +    else:
-             'text': doc.materialize(),
-             'revision': doc.change_set.count(),
-             'commit': doc.head.id,
++        revision = request.GET.get("revision", None)
++        
++        try:
++            revision = int(revision)
++        except (ValueError, TypeError):
++            revision = doc.revision()
++
++        if revision is not None:
++            text = doc.at_revision(revision).materialize()
++        else:
++            text = ''
++
 +        return JSONResponse({
++            'text': text,
++            'meta': {},
++            'revision': revision,
 +        })
 +
++
++@never_cache
++def history(request, chunk_id):
++    # TODO: pagination
++    doc = get_object_or_404(Image, pk=chunk_id)
++    if not doc.accessible(request):
++        return HttpResponseForbidden("Not authorized.")
++
++    changes = []
++    for change in doc.history().reverse():
++        changes.append({
++                "version": change.revision,
++                "description": change.description,
++                "author": change.author_str(),
++                "date": localize(change.created_at),
++                "publishable": _("Publishable") + "\n" if change.publishable else "",
++                "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()),
++            })
++    return JSONResponse(changes)
@@@ -10,8 -10,6 +10,6 @@@ TEMPLATE_DEBUG = DEBU
  MAINTENANCE_MODE = False
  
  ADMINS = (
-     # (u'Marek Stępniowski', 'marek@stepniowski.com'),
-     # (u'Łukasz Rekucki', 'lrekucki@gmail.com'),
      (u'Radek Czajka', 'radoslaw.czajka@nowoczesnapolska.org.pl'),
  )
  
@@@ -36,6 -34,8 +34,8 @@@ SITE_ID = 
  # If you set this to False, Django will make some optimizations so as not
  # to load the internationalization machinery.
  USE_I18N = True
+ USE_L10N = True
  
  # Absolute path to the directory that holds media.
  # Example: "/home/media/media.lawrence.com/"
@@@ -59,28 -59,30 +59,30 @@@ SESSION_COOKIE_NAME = "redakcja_session
  
  # List of callables that know how to import templates from various sources.
  TEMPLATE_LOADERS = (
-     'django.template.loaders.filesystem.load_template_source',
-     'django.template.loaders.app_directories.load_template_source',
- #     'django.template.loaders.eggs.load_template_source',
+     'django.template.loaders.filesystem.Loader',
+     'django.template.loaders.app_directories.Loader',
  )
  
  TEMPLATE_CONTEXT_PROCESSORS = (
-     "django.core.context_processors.auth",
+     "django.contrib.auth.context_processors.auth",
      "django.core.context_processors.debug",
      "django.core.context_processors.i18n",
      "redakcja.context_processors.settings", # this is instead of media
+     'django.core.context_processors.csrf',
      "django.core.context_processors.request",
  )
  
  
  MIDDLEWARE_CLASSES = (
      'django.middleware.common.CommonMiddleware',
+     'django.middleware.csrf.CsrfViewMiddleware',
      'django.contrib.sessions.middleware.SessionMiddleware',
  
      'django.contrib.auth.middleware.AuthenticationMiddleware',
      'django_cas.middleware.CASMiddleware',
  
      'django.middleware.doc.XViewMiddleware',
+     'pagination.middleware.PaginationMiddleware',
      'maintenancemode.middleware.MaintenanceModeMiddleware',
  )
  
@@@ -97,13 -99,6 +99,6 @@@ TEMPLATE_DIRS = 
  
  FIREPYTHON_LOGGER_NAME = "fnp"
  
- #
- # Central Auth System
- #
- ## Set this to where the CAS server lives
- # CAS_SERVER_URL = "http://cas.fnp.pl/
- CAS_LOGOUT_COMPLETELY = True
  INSTALLED_APPS = (
      'django.contrib.auth',
      'django.contrib.contenttypes',
      'django.contrib.sites',
      'django.contrib.admin',
      'django.contrib.admindocs',
+     'django.contrib.comments',
  
-     'django_cas',
      'compress',
      'south',
      'sorl.thumbnail',
      'filebrowser',
+     'pagination',
+     'gravatar',
+     'djcelery',
+     'djkombu',
  
+     'catalogue',
      'dvcs',
      'wiki',
 +    'wiki_img',
      'toolbar',
+     'apiclient',
+     'email_mangler',
  )
  
+ LOGIN_REDIRECT_URL = '/documents/user'
+ CAS_USER_ATTRS_MAP = {
+     'email': 'email', 'firstname': 'first_name', 'lastname': 'last_name'}
  FILEBROWSER_URL_FILEBROWSER_MEDIA = STATIC_URL + 'filebrowser/'
  FILEBROWSER_DIRECTORY = 'images/'
  FILEBROWSER_ADMIN_VERSIONS = []
@@@ -134,12 -141,15 +142,15 @@@ FILEBROWSER_DEFAULT_ORDER = "path_relat
  IMAGE_DIR = 'images'
  
  
- WL_API_CONFIG = {
-     "URL": "http://localhost:7000/api/",
-     "AUTH_REALM": "WL API",
-     "AUTH_USER": "platforma",
-     "AUTH_PASSWD": "platforma",
- }
+ import djcelery
+ djcelery.setup_loader()
+ BROKER_BACKEND = "djkombu.transport.DatabaseTransport"
+ BROKER_HOST = "localhost"
+ BROKER_PORT = 5672
+ BROKER_USER = "guest"
+ BROKER_PASSWORD = "guest"
+ BROKER_VHOST = "/"
  
  SHOW_APP_VERSION = False
  
@@@ -147,3 -157,4 +158,4 @@@ try
      from redakcja.settings.compress import *
  except ImportError:
      pass
@@@ -9,16 -9,15 +9,16 @@@ COMPRESS_CSS = 
              'css/summary.css',
              'css/html.css',
              'css/jquery.autocomplete.css',
 +            'css/imgareaselect-default.css', #img!
              'css/dialogs.css',
          ),
          'output_filename': 'compressed/detail_styles_?.css',
      },
-     'listing': {
+     'catalogue': {
          'source_filenames': (
              'css/filelist.css',
          ),
-         'output_filename': 'compressed/listing_styles_?.css',
+         'output_filename': 'compressed/catalogue_styles_?.css',
       }
  }
  
@@@ -27,10 -26,10 +27,10 @@@ COMPRESS_JS = 
      'detail': {
          'source_filenames': (
                  # libraries
-                 'js/lib/jquery-1.4.2.min.js',
                  'js/lib/jquery/jquery.autocomplete.js',
                  'js/lib/jquery/jquery.blockui.js',
                  'js/lib/jquery/jquery.elastic.js',
+                 'js/lib/jquery/jquery.xmlns.js',
                  'js/button_scripts.js',
                  'js/slugify.js',
  
@@@ -44,7 -43,8 +44,8 @@@
  
                  # dialogs
                  'js/wiki/dialog_save.js',
-                 'js/wiki/dialog_addtag.js',
+                 'js/wiki/dialog_revert.js',
+                 'js/wiki/dialog_pubmark.js',
  
                  # views
                  'js/wiki/view_history.js',
          ),
          'output_filename': 'compressed/detail_scripts_?.js',
       },
-     'listing': {
 +    'wiki_img': {
 +        'source_filenames': (
 +                # libraries
 +                'js/lib/jquery-1.4.2.min.js',
 +                'js/lib/jquery/jquery.autocomplete.js',
 +                'js/lib/jquery/jquery.blockui.js',
 +                'js/lib/jquery/jquery.elastic.js',
 +                'js/lib/jquery/jquery.imgareaselect.js',
 +                'js/button_scripts.js',
 +                'js/slugify.js',
 +
 +                # wiki scripts
 +                'js/wiki_img/wikiapi.js',
 +
 +                # base UI
 +                'js/wiki_img/base.js',
 +                'js/wiki_img/toolbar.js',
 +
 +                # dialogs
 +                'js/wiki_img/dialog_save.js',
 +                'js/wiki_img/dialog_addtag.js',
 +
 +                # views
 +                'js/wiki_img/view_summary.js',
 +                'js/wiki_img/view_editor_objects.js',
 +                'js/wiki_img/view_editor_motifs.js',
 +                'js/wiki/view_editor_source.js',
++                'js/wiki/view_history.js',
 +        ),
 +        'output_filename': 'compressed/detail_img_scripts_?.js',
 +     },
+     'catalogue': {
          'source_filenames': (
-                 'js/lib/jquery-1.4.2.min.js',
+                 'js/catalogue/catalogue.js',
                  'js/slugify.js',
+                 'email_mangler/email_mangler.js',
          ),
-         'output_filename': 'compressed/listing_scripts_?.js',
+         'output_filename': 'compressed/catalogue_scripts_?.js',
       }
  }
  
@@@ -57,29 -57,6 +57,29 @@@ body 
      overflow: hidden;
  }
  
 +.sideless .editor {
 +    right: 0;
 +}
 +.image-object {
 +    padding-left: 1em;
 +    font: 12px Sans, Helvetica, Verdana, sans-serif;
 +}
 +.image-object:hover {
 +    cursor: pointer;
 +}
 +#objects-list .delete {
 +    padding-left: 3px;
 +    font: 10px Sans, Helvetica, Verdana, sans-serif;
 +}
 +#objects-list .delete:hover {
 +    cursor: pointer;
 +}
 +
 +#objects-list .active {
 +    color: #800;
 +}
 +
 +
  #editor.readonly .editor {
        right: 0px;
  }
  
      font: 11px Helvetica, Verdana, sans-serif;
      font-weight: bold;
 +
 +    z-index: 100;
  }
  
  
@@@ -360,7 -335,15 +360,15 @@@ img.tabclose 
  }
  
  .saveNotify {
-     position:absolute; bottom:7px; left:30px; z-index:800; background-color: #E6E6E6; padding:20px; border: 1px solid black;
+     position:absolute; 
+     top:22px; 
+     right:7px; 
+     z-index:800;
+     background-color: #FFFF69; 
+     padding:10px; 
+     border: 1px solid black;
+     border-radius: 5px;
+     -moz-border-radius: 15px;
  }
  
  .notifyTip {
  .saveNotify span {
      font-weight: bold;
  }
 +
 +
 +
 +.scrolled {
 +    position: absolute;
 +    top: 29px;
 +    left: 0;
 +    right: 0;
 +    bottom: 0;
 +    overflow: auto;
 +}
index 41a029d,0000000..9cfc640
mode 100644,000000..100644
--- /dev/null
@@@ -1,159 -1,0 +1,136 @@@
- DEFAULT_PERSPECTIVE = "#SummaryPerspective";
 +if (!window.console) {
 +    window.console = {
 +        log: function(){
 +        }
 +    }
 +}
 +
- // Wykonuje block z załadowanymi kanonicznymi motywami
- function withThemes(code_block, onError)
- {
-     if (typeof withThemes.canon == 'undefined') {
-         $.ajax({
-             url: '/themes',
-             dataType: 'text',
-             success: function(data) {
-                 withThemes.canon = data.split('\n');
-                 code_block(withThemes.canon);
-             },
-             error: function() {
-                 withThemes.canon = null;
-                 code_block(withThemes.canon);
-             }
-         })
-     }
-     else {
-         code_block(withThemes.canon);
-     }
- }
++DEFAULT_PERSPECTIVE = "#MotifsPerspective";
 +
 +$(function()
 +{
 +      var tabs = $('ol#tabs li');
 +      var gallery = null;
 +      CurrentDocument = new $.wikiapi.WikiDocument("document-meta");
 +
 +      $.blockUI.defaults.baseZ = 10000;
 +
 +    function initialize()
 +      {
 +              $(document).keydown(function(event) {
 +                      console.log("Received key:", event);
 +              });
 +
 +              /* The save button */
 +        $('#save-button').click(function(event){
 +            event.preventDefault();
 +                      $.wiki.showDialog('#save_dialog');
 +        });
 +
 +              $('.editor').hide();
 +
 +              /*
 +               * TABS
 +               */
 +        $('.tabs li').live('click', function(event, callback) {
 +                      $.wiki.switchToTab(this);
 +        });
 +
 +              $('#tabs li > .tabclose').live('click', function(event, callback) {
 +                      var $tab = $(this).parent();
 +
 +                      if($tab.is('.active'))
 +                              $.wiki.switchToTab(DEFAULT_PERSPECTIVE);
 +
 +                      var p = $.wiki.perspectiveForTab($tab);
 +                      p.destroy();
 +
 +                      return false;
 +        });
 +
 +
 +        /*$(window).resize(function(){
 +            $('iframe').height($(window).height() - $('#tabs').outerHeight() - $('#source-editor .toolbar').outerHeight());
 +        });
 +
 +        $(window).resize();*/
 +
 +        /*$('.vsplitbar').toggle(
 +                      function() {
 +                              $.wiki.state.perspectives.ScanGalleryPerspective.show = true;
 +                              $('#sidebar').show();
 +                              $('.vsplitbar').css('right', 480).addClass('active');
 +                              $('#editor .editor').css('right', 510);
 +                              $(window).resize();
 +                              $.wiki.perspectiveForTab('#tabs-right .active').onEnter();
 +                      },
 +                      function() {
 +                          var active_right = $.wiki.perspectiveForTab('#tabs-right .active');
 +                              $.wiki.state.perspectives.ScanGalleryPerspective.show = false;
 +                              $('#sidebar').hide();
 +                              $('.vsplitbar').css('right', 0).removeClass('active');
 +                              $(".vsplitbar-title").html("&uarr;&nbsp;" + active_right.vsplitbar + "&nbsp;&uarr;");
 +                              $('#editor .editor').css('right', 30);
 +                              $(window).resize();
 +                              active_right.onExit();
 +                      }
 +              );*/
 +
 +        window.onbeforeunload = function(e) {
 +            if($.wiki.isDirty()) {
 +                              e.returnValue = "Na stronie mogą być nie zapisane zmiany.";
 +                              return "Na stronie mogą być nie zapisane zmiany.";
 +                      };
 +        };
 +
 +              console.log("Fetching document's text");
 +
 +              $(document).bind('wlapi_document_changed', function(event, doc) {
 +                      try {
 +                              $('#document-revision').text(doc.revision);
 +                      } catch(e) {
 +                              console.log("Failed handler", e);
 +                      }
 +              });
 +
 +              CurrentDocument.fetch({
 +                      success: function(){
 +                              console.log("Fetch success");
 +                              $('#loading-overlay').fadeOut();
 +                              var active_tab = document.location.hash || DEFAULT_PERSPECTIVE;
 +
 +                              console.log("Initial tab is:", active_tab)
 +                              $.wiki.switchToTab(active_tab);
 +                      },
 +                      failure: function() {
 +                              $('#loading-overlay').fadeOut();
 +                              alert("FAILURE");
 +                      }
 +              });
 +    }; /* end of initialize() */
 +
 +
 +      /* Load configuration */
 +      $.wiki.loadConfig();
 +
 +      var initAll = function(a, f) {
 +              if (a.length == 0) return f();
 +
 +              $.wiki.initTab({
 +                      tab: a.pop(),
 +                      doc: CurrentDocument,
 +                      callback: function(){
 +                              initAll(a, f);
 +                      }
 +              });
 +      };
 +
 +
 +      /*
 +       * Initialize all perspectives
 +       */
 +      initAll( $.makeArray($('.tabs li')), initialize);
 +      console.log(location.hash);
 +});
 +
 +
index 0000000,0000000..1ce15b7
new file mode 100755 (executable)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,92 @@@
++if (!window.console) {
++    window.console = {
++        log: function(){
++        }
++    }
++}
++
++
++DEFAULT_PERSPECTIVE = "#MotifsPerspective";
++
++$(function()
++{
++      var tabs = $('ol#tabs li');
++      var gallery = null;
++
++      CurrentDocument = new $.wikiapi.WikiDocument("document-meta");
++      $.blockUI.defaults.baseZ = 10000;
++
++      function initialize()
++      {
++              $('.editor').hide();
++
++              /*
++               * TABS
++               */
++        $('#tabs li').live('click', function(event, callback) {
++                      $.wiki.switchToTab(this);
++        });
++
++              $('#tabs li > .tabclose').live('click', function(event, callback) {
++                      var $tab = $(this).parent();
++
++                      if($tab.is('.active'))
++                              $.wiki.switchToTab(DEFAULT_PERSPECTIVE);
++
++                      var p = $.wiki.perspectiveForTab($tab);
++                      p.destroy();
++                      return false;
++        });
++
++        $(window).resize(function(){
++            $('iframe').height($(window).height() - $('#tabs').outerHeight() - $('#source-editor .toolbar').outerHeight());
++        });
++
++              $(document).bind('wlapi_document_changed', function(event, doc) {
++                      try {
++                              $('#document-revision').text(doc.revision);
++                      } catch(e) {
++                              console.log("Failed handler", e);
++                      }
++              });
++
++              CurrentDocument.fetch({
++                      success: function(){
++                              console.log("Fetch success");
++                              $('#loading-overlay').fadeOut();
++                              var active_tab = document.location.hash || DEFAULT_PERSPECTIVE;
++
++                              $(window).resize();
++
++                              console.log("Initial tab is:", active_tab)
++                              $.wiki.switchToTab(active_tab);
++                      },
++                      failure: function() {
++                              $('#loading-overlay').fadeOut();
++                              alert("FAILURE");
++                      }
++              });
++    }; /* end of initialize() */
++
++      /* Load configuration */
++      $.wiki.loadConfig();
++
++      var initAll = function(a, f) {
++              if (a.length == 0) return f();
++
++              $.wiki.initTab({
++                      tab: a.pop(),
++                      doc: CurrentDocument,
++                      callback: function(){
++                              initAll(a, f);
++                      }
++              });
++      };
++
++
++      /*
++       * Initialize all perspectives
++       */
++      initAll( $.makeArray($('ol#tabs li')), initialize);
++      console.log(location.hash);
++});
index 0990e60,0000000..0f56ffe
mode 100644,000000..100644
--- /dev/null
@@@ -1,310 -1,0 +1,350 @@@
-                       var path = "/" + arguments[1] + "/text";
-                   if (arguments[2] !== undefined)
-                               path += "/" + arguments[2];
-                       return base_path + path;
 +(function($) {
 +      $.wikiapi = {};
 +      var noop = function() {
 +      };
 +      var noops = {
 +              success: noop,
 +              failure: noop
 +      };
 +      /*
 +       * Return absolute reverse path of given named view. (at least he have it
 +       * hard-coded in one place)
 +       *
 +       * TODO: think of a way, not to hard-code it here ;)
 +       *
 +       */
 +      function reverse() {
 +              var vname = arguments[0];
 +              var base_path = "/images";
 +
 +              if (vname == "ajax_document_text") {
-               /*if (vname == "ajax_document_history") {
++                      return base_path + "/text/" + arguments[1] + "/";
 +              }
 +
-                       return base_path + "/" + arguments[1] + "/history";
++              if (vname == "ajax_document_history") {
 +
-               if (vname == "ajax_document_gallery") {
-                       return base_path + "/" + arguments[1] + "/gallery";
-               }
++                      return base_path + "/history/" + arguments[1] + "/";
 +              }
 +*/
-               this.id = meta.attr('data-document-name');
 +/*
 +              if (vname == "ajax_document_diff")
 +                      return base_path + "/" + arguments[1] + "/diff";
 +
 +        if (vname == "ajax_document_rev")
 +            return base_path + "/" + arguments[1] + "/rev";
 +
 +              if (vname == "ajax_document_addtag")
 +                      return base_path + "/" + arguments[1] + "/tags";
 +
 +              if (vname == "ajax_publish")
 +                      return base_path + "/" + arguments[1] + "/publish";*/
 +
 +              console.log("Couldn't reverse match:", vname);
 +              return "/404.html";
 +      };
 +
 +      /*
 +       * Document Abstraction
 +       */
 +      function WikiDocument(element_id) {
 +              var meta = $('#' + element_id);
-         this.commit = $("*[data-key='commit']", meta).text();
++              this.id = meta.attr('data-object-id');
 +
 +              this.revision = $("*[data-key='revision']", meta).text();
-               this.galleryLink = $("*[data-key='gallery']", meta).text();
-               this.galleryImages = [];
 +              this.readonly = !!$("*[data-key='readonly']", meta).text();
 +
 +              this.text = null;
 +              this.has_local_changes = false;
 +              this._lock = -1;
 +              this._context_lock = -1;
 +              this._lock_count = 0;
 +      };
 +
 +      WikiDocument.prototype.triggerDocumentChanged = function() {
 +              $(document).trigger('wlapi_document_changed', this);
 +      };
 +      /*
 +       * Fetch text of this document.
 +       */
 +      WikiDocument.prototype.fetch = function(params) {
 +              params = $.extend({}, noops, params);
 +              var self = this;
 +              $.ajax({
 +                      method: "GET",
 +                      url: reverse("ajax_document_text", self.id),
 +                      data: {"commit": self.commit},
 +                      dataType: 'json',
 +                      success: function(data) {
 +                              var changed = false;
 +
 +                              if (self.text === null || self.commit !== data.commit) {
 +                                      self.text = data.text;
 +                                      if (self.text === '') {
 +                                          self.text = '<obraz></obraz>';
 +                                      }
 +                                      self.revision = data.revision;
 +                    self.commit = data.commit;
 +                                      changed = true;
 +                                      self.triggerDocumentChanged();
 +                              };
 +
 +                              self.has_local_changes = false;
 +                              params['success'](self, changed);
 +                      },
 +                      error: function() {
 +                              params['failure'](self, "Nie udało się wczytać treści dokumentu.");
 +                      }
 +              });
 +      };
++      /*
++       * Fetch history of this document.
++       *
++       * from - First revision to fetch (default = 0) upto - Last revision to
++       * fetch (default = tip)
++       *
++       */
++      WikiDocument.prototype.fetchHistory = function(params) {
++              /* this doesn't modify anything, so no locks */
++              params = $.extend({}, noops, params);
++              var self = this;
++              $.ajax({
++                      method: "GET",
++                      url: reverse("ajax_document_history", self.id),
++                      dataType: 'json',
++                      data: {
++                              "from": params['from'],
++                              "upto": params['upto']
++                      },
++                      success: function(data) {
++                              params['success'](self, data);
++                      },
++                      error: function() {
++                              params['failure'](self, "Nie udało się wczytać historii dokumentu.");
++                      }
++              });
++      };
 +
 +      /*
 +       * Set document's text
 +       */
 +      WikiDocument.prototype.setText = function(text) {
 +              this.text = text;
 +              this.has_local_changes = true;
 +      };
 +
 +      /*
 +       * Save text back to the server
 +       */
 +      WikiDocument.prototype.save = function(params) {
 +              params = $.extend({}, noops, params);
 +              var self = this;
 +
 +              if (!self.has_local_changes) {
 +                      console.log("Abort: no changes.");
 +                      return params['success'](self, false, "Nie ma zmian do zapisania.");
 +              };
 +
 +              // Serialize form to dictionary
 +              var data = {};
 +              $.each(params['form'].serializeArray(), function() {
 +                      data[this.name] = this.value;
 +              });
 +
 +              data['textsave-text'] = self.text;
 +
 +              $.ajax({
 +                      url: reverse("ajax_document_text", self.id),
 +                      type: "POST",
 +                      dataType: "json",
 +                      data: data,
 +                      success: function(data) {
 +                              var changed = false;
 +
 +                $('#header').removeClass('saving');
 +
 +                              if (data.text) {
 +                                      self.text = data.text;
 +                                      self.revision = data.revision;
 +                    self.commit = data.commit;
 +                                      changed = true;
 +                                      self.triggerDocumentChanged();
 +                              };
 +
 +                              params['success'](self, changed, ((changed && "Udało się zapisać :)") || "Twoja wersja i serwera jest identyczna"));
 +                      },
 +                      error: function(xhr) {
 +                if ($('#header').hasClass('saving')) {
 +                    $('#header').removeClass('saving');
 +                    $.blockUI({
 +                        message: "<p>Nie udało się zapisać zmian. <br/><button onclick='$.unblockUI()'>OK</button></p>"
 +                    })
 +                }
 +                else {
 +                    try {
 +                        params['failure'](self, $.parseJSON(xhr.responseText));
 +                    }
 +                    catch (e) {
 +                        params['failure'](self, {
 +                            "__message": "<p>Nie udało się zapisać - błąd serwera.</p>"
 +                        });
 +                    };
 +                }
 +
 +                      }
 +              });
 +
 +        $('#save-hide').click(function(){
 +            $('#header').addClass('saving');
 +            $.unblockUI();
 +            $.wiki.blocking.unblock();
 +        });
 +      }; /* end of save() */
 +
 +      WikiDocument.prototype.publish = function(params) {
 +              params = $.extend({}, noops, params);
 +              var self = this;
 +              $.ajax({
 +                      url: reverse("ajax_publish", self.id),
 +                      type: "POST",
 +                      dataType: "json",
 +                      success: function(data) {
 +                              params.success(self, data);
 +                      },
 +                      error: function(xhr) {
 +                              if (xhr.status == 403 || xhr.status == 401) {
 +                                      params.failure(self, "Nie masz uprawnień lub nie jesteś zalogowany.");
 +                              }
 +                              else {
 +                                      try {
 +                                              params.failure(self, xhr.responseText);
 +                                      }
 +                                      catch (e) {
 +                                              params.failure(self, "Nie udało się - błąd serwera.");
 +                                      };
 +                              };
 +
 +                      }
 +              });
 +      };
 +      WikiDocument.prototype.setTag = function(params) {
 +              params = $.extend({}, noops, params);
 +              var self = this;
 +              var data = {
 +                      "addtag-id": self.id,
 +              };
 +
 +              /* unpack form */
 +              $.each(params.form.serializeArray(), function() {
 +                      data[this.name] = this.value;
 +              });
 +
 +              $.ajax({
 +                      url: reverse("ajax_document_addtag", self.id),
 +                      type: "POST",
 +                      dataType: "json",
 +                      data: data,
 +                      success: function(data) {
 +                              params.success(self, data.message);
 +                      },
 +                      error: function(xhr) {
 +                              if (xhr.status == 403 || xhr.status == 401) {
 +                                      params.failure(self, {
 +                                              "__all__": ["Nie masz uprawnień lub nie jesteś zalogowany."]
 +                                      });
 +                              }
 +                              else {
 +                                      try {
 +                                              params.failure(self, $.parseJSON(xhr.responseText));
 +                                      }
 +                                      catch (e) {
 +                                              params.failure(self, {
 +                                                      "__all__": ["Nie udało się - błąd serwera."]
 +                                              });
 +                                      };
 +                              };
 +                      }
 +              });
 +      };
 +
 +    WikiDocument.prototype.getImageItems = function(tag) {
 +        var self = this;
 +
 +        var parser = new DOMParser();
 +        var doc = parser.parseFromString(self.text, 'text/xml');
 +        var error = $('parsererror', doc);
 +
 +        if (error.length != 0) {
 +            return null;
 +        }
 +
 +        var a = [];
 +        $(tag, doc).each(function(i, e) {
 +            var $e = $(e);
 +            a.push([
 +                $e.text(),
 +                $e.attr('x1'),
 +                $e.attr('y1'),
 +                $e.attr('x2'),
 +                $e.attr('y2')
 +            ]);
 +        });
 +
 +        return a;
 +    }
 +
 +    WikiDocument.prototype.setImageItems = function(tag, items) {
 +        var self = this;
 +
 +        var parser = new DOMParser();
 +        var doc = parser.parseFromString(self.text, 'text/xml');
 +        var serializer = new XMLSerializer();
 +        var error = $('parsererror', doc);
 +
 +        if (error.length != 0) {
 +            return null;
 +        }
 +
 +        $(tag, doc).remove();
 +        $root = $(doc.firstChild);
 +        $.each(items, function(i, e) {
 +            var el = $(doc.createElement(tag));
 +            el.text(e[0]);
 +            if (e[1] !== null) {
 +                el.attr('x1', e[1]);
 +                el.attr('y1', e[2]);
 +                el.attr('x2', e[3]);
 +                el.attr('y2', e[4]);
 +            }
 +            $root.append(el);
 +        });
 +        self.setText(serializer.serializeToString(doc));
 +    }
 +
 +
 +      $.wikiapi.WikiDocument = WikiDocument;
 +})(jQuery);
++
++
++
++// Wykonuje block z załadowanymi kanonicznymi motywami
++function withThemes(code_block, onError)
++{
++    if (typeof withThemes.canon == 'undefined') {
++        $.ajax({
++            url: '/editor/themes',
++            dataType: 'text',
++            success: function(data) {
++                withThemes.canon = data.split('\n');
++                code_block(withThemes.canon);
++            },
++            error: function() {
++                withThemes.canon = null;
++                code_block(withThemes.canon);
++            }
++        })
++    }
++    else {
++        code_block(withThemes.canon);
++    }
++}
++
diff --combined redakcja/urls.py
@@@ -4,12 -4,14 +4,14 @@@ from django.conf.urls.defaults import 
  from django.contrib import admin
  from django.conf import settings
  
- import wiki.urls
  
  admin.autodiscover()
  
  urlpatterns = patterns('',
      # Auth
+     #url(r'^accounts/login/$', 'django.contrib.auth.views.login', name='login'),
+     #url(r'^accounts/logout/$', 'catalogue.views.logout_then_redirect', name='logout'),
      url(r'^accounts/login/$', 'django_cas.views.login', name='login'),
      url(r'^accounts/logout/$', 'django_cas.views.logout', name='logout'),
  
      url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
      (r'^admin/', include(admin.site.urls)),
  
-     url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/images/'}),
-     url(r'^documents/', include('wiki.urls')),
+     (r'^comments/', include('django.contrib.comments.urls')),
+     url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
+     url(r'^documents/', include('catalogue.urls')),
+     url(r'^apiclient/', include('apiclient.urls')),
+     url(r'^editor/', include('wiki.urls')),
 +    url(r'^images/', include('wiki_img.urls')),
  
      # Static files (should be served by Apache)
      url(r'^%s(?P<path>.+)$' % settings.MEDIA_URL[1:], 'django.views.static.serve',
@@@ -29,8 -34,7 +35,7 @@@
          {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}),
      url(r'^%s(?P<path>.+)$' % settings.STATIC_URL[1:], 'django.views.static.serve',
          {'document_root': settings.STATIC_ROOT, 'show_indexes': True}),
-     (r'^documents/', include(wiki.urls)),
-     url(r'^themes$', 'wiki.views.themes', name="themes"),
      url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
  
  )