From: Radek Czajka Date: Wed, 14 Dec 2011 13:44:29 +0000 (+0100) Subject: Merge master into img-playground. Image support with new management features. Missing... X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/73ef2b8442dc95f8b7279de812c30ac8626d5f39 Merge master into img-playground. Image support with new management features. Missing history-related stuff, needs refactoring (doubled code). --- 73ef2b8442dc95f8b7279de812c30ac8626d5f39 diff --cc apps/catalogue/admin.py index 00000000,a3faa98e..8ba803e7 mode 000000,100644..100644 --- a/apps/catalogue/admin.py +++ b/apps/catalogue/admin.py @@@ -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) diff --cc apps/catalogue/migrations/0009_auto__add_imagechange__add_unique_imagechange_tree_revision__add_image.py index 00000000,00000000..4de5212e new file mode 100644 --- /dev/null +++ b/apps/catalogue/migrations/0009_auto__add_imagechange__add_unique_imagechange_tree_revision__add_image.py @@@ -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'] diff --cc apps/catalogue/models/__init__.py index 00000000,6161807b..82e1c116 mode 000000,100755..100755 --- a/apps/catalogue/models/__init__.py +++ b/apps/catalogue/models/__init__.py @@@ -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) diff --cc apps/catalogue/models/image.py index 00000000,00000000..53f8830d new file mode 100755 --- /dev/null +++ b/apps/catalogue/models/image.py @@@ -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 diff --cc apps/catalogue/models/listeners.py index 00000000,532f1e79..de1387ee mode 000000,100755..100755 --- a/apps/catalogue/models/listeners.py +++ b/apps/catalogue/models/listeners.py @@@ -1,0 -1,53 +1,58 @@@ + # -*- 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 ++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) + diff --cc apps/catalogue/models/publish_log.py index 00000000,f422e377..6cc86d08 mode 000000,100755..100755 --- a/apps/catalogue/models/publish_log.py +++ b/apps/catalogue/models/publish_log.py @@@ -1,0 -1,39 +1,54 @@@ + # -*- 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 ++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') diff --cc apps/catalogue/templates/catalogue/book_html.html index 00000000,00000000..af4cfa79 new file mode 100755 --- /dev/null +++ b/apps/catalogue/templates/catalogue/book_html.html @@@ -1,0 -1,0 +1,30 @@@ ++{% load i18n %} ++{% load compressed %} ++ ++ ++ ++ ++ {{ book.title }} ++ ++ ++ ++
++ {#% book_info book %#} ++
++ ++ ++ {{ html|safe }} ++ ++ ++ diff --cc apps/catalogue/templates/catalogue/image_list.html index 00000000,00000000..3ff75bc0 new file mode 100755 --- /dev/null +++ b/apps/catalogue/templates/catalogue/image_list.html @@@ -1,0 -1,0 +1,9 @@@ ++{% extends "catalogue/base.html" %} ++ ++{% load i18n %} ++{% load catalogue book_list %} ++ ++ ++{% block content %} ++ {% image_list %} ++{% endblock content %} diff --cc apps/catalogue/templates/catalogue/image_short.html index 00000000,00000000..2e2b386c new file mode 100755 --- /dev/null +++ b/apps/catalogue/templates/catalogue/image_short.html @@@ -1,0 -1,0 +1,18 @@@ ++{% load i18n %} ++ ++ ++ [B] ++ ++ {{ image.title }} ++ {% if image.stage %} ++ {{ image.stage }} ++ {% else %}– ++ {% endif %} ++ {% if image.user %}{{ image.user.first_name }} {{ image.user.last_name }}{% endif %} ++ ++ {% if image.published %}P{% endif %} ++ {% if image.new_publishable %}p{% endif %} ++ {% if image.changed %}+{% endif %} ++ ++ diff --cc apps/catalogue/templates/catalogue/image_table.html index 00000000,00000000..68293e77 new file mode 100755 --- /dev/null +++ b/apps/catalogue/templates/catalogue/image_table.html @@@ -1,0 -1,0 +1,69 @@@ ++{% load i18n %} ++{% load pagination_tags %} ++ ++ ++
++ ++ ++{% if not viewed_user %} ++ ++{% endif %} ++ ++
++ ++ ++ ++ ++ ++ ++ ++ {% if not viewed_user %} ++ ++ {% endif %} ++ ++ ++ ++ ++ ++ {% with cnt=objects|length %} ++ {% autopaginate objects 100 %} ++ ++ {% for item in objects %} ++ {{ item.short_html|safe }} ++ {% endfor %} ++ ++ ++ {% endwith %} ++
++
++ ++
++
++ {% paginate %} ++ {% blocktrans count c=cnt %}{{c}} image{% plural %}{{c}} images{% endblocktrans %}
++{% if not objects %} ++

{% trans "No images found." %}

++{% endif %} diff --cc apps/catalogue/templates/catalogue/upload_pdf.html index 00000000,00000000..a9670e47 new file mode 100755 --- /dev/null +++ b/apps/catalogue/templates/catalogue/upload_pdf.html @@@ -1,0 -1,0 +1,17 @@@ ++{% extends "catalogue/base.html" %} ++{% load i18n %} ++ ++ ++{% block content %} ++ ++ ++

{% trans "PDF file upload" %}

++ ++
++{% csrf_token %} ++{{ form.as_p }} ++

++
++ ++ ++{% endblock content %} diff --cc apps/catalogue/templatetags/book_list.py index 00000000,f7e70474..5e18b7e2 mode 000000,100755..100755 --- a/apps/catalogue/templatetags/book_list.py +++ b/apps/catalogue/templatetags/book_list.py @@@ -1,0 -1,140 +1,195 @@@ + 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 ++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 diff --cc apps/catalogue/templatetags/catalogue.py index 00000000,3cc7210c..0b57b498 mode 000000,100644..100644 --- a/apps/catalogue/templatetags/catalogue.py +++ b/apps/catalogue/templatetags/catalogue.py @@@ -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 --cc apps/catalogue/urls.py index 00000000,ab9b5704..621eb12a mode 000000,100644..100644 --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@@ -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[^/]+)/$', 'image', name="catalogue_image"), ++ + url(r'^catalogue/$', 'document_list', name='catalogue_document_list'), + url(r'^user/$', 'my', name='catalogue_user'), + url(r'^user/(?P[^/]+)/$', 'user', name='catalogue_user'), + url(r'^users/$', 'users', name='catalogue_users'), + url(r'^activity/$', 'activity', name='catalogue_activity'), + url(r'^activity/(?P\d{4}-\d{2}-\d{2})/$', + 'activity', name='catalogue_activity'), + + url(r'^upload/$', + 'upload', name='catalogue_upload'), + + url(r'^create/(?P[^/]*)/', + 'create_missing', name='catalogue_create_missing'), + url(r'^create/', + 'create_missing', name='catalogue_create_missing'), + + url(r'^book/(?P[^/]+)/publish$', 'publish', name="catalogue_publish"), + #url(r'^(?P[^/]+)/publish/(?P\d+)$', 'publish', name="catalogue_publish"), + + url(r'^book/(?P[^/]+)/$', 'book', name="catalogue_book"), + url(r'^book/(?P[^/]+)/xml$', 'book_xml', name="catalogue_book_xml"), + url(r'^book/(?P[^/]+)/txt$', 'book_txt', name="catalogue_book_txt"), + url(r'^book/(?P[^/]+)/html$', 'book_html', name="catalogue_book_html"), + url(r'^book/(?P[^/]+)/epub$', 'book_epub', name="catalogue_book_epub"), + url(r'^book/(?P[^/]+)/pdf$', 'book_pdf', name="catalogue_book_pdf"), + url(r'^chunk_add/(?P[^/]+)/(?P[^/]+)/$', + 'chunk_add', name="catalogue_chunk_add"), + url(r'^chunk_edit/(?P[^/]+)/(?P[^/]+)/$', + 'chunk_edit', name="catalogue_chunk_edit"), + url(r'^book_append/(?P[^/]+)/$', + 'book_append', name="catalogue_book_append"), + + ) diff --cc apps/catalogue/views.py index 00000000,3c0e70ff..3c37ee60 mode 000000,100644..100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@@ -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 --cc apps/wiki/admin.py index 9c32b434,ae309a9d..90da85e6 --- a/apps/wiki/admin.py +++ b/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 --cc apps/wiki_img/forms.py index bc9e2d6a,00000000..555f2647 mode 100644,000000..100644 --- a/apps/wiki_img/forms.py +++ b/apps/wiki_img/forms.py @@@ -1,97 -1,0 +1,20 @@@ +# -*- 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 - from wiki.constants import DOCUMENT_TAGS, DOCUMENT_STAGES +from django.utils.translation import ugettext_lazy as _ ++from wiki.forms import DocumentTextSaveForm ++from catalogue.models import Image + + - class DocumentTagForm(forms.Form): - """ - Form for tagging revisions. - """ ++class ImageSaveForm(DocumentTextSaveForm): ++ """Form for saving document's text.""" + - 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( ++ stage_completed = forms.ModelChoiceField( ++ queryset=Image.tag_model.objects.all(), + required=False, - 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."), ++ label=_(u"Completed"), ++ help_text=_(u"If you completed a life cycle stage, select it."), + ) diff --cc apps/wiki_img/models.py index dd16a87d,00000000..b685324b mode 100644,000000..100644 --- a/apps/wiki_img/models.py +++ b/apps/wiki_img/models.py @@@ -1,29 -1,0 +1,5 @@@ +# -*- 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.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) diff --cc apps/wiki_img/templates/wiki_img/document_details.html index d03a0bfa,00000000..fc2e2076 mode 100644,000000..100644 --- a/apps/wiki_img/templates/wiki_img/document_details.html +++ b/apps/wiki_img/templates/wiki_img/document_details.html @@@ -1,32 -1,0 +1,34 @@@ +{% extends "wiki_img/document_details_base.html" %} +{% load i18n %} + +{% block extrabody %} +{{ block.super }} + + +{% 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 %} + diff --cc apps/wiki_img/templates/wiki_img/document_details_base.html index 30accf26,00000000..8cba7bf3 mode 100644,000000..100644 --- a/apps/wiki_img/templates/wiki_img/document_details_base.html +++ b/apps/wiki_img/templates/wiki_img/document_details_base.html @@@ -1,57 -1,0 +1,52 @@@ +{% extends "base.html" %} +{% load toolbar_tags i18n %} + +{% block title %}{{ document.name }} - {{ block.super }}{% endblock %} +{% block extrahead %} +{% load compressed %} +{% compressed_css 'detail' %} +{% endblock %} + +{% block extrabody %} + +{% compressed_js 'wiki_img' %} +{% endblock %} + +{% block maincontent %} + + + +
+
+ {% block tabs-content %} {% endblock %} +
+
+ +{% block dialogs %} {% endblock %} + +{% endblock %} diff --cc apps/wiki_img/templates/wiki_img/document_details_readonly.html index 71556a19,00000000..ca38838e mode 100644,000000..100644 --- a/apps/wiki_img/templates/wiki_img/document_details_readonly.html +++ b/apps/wiki_img/templates/wiki_img/document_details_readonly.html @@@ -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 }} - - ++ +{% 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 %} diff --cc apps/wiki_img/templates/wiki_img/save_dialog.html index fc239d22,00000000..e8f89e6a mode 100644,000000..100644 --- a/apps/wiki_img/templates/wiki_img/save_dialog.html +++ b/apps/wiki_img/templates/wiki_img/save_dialog.html @@@ -1,24 -1,0 +1,25 @@@ +{% load i18n %} +
+
++ {% csrf_token %} +

{{ forms.text_save.comment.label }}

+

+ {{ forms.text_save.comment.help_text}} + +

+ {{forms.text_save.comment }} + + + + {% for f in forms.text_save.hidden_fields %} + {{ f }} + {% endfor %} + +

+ +

+ + +

+
+
diff --cc apps/wiki_img/templates/wiki_img/tabs/history_view.html index 00000000,00000000..db49d648 new file mode 100755 --- /dev/null +++ b/apps/wiki_img/templates/wiki_img/tabs/history_view.html @@@ -1,0 -1,0 +1,40 @@@ ++{% load i18n %} ++ diff --cc apps/wiki_img/urls.py index 075e5adf,00000000..b4396c6c mode 100644,000000..100644 --- a/apps/wiki_img/urls.py +++ b/apps/wiki_img/urls.py @@@ -1,20 -1,0 +1,18 @@@ +# -*- coding: utf-8 +from django.conf.urls.defaults import * - from django.conf import settings - from django.views.generic.list_detail import object_list + - from wiki_img.models import ImageDocument - - - PART = ur"""[ ĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9\w_.-]+""" + +urlpatterns = patterns('wiki_img.views', - url(r'^$', object_list, {'queryset': ImageDocument.objects.all(), "template_name": "wiki_img/document_list.html"}), - - url(r'^edit/(?P%s)$' % PART, ++ url(r'^edit/(?P[^/]+)/$', + 'editor', name="wiki_img_editor"), + - url(r'^(?P[^/]+)/text$', ++ url(r'^readonly/(?P[^/]+)/$', ++ 'editor_readonly', name="wiki_img_editor_readonly"), ++ ++ url(r'^text/(?P\d+)/$', + 'text', name="wiki_img_text"), + ++ url(r'^history/(?P\d+)/$', ++ 'history', name="wiki_history"), ++ +) diff --cc apps/wiki_img/views.py index c4d32b78,00000000..9e87f660 mode 100644,000000..100644 --- a/apps/wiki_img/views.py +++ b/apps/wiki_img/views.py @@@ -1,70 -1,0 +1,129 @@@ +import os +import functools +import logging - logger = logging.getLogger("fnp.wiki") ++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 + - from wiki_img.models import ImageDocument ++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'): - doc = get_object_or_404(ImageDocument, slug=slug) ++ doc = get_object_or_404(Image, slug=slug) + + return direct_to_template(request, template_name, extra_context={ + 'document': doc, + 'forms': { - "text_save": DocumentTextSaveForm(prefix="textsave"), ++ "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 - def text(request, slug): ++def text(request, image_id): ++ doc = get_object_or_404(Image, pk=image_id) + if request.method == 'POST': - form = DocumentTextSaveForm(request.POST, prefix="textsave") ++ form = DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave") + if form.is_valid(): - document = get_object_or_404(ImageDocument, slug=slug) - commit = form.cleaned_data['parent_commit'] - - comment = form.cleaned_data['comment'] - + if request.user.is_authenticated(): - user = request.user ++ author = request.user + else: - user = None - - document.doc.commit( - parent=commit, - text=form.cleaned_data['text'], - author=user, - description=comment - ) - ++ 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({ - 'text': document.doc.materialize(), - 'revision': document.doc.change_set.count(), ++ 'text': doc.materialize() if parent_revision != revision else None, ++ 'meta': {}, ++ 'revision': revision, + }) + else: + return JSONFormInvalid(form) + else: - doc = get_object_or_404(ImageDocument, slug=slug).doc ++ 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': doc.materialize(), - 'revision': doc.change_set.count(), - 'commit': doc.head.id, ++ '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) diff --cc redakcja/settings/common.py index 5acf2658,1e16bb1e..cc7765e6 --- a/redakcja/settings/common.py +++ b/redakcja/settings/common.py @@@ -117,13 -112,25 +112,26 @@@ INSTALLED_APPS = '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 = [] diff --cc redakcja/settings/compress.py index 714e4c2a,d4425c21..34cfa2ff --- a/redakcja/settings/compress.py +++ b/redakcja/settings/compress.py @@@ -58,42 -58,13 +59,44 @@@ COMPRESS_JS = ), 'output_filename': 'compressed/detail_scripts_?.js', }, + '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', + }, - 'listing': { + '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', } } diff --cc redakcja/static/js/wiki_img/loader.js index 41a029d7,00000000..9cfc640d mode 100644,000000..100644 --- a/redakcja/static/js/wiki_img/loader.js +++ b/redakcja/static/js/wiki_img/loader.js @@@ -1,159 -1,0 +1,136 @@@ +if (!window.console) { + window.console = { + log: function(){ + } + } +} + - DEFAULT_PERSPECTIVE = "#SummaryPerspective"; ++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); +}); + + - // 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); - } - } - - diff --cc redakcja/static/js/wiki_img/loader_readonly.js index 00000000,00000000..1ce15b77 new file mode 100755 --- /dev/null +++ b/redakcja/static/js/wiki_img/loader_readonly.js @@@ -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); ++}); diff --cc redakcja/static/js/wiki_img/wikiapi.js index 0990e604,00000000..0f56ffe7 mode 100644,000000..100644 --- a/redakcja/static/js/wiki_img/wikiapi.js +++ b/redakcja/static/js/wiki_img/wikiapi.js @@@ -1,310 -1,0 +1,350 @@@ +(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") { - var path = "/" + arguments[1] + "/text"; - - if (arguments[2] !== undefined) - path += "/" + arguments[2]; - - return base_path + path; ++ return base_path + "/text/" + arguments[1] + "/"; + } + - /*if (vname == "ajax_document_history") { ++ if (vname == "ajax_document_history") { + - return base_path + "/" + arguments[1] + "/history"; ++ return base_path + "/history/" + arguments[1] + "/"; + } +*/ - if (vname == "ajax_document_gallery") { - - return base_path + "/" + arguments[1] + "/gallery"; - } +/* + 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.id = meta.attr('data-document-name'); ++ this.id = meta.attr('data-object-id'); + + this.revision = $("*[data-key='revision']", meta).text(); - this.commit = $("*[data-key='commit']", meta).text(); + this.readonly = !!$("*[data-key='readonly']", meta).text(); + - this.galleryLink = $("*[data-key='gallery']", meta).text(); - this.galleryImages = []; + 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 = ''; + } + 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: "

Nie udało się zapisać zmian.

" + }) + } + else { + try { + params['failure'](self, $.parseJSON(xhr.responseText)); + } + catch (e) { + params['failure'](self, { + "__message": "

Nie udało się zapisać - błąd serwera.

" + }); + }; + } + + } + }); + + $('#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 --cc redakcja/urls.py index 08073a41,4aedf58f..2343b05d --- a/redakcja/urls.py +++ b/redakcja/urls.py @@@ -18,9 -20,12 +20,13 @@@ urlpatterns = patterns('' 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.+)$' % settings.MEDIA_URL[1:], 'django.views.static.serve',