--- /dev/null
-
+ 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)
--- /dev/null
--- /dev/null
++# encoding: utf-8
++import datetime
++from south.db import db
++from south.v2 import SchemaMigration
++from django.db import models
++
++class Migration(SchemaMigration):
++
++ def forwards(self, orm):
++
++ # Adding 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']
--- /dev/null
+ # -*- 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)
--- /dev/null
--- /dev/null
++# -*- 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
--- /dev/null
-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)
+
--- /dev/null
-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')
--- /dev/null
--- /dev/null
++{% 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>
--- /dev/null
--- /dev/null
++{% extends "catalogue/base.html" %}
++
++{% load i18n %}
++{% load catalogue book_list %}
++
++
++{% block content %}
++ {% image_list %}
++{% endblock content %}
--- /dev/null
--- /dev/null
++{% 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>
--- /dev/null
--- /dev/null
++{% 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 %}
--- /dev/null
--- /dev/null
++{% 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 %}
--- /dev/null
-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
--- /dev/null
+ 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
+
--- /dev/null
+ # -*- 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"),
+
+ )
--- /dev/null
+ 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())
from wiki import models
- #admin.site.register(models.Theme)
-
+ class ThemeAdmin(admin.ModelAdmin):
+ search_fields = ['name']
+
+ admin.site.register(models.Theme, ThemeAdmin)
--- /dev/null
- 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."),
+ )
--- /dev/null
- 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.
+#
--- /dev/null
+{% 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 %}
+
--- /dev/null
- 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 %}
--- /dev/null
- {% 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 %}
--- /dev/null
+{% 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>
--- /dev/null
--- /dev/null
++{% 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>
--- /dev/null
- 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"),
++
+)
--- /dev/null
- 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)
'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 = []
),
'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',
}
}
--- /dev/null
- 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("↑ " + active_right.vsplitbar + " ↑");
+ $('#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);
+});
+
+
--- /dev/null
--- /dev/null
++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);
++});
--- /dev/null
- 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);
++ }
++}
++
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',