From 6ef9b5840f09dc8816cf054fc498ef7fa84c5465 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Nov 2011 11:57:18 -0400 Subject: [PATCH 01/92] Removed extra slash in ajax-loader.gif URL fixes #15, thanks to IHLeanne for finding this one --- apps/sources/widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sources/widgets.py b/apps/sources/widgets.py index 665303e427..ae5bf8e1bc 100644 --- a/apps/sources/widgets.py +++ b/apps/sources/widgets.py @@ -28,9 +28,9 @@ class FamFamRadioSelect(forms.widgets.RadioSelect): def staging_file_thumbnail(staging_file): try: staging_file.get_valid_image() - template = u'%(string)s' + template = u'%(string)s' except: - template = u'%(string)s' + template = u'%(string)s' return mark_safe(template % { 'url': reverse('staging_file_preview', args=[staging_file.source.source_type, staging_file.source.pk, staging_file.id]), From 0fc285390375e6bdf6077e8a4e46fc0cfc576d5a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Nov 2011 12:00:23 -0400 Subject: [PATCH 02/92] Updated contributors file --- docs/contributors.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/contributors.rst b/docs/contributors.rst index f6179efb07..cae85f3c13 100644 --- a/docs/contributors.rst +++ b/docs/contributors.rst @@ -12,6 +12,9 @@ You can help further the development of **Mayan EDMS** by reporting bugs, submit Bug fixes --------- * Aziz M. Bookwala (https://github.com/azizmb) +* IHLeanne (https://github.com/IHLeanne) +* Сергей Глита [Sergey Glita] (s.v.glita@gmail.com) +* Meurig Freeman (https://github.com/meurig) Bug reports ----------- @@ -21,6 +24,7 @@ Bug reports * Brian Huxley * dAnjou (https://github.com/dAnjou) * Сергей Глита [Sergey Glita] (s.v.glita@gmail.com) +* IHLeanne (https://github.com/IHLeanne) Patches ------- From 188c7f1efd8b0d768aa993df29a2c3c9d14b5dfa Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Nov 2011 08:52:00 -0400 Subject: [PATCH 03/92] Added South to the requirements --- requirements/development.txt | 1 + requirements/production.txt | 1 + settings.py | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements/development.txt b/requirements/development.txt index d363fd07d9..7dc2ed08bc 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -19,3 +19,4 @@ cssmin==0.1.4 django-compressor==1.1 -e git://github.com/rosarior/django-sendfile.git#egg=django-sendfile djangorestframework==0.2.3 +South==0.7.3 diff --git a/requirements/production.txt b/requirements/production.txt index e2654a82b1..d0438944d1 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -16,3 +16,4 @@ cssmin==0.1.4 django-compressor==1.1 -e git://github.com/rosarior/django-sendfile.git#egg=django-sendfile djangorestframework==0.2.3 +South==0.7.3 diff --git a/settings.py b/settings.py index 27ef8c818f..c31e499fb0 100644 --- a/settings.py +++ b/settings.py @@ -133,7 +133,6 @@ INSTALLED_APPS = ( 'lock_manager', 'web_theme', 'common', - 'metadata', 'pagination', 'dynamic_search', 'filetransfers', @@ -151,6 +150,7 @@ INSTALLED_APPS = ( 'tags', 'document_comments', 'user_management', + 'metadata', 'documents', 'linking', 'mptt', @@ -165,6 +165,7 @@ INSTALLED_APPS = ( 'compressor', 'djangorestframework', 'rest_api', + 'south', ) TEMPLATE_CONTEXT_PROCESSORS = ( From c6f85342081280eb9fecbcddc50709165db140f7 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Nov 2011 08:52:18 -0400 Subject: [PATCH 04/92] Initial migrations for the documents app These migrations are to merge the filename and extension fields into a single filename field Mayan can't assume filesystem convetion of extension separator as the OS where the document was uploaded might be differente that the OS running Mayan --- apps/documents/migrations/0001_initial.py | 219 ++++++++++++++++++ .../0002_filename_extension_merge.py | 146 ++++++++++++ ...auto__del_field_document_file_extension.py | 144 ++++++++++++ apps/documents/migrations/__init__.py | 0 4 files changed, 509 insertions(+) create mode 100644 apps/documents/migrations/0001_initial.py create mode 100644 apps/documents/migrations/0002_filename_extension_merge.py create mode 100644 apps/documents/migrations/0003_auto__del_field_document_file_extension.py create mode 100644 apps/documents/migrations/__init__.py diff --git a/apps/documents/migrations/0001_initial.py b/apps/documents/migrations/0001_initial.py new file mode 100644 index 0000000000..25acbc89de --- /dev/null +++ b/apps/documents/migrations/0001_initial.py @@ -0,0 +1,219 @@ +# 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 'DocumentType' + db.create_table('documents_documenttype', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=32)), + )) + db.send_create_signal('documents', ['DocumentType']) + + # Adding model 'Document' + db.create_table('documents_document', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('document_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.DocumentType'], null=True, blank=True)), + ('file', self.gf('django.db.models.fields.files.FileField')(max_length=100)), + ('uuid', self.gf('django.db.models.fields.CharField')(default=u'107e50a8-83b3-46da-bd14-460489527ab1', max_length=48, blank=True)), + ('file_mimetype', self.gf('django.db.models.fields.CharField')(default='', max_length=64)), + ('file_mime_encoding', self.gf('django.db.models.fields.CharField')(default='', max_length=64)), + ('file_filename', self.gf('django.db.models.fields.CharField')(default=u'', max_length=255, db_index=True)), + ('file_extension', self.gf('django.db.models.fields.CharField')(default=u'', max_length=16, db_index=True)), + ('date_added', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)), + ('date_updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('checksum', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(db_index=True, null=True, blank=True)), + )) + db.send_create_signal('documents', ['Document']) + + # Adding model 'DocumentTypeFilename' + db.create_table('documents_documenttypefilename', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('document_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.DocumentType'])), + ('filename', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=True)), + )) + db.send_create_signal('documents', ['DocumentTypeFilename']) + + # Adding model 'DocumentPage' + db.create_table('documents_documentpage', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('document', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.Document'])), + ('content', self.gf('django.db.models.fields.TextField')(db_index=True, null=True, blank=True)), + ('page_label', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)), + ('page_number', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True)), + )) + db.send_create_signal('documents', ['DocumentPage']) + + # Adding model 'DocumentPageTransformation' + db.create_table('documents_documentpagetransformation', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('document_page', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.DocumentPage'])), + ('order', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, null=True, db_index=True, blank=True)), + ('transformation', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('arguments', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('documents', ['DocumentPageTransformation']) + + # Adding model 'RecentDocument' + db.create_table('documents_recentdocument', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('document', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.Document'])), + ('datetime_accessed', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), + )) + db.send_create_signal('documents', ['RecentDocument']) + + + def backwards(self, orm): + + # Deleting model 'DocumentType' + db.delete_table('documents_documenttype') + + # Deleting model 'Document' + db.delete_table('documents_document') + + # Deleting model 'DocumentTypeFilename' + db.delete_table('documents_documenttypefilename') + + # Deleting model 'DocumentPage' + db.delete_table('documents_documentpage') + + # Deleting model 'DocumentPageTransformation' + db.delete_table('documents_documentpagetransformation') + + # Deleting model 'RecentDocument' + db.delete_table('documents_recentdocument') + + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'file_extension': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '16', 'db_index': 'True'}), + 'file_filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'file_mime_encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file_mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "u'107e50a8-83b3-46da-bd14-460489527ab1'", 'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] diff --git a/apps/documents/migrations/0002_filename_extension_merge.py b/apps/documents/migrations/0002_filename_extension_merge.py new file mode 100644 index 0000000000..6d7dc4182b --- /dev/null +++ b/apps/documents/migrations/0002_filename_extension_merge.py @@ -0,0 +1,146 @@ +# encoding: utf-8 +import os +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + for document in orm.Document.objects.all(): + document.file_filename = os.extsep.join([document.file_filename, document.file_extension]) + document.save() + + def backwards(self, orm): + "Write your backwards methods here." + for document in orm.Document.objects.all(): + document.file_filename, document.file_extension = document.file_filename.split(os.extsep) + document.save() + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'file_extension': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '16', 'db_index': 'True'}), + 'file_filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'file_mime_encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file_mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "u'28bd60c6-a5c2-4adb-8dab-1b6c0098cc9c'", 'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] diff --git a/apps/documents/migrations/0003_auto__del_field_document_file_extension.py b/apps/documents/migrations/0003_auto__del_field_document_file_extension.py new file mode 100644 index 0000000000..85f3a1821f --- /dev/null +++ b/apps/documents/migrations/0003_auto__del_field_document_file_extension.py @@ -0,0 +1,144 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Deleting field 'Document.file_extension' + db.delete_column('documents_document', 'file_extension') + + + def backwards(self, orm): + + # Adding field 'Document.file_extension' + db.add_column('documents_document', 'file_extension', self.gf('django.db.models.fields.CharField')(default=u'', max_length=16, db_index=True), keep_default=False) + + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'file_filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'file_mime_encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file_mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "u'06a88ff6-11b2-44b3-8409-21bd58577d4f'", 'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] diff --git a/apps/documents/migrations/__init__.py b/apps/documents/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From f5546556885068a3d85c7bfd0d81b192d37d4b0d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Nov 2011 08:54:37 -0400 Subject: [PATCH 05/92] Updated documents app admin, model and view modules to use the merged filename and extension --- apps/documents/admin.py | 2 +- apps/documents/models.py | 11 +++-------- apps/documents/views.py | 5 ++--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/documents/admin.py b/apps/documents/admin.py index ff4aa33cb0..71d0fab939 100644 --- a/apps/documents/admin.py +++ b/apps/documents/admin.py @@ -35,7 +35,7 @@ class DocumentAdmin(admin.ModelAdmin): inlines = [ DocumentMetadataInline, DocumentPageInline ] - list_display = ('uuid', 'file_filename', 'file_extension') + list_display = ('uuid', 'file_filename',) class RecentDocumentAdmin(admin.ModelAdmin): diff --git a/apps/documents/models.py b/apps/documents/models.py index 914426a568..14117269a0 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -46,10 +46,7 @@ def get_filename_from_uuid(instance, filename): Store the orignal filename of the uploaded file and replace it with a UUID """ - filename, extension = os.path.splitext(filename) instance.file_filename = filename - #remove prefix '.' - instance.file_extension = extension[1:] uuid = UUID_FUNCTION() instance.uuid = uuid return uuid @@ -82,7 +79,6 @@ class Document(models.Model): file_mime_encoding = models.CharField(max_length=64, default='', editable=False) #FAT filename can be up to 255 using LFN file_filename = models.CharField(max_length=255, default=u'', editable=False, db_index=True) - file_extension = models.CharField(max_length=16, default=u'', editable=False, db_index=True) date_added = models.DateTimeField(verbose_name=_(u'added'), auto_now_add=True, db_index=True) date_updated = models.DateTimeField(verbose_name=_(u'updated'), auto_now=True) checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False) @@ -102,7 +98,7 @@ class Document(models.Model): ordering = ['-date_added'] def __unicode__(self): - return os.extsep.join([self.file_filename, self.file_extension]) + return self.get_fullname() def save(self, *args, **kwargs): """ @@ -130,7 +126,7 @@ class Document(models.Model): """ Return the fullname of the document's file """ - return os.extsep.join([self.file_filename, self.file_extension]) + return self.file_filename def update_mimetype(self, save=True): """ @@ -420,7 +416,6 @@ register('document', Document, _(u'document'), [ {'name': u'document_type__name', 'title': _(u'Document type')}, {'name': u'file_mimetype', 'title': _(u'MIME type')}, {'name': u'file_filename', 'title': _(u'Filename')}, - {'name': u'file_extension', 'title': _(u'Filename extension')}, {'name': u'documentmetadata__value', 'title': _(u'Metadata value')}, {'name': u'documentpage__content', 'title': _(u'Content')}, {'name': u'description', 'title': _(u'Description')}, @@ -428,4 +423,4 @@ register('document', Document, _(u'document'), [ {'name': u'comments__comment', 'title': _(u'Comments')}, ] ) -#register(Document, _(u'document'), ['document_type__name', 'file_mimetype', 'file_extension', 'documentmetadata__value', 'documentpage__content', 'description', {'field_name':'file_filename', 'comparison':'iexact'}]) +#register(Document, _(u'document'), ['document_type__name', 'file_mimetype', 'documentmetadata__value', 'documentpage__content', 'description', {'field_name':'file_filename', 'comparison':'iexact'}]) diff --git a/apps/documents/views.py b/apps/documents/views.py index 60a03ee74c..68a70f2435 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -65,7 +65,7 @@ def document_list(request, object_list=None, title=None, extra_context=None): check_permissions(request.user, [PERMISSION_DOCUMENT_VIEW]) context = { - 'object_list': object_list if not (object_list is None) else Document.objects.only('file_filename', 'file_extension').all(), + 'object_list': object_list if not (object_list is None) else Document.objects.only('file_filename',).all(), 'title': title if title else _(u'documents'), 'multi_select_as_buttons': True, 'hide_links': True, @@ -115,7 +115,6 @@ def document_view(request, document_id, advanced=False): if advanced: document_properties_form = DocumentPropertiesForm(instance=document, extra_fields=[ {'label': _(u'Filename'), 'field': 'file_filename'}, - {'label': _(u'File extension'), 'field': 'file_extension'}, {'label': _(u'File mimetype'), 'field': 'file_mimetype'}, {'label': _(u'File mime encoding'), 'field': 'file_mime_encoding'}, {'label': _(u'File size'), 'field':lambda x: pretty_size(x.size) if x.size else '-'}, @@ -482,7 +481,7 @@ def document_update_page_count(request): previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) office_converter = OfficeConverter() - qs = Document.objects.exclude(file_extension__iendswith='dxf').filter(file_mimetype__in=office_converter.mimetypes()) + qs = Document.objects.exclude(file_filename__iendswith='dxf').filter(file_mimetype__in=office_converter.mimetypes()) if request.method == 'POST': updated = 0 From d37b36bce3dd7f6892a80d5bee892d059f53a4ec Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Nov 2011 08:55:19 -0400 Subject: [PATCH 06/92] Updated document indexing app modules for the new filename and extension merged field --- apps/document_indexing/api.py | 3 ++- apps/document_indexing/filesystem.py | 9 ++------- apps/document_indexing/os_agnostic.py | 14 +++++++++++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/document_indexing/api.py b/apps/document_indexing/api.py index b2e166f347..972b903210 100644 --- a/apps/document_indexing/api.py +++ b/apps/document_indexing/api.py @@ -127,7 +127,8 @@ def do_rebuild_all_indexes(): # Internal functions def find_lowest_available_suffix(index_instance, document): - index_instance_documents = DocumentRenameCount.objects.filter(index_instance=index_instance).filter(document__file_extension=document.file_extension) + # TODO: verify extension's role in query + index_instance_documents = DocumentRenameCount.objects.filter(index_instance=index_instance)#.filter(document__file_extension=document.file_extension) files_list = [] for index_instance_document in index_instance_documents: files_list.append(assemble_document_filename(index_instance_document.document, index_instance_document.suffix)) diff --git a/apps/document_indexing/filesystem.py b/apps/document_indexing/filesystem.py index 04744f4254..f8762060ce 100644 --- a/apps/document_indexing/filesystem.py +++ b/apps/document_indexing/filesystem.py @@ -36,8 +36,7 @@ def fs_create_index_directory(index_instance): def fs_create_document_link(index_instance, document, suffix=0): if FILESERVING_ENABLE: - name_part = assemble_document_filename(document, suffix) - filename = os.extsep.join([name_part, document.file_extension]) + filename = assemble_document_filename(document.file_filename, suffix) filepath = os.path.join(FILESERVING_PATH, get_instance_path(index_instance), filename) try: os.symlink(document.file.path, filepath) @@ -56,11 +55,7 @@ def fs_create_document_link(index_instance, document, suffix=0): def fs_delete_document_link(index_instance, document, suffix=0): if FILESERVING_ENABLE: - name_part = document.file_filename - if suffix: - name_part = u'_'.join([name_part, unicode(suffix)]) - - filename = os.extsep.join([name_part, document.file_extension]) + filename = assemble_document_filename(document.file_filename, suffix) filepath = os.path.join(FILESERVING_PATH, get_instance_path(index_instance), filename) try: diff --git a/apps/document_indexing/os_agnostic.py b/apps/document_indexing/os_agnostic.py index ac8ef648ec..f33550609a 100644 --- a/apps/document_indexing/os_agnostic.py +++ b/apps/document_indexing/os_agnostic.py @@ -1,8 +1,16 @@ +import os + from document_indexing.conf.settings import SUFFIX_SEPARATOR -def assemble_document_filename(document, suffix=0): +def assemble_document_filename(filename, suffix=0): + ''' + Split document filename, to attach suffix to the name part then + re attacht the extension + ''' + if suffix: - return SUFFIX_SEPARATOR.join([document.file_filename, unicode(suffix)]) + name, extension = filename.split(os.split(os.extsep)) + return SUFFIX_SEPARATOR.join([name, unicode(suffix), os.extsep, extension]) else: - return document.file_filename + return file_filename From a646997ab85405de19a1f75a2eff08626bbc05aa Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Nov 2011 09:31:32 -0400 Subject: [PATCH 07/92] Rename os_agnostic.py to the more appropiate os_specifics to encapsulate file system unique filename and path handling --- apps/document_indexing/{os_agnostic.py => os_specifics.py} | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) rename apps/document_indexing/{os_agnostic.py => os_specifics.py} (73%) diff --git a/apps/document_indexing/os_agnostic.py b/apps/document_indexing/os_specifics.py similarity index 73% rename from apps/document_indexing/os_agnostic.py rename to apps/document_indexing/os_specifics.py index f33550609a..90a9ba8a75 100644 --- a/apps/document_indexing/os_agnostic.py +++ b/apps/document_indexing/os_specifics.py @@ -3,7 +3,7 @@ import os from document_indexing.conf.settings import SUFFIX_SEPARATOR -def assemble_document_filename(filename, suffix=0): +def assemble_suffixed_filename(filename, suffix=0): ''' Split document filename, to attach suffix to the name part then re attacht the extension @@ -14,3 +14,7 @@ def assemble_document_filename(filename, suffix=0): return SUFFIX_SEPARATOR.join([name, unicode(suffix), os.extsep, extension]) else: return file_filename + + +def assemble_path_from_list(directory_list): + return os.sep.join(directory_list) From 1e22d1099e48f9318a7a337de168d52d1b5987e9 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Nov 2011 09:33:32 -0400 Subject: [PATCH 08/92] Update api and filesystem modules to new os_specifics renames --- apps/document_indexing/api.py | 6 +++--- apps/document_indexing/filesystem.py | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/document_indexing/api.py b/apps/document_indexing/api.py index 972b903210..269d5ceddd 100644 --- a/apps/document_indexing/api.py +++ b/apps/document_indexing/api.py @@ -15,7 +15,7 @@ from document_indexing.filesystem import fs_create_index_directory, \ fs_create_document_link, fs_delete_document_link, \ fs_delete_index_directory, fs_delete_directory_recusive from document_indexing.conf.settings import SLUGIFY_PATHS -from document_indexing.os_agnostic import assemble_document_filename +from document_indexing.os_specifics import assemble_suffixed_filename if SLUGIFY_PATHS == False: # Do not slugify path or filenames and extensions @@ -131,10 +131,10 @@ def find_lowest_available_suffix(index_instance, document): index_instance_documents = DocumentRenameCount.objects.filter(index_instance=index_instance)#.filter(document__file_extension=document.file_extension) files_list = [] for index_instance_document in index_instance_documents: - files_list.append(assemble_document_filename(index_instance_document.document, index_instance_document.suffix)) + files_list.append(assemble_suffixed_filename(index_instance_document.document.file.name, index_instance_document.suffix)) for suffix in xrange(MAX_SUFFIX_COUNT): - if assemble_document_filename(document, suffix) not in files_list: + if assemble_suffixed_filename(document.file.name, suffix) not in files_list: return suffix raise MaxSuffixCountReached(ugettext(u'Maximum suffix (%s) count reached.') % MAX_SUFFIX_COUNT) diff --git a/apps/document_indexing/filesystem.py b/apps/document_indexing/filesystem.py index f8762060ce..c89dad4cf6 100644 --- a/apps/document_indexing/filesystem.py +++ b/apps/document_indexing/filesystem.py @@ -3,7 +3,8 @@ import os from django.utils.translation import ugettext_lazy as _ -from document_indexing.os_agnostic import assemble_document_filename +from document_indexing.os_specifics import (assemble_suffixed_filename, + assemble_path_from_list) from document_indexing.conf.settings import FILESERVING_ENABLE from document_indexing.conf.settings import FILESERVING_PATH @@ -19,12 +20,12 @@ def get_instance_path(index_instance): names.append(index_instance.value) - return os.sep.join(names) + return assemble_path_from_list(names) def fs_create_index_directory(index_instance): if FILESERVING_ENABLE: - target_directory = os.path.join(FILESERVING_PATH, get_instance_path(index_instance)) + target_directory = assemble_path_from_list([FILESERVING_PATH, get_instance_path(index_instance)]) try: os.mkdir(target_directory) except OSError, exc: @@ -36,8 +37,9 @@ def fs_create_index_directory(index_instance): def fs_create_document_link(index_instance, document, suffix=0): if FILESERVING_ENABLE: - filename = assemble_document_filename(document.file_filename, suffix) - filepath = os.path.join(FILESERVING_PATH, get_instance_path(index_instance), filename) + filename = assemble_suffixed_filename(document.file.name, suffix) + filepath = assemble_path_from_list([FILESERVING_PATH, get_instance_path(index_instance), filename]) + try: os.symlink(document.file.path, filepath) except OSError, exc: @@ -55,8 +57,8 @@ def fs_create_document_link(index_instance, document, suffix=0): def fs_delete_document_link(index_instance, document, suffix=0): if FILESERVING_ENABLE: - filename = assemble_document_filename(document.file_filename, suffix) - filepath = os.path.join(FILESERVING_PATH, get_instance_path(index_instance), filename) + filename = assemble_suffixed_filename(document.file.name, suffix) + filepath = assemble_path_from_list([FILESERVING_PATH, get_instance_path(index_instance), filename]) try: os.unlink(filepath) @@ -68,7 +70,7 @@ def fs_delete_document_link(index_instance, document, suffix=0): def fs_delete_index_directory(index_instance): if FILESERVING_ENABLE: - target_directory = os.path.join(FILESERVING_PATH, get_instance_path(index_instance)) + target_directory = assemble_path_from_list([FILESERVING_PATH, get_instance_path(index_instance)]) try: os.removedirs(target_directory) except OSError, exc: From 2b7c379822f12579016d2e577d3d14da2ef37d1d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Nov 2011 09:45:29 -0400 Subject: [PATCH 09/92] Add new office document mimetype * application/vnd.ms-office --- apps/converter/office_converter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/converter/office_converter.py b/apps/converter/office_converter.py index dac7ea3d69..814a69bb24 100644 --- a/apps/converter/office_converter.py +++ b/apps/converter/office_converter.py @@ -26,6 +26,7 @@ CONVERTER_OFFICE_FILE_MIMETYPES = [ 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.oasis.opendocument.graphics', + 'application/vnd.ms-office', ] logger = logging.getLogger(__name__) From f76baa4efc762b3737459114bafc6eee55ba4bfc Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Nov 2011 20:03:46 -0400 Subject: [PATCH 10/92] Changed office documents mimetypes strings to unicode --- apps/converter/office_converter.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/converter/office_converter.py b/apps/converter/office_converter.py index 814a69bb24..37a5d50ffc 100644 --- a/apps/converter/office_converter.py +++ b/apps/converter/office_converter.py @@ -13,20 +13,20 @@ from converter.exceptions import (OfficeConversionError, CACHED_FILE_SUFFIX = u'_office_converter' CONVERTER_OFFICE_FILE_MIMETYPES = [ - 'application/msword', - 'application/mswrite', - 'application/mspowerpoint', - 'application/msexcel', - 'application/vnd.ms-excel', - 'application/vnd.ms-powerpoint', - 'text/plain', - 'application/vnd.oasis.opendocument.presentation', - 'application/vnd.oasis.opendocument.text', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.oasis.opendocument.spreadsheet', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.oasis.opendocument.graphics', - 'application/vnd.ms-office', + u'application/msword', + u'application/mswrite', + u'application/mspowerpoint', + u'application/msexcel', + u'application/vnd.ms-excel', + u'application/vnd.ms-powerpoint', + u'text/plain', + u'application/vnd.oasis.opendocument.presentation', + u'application/vnd.oasis.opendocument.text', + u'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + u'application/vnd.oasis.opendocument.spreadsheet', + u'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + u'application/vnd.oasis.opendocument.graphics', + u'application/vnd.ms-office', ] logger = logging.getLogger(__name__) From 394762047a9fb2db8d174b1ed03d6d310c797bc4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Nov 2011 20:04:29 -0400 Subject: [PATCH 11/92] Fix documents not saving the file encoding --- apps/documents/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index 14117269a0..2e5a627a73 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -135,7 +135,7 @@ class Document(models.Model): """ if self.exists(): try: - self.file_mimetype, self.mime_encoding = get_mimetype(self.open(), self.get_fullname()) + self.file_mimetype, self.file_mime_encoding = get_mimetype(self.open(), self.get_fullname()) except: self.file_mimetype = u'' self.file_mime_encoding = u'' From a4513951edd2fd5d1c2ef61e1136ba4a6ddefcd0 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 02:15:15 -0400 Subject: [PATCH 12/92] Add '=' to unoconv command line --- apps/converter/office_converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/converter/office_converter.py b/apps/converter/office_converter.py index 37a5d50ffc..575c364a14 100644 --- a/apps/converter/office_converter.py +++ b/apps/converter/office_converter.py @@ -29,6 +29,7 @@ CONVERTER_OFFICE_FILE_MIMETYPES = [ u'application/vnd.ms-office', ] + logger = logging.getLogger(__name__) @@ -92,7 +93,7 @@ class OfficeConverterBackendUnoconv(object): command.append(self.unoconv_path) if UNOCONV_USE_PIPE: - command.append(u'--pipe') + command.append(u'--pipe=') command.append(u'mayan-%s' % id_generator()) command.append(u'--format=pdf') From cf9b28ad309040ee73be23dd63f68996a29437f5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 04:30:38 -0400 Subject: [PATCH 13/92] Separate unoconv command line arguments and add further logging --- apps/converter/office_converter.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/converter/office_converter.py b/apps/converter/office_converter.py index 575c364a14..c7c8707c36 100644 --- a/apps/converter/office_converter.py +++ b/apps/converter/office_converter.py @@ -93,22 +93,24 @@ class OfficeConverterBackendUnoconv(object): command.append(self.unoconv_path) if UNOCONV_USE_PIPE: - command.append(u'--pipe=') + command.append(u'--pipe') command.append(u'mayan-%s' % id_generator()) - command.append(u'--format=pdf') - command.append(u'--output=%s' % self.output_filepath) + command.append(u'--format') + command.append(u'pdf') + command.append(u'--output') + command.append(self.output_filepath) command.append(self.input_filepath) try: - logger.debug('prev environment: %s' % os.environ) proc = subprocess.Popen(command, close_fds=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) return_code = proc.wait() - logger.debug('post environment: %s' % os.environ) + logger.debug('return_code: %s' % return_code) readline = proc.stderr.readline() + logger.debug('stderr: %s' % readline) if return_code != 0: - raise OfficeBackendError(proc.stderr.readline()) + raise OfficeBackendError(readline) except OSError, msg: raise OfficeBackendError(msg) except Exception, msg: From 8a2d526e943a8b13ac15760d9a013eb14e2e5d51 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 04:31:52 -0400 Subject: [PATCH 14/92] Add RTF text file to the list of office document mimetypes --- apps/converter/office_converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/converter/office_converter.py b/apps/converter/office_converter.py index c7c8707c36..e98b8cfdd4 100644 --- a/apps/converter/office_converter.py +++ b/apps/converter/office_converter.py @@ -19,7 +19,6 @@ CONVERTER_OFFICE_FILE_MIMETYPES = [ u'application/msexcel', u'application/vnd.ms-excel', u'application/vnd.ms-powerpoint', - u'text/plain', u'application/vnd.oasis.opendocument.presentation', u'application/vnd.oasis.opendocument.text', u'application/vnd.openxmlformats-officedocument.wordprocessingml.document', @@ -27,6 +26,8 @@ CONVERTER_OFFICE_FILE_MIMETYPES = [ u'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', u'application/vnd.oasis.opendocument.graphics', u'application/vnd.ms-office', + u'text/plain', + u'text/rtf', ] From cb8b4a4def127fc8b293dee140117f5b633873db Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 04:43:59 -0400 Subject: [PATCH 15/92] Add more logging --- apps/lock_manager/managers.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/lock_manager/managers.py b/apps/lock_manager/managers.py index 6c69fada19..f356be42dd 100644 --- a/apps/lock_manager/managers.py +++ b/apps/lock_manager/managers.py @@ -20,7 +20,8 @@ class LockManager(models.Manager): lock.save(force_insert=True) logger.debug('acquired lock: %s' % name) return lock - except IntegrityError: + except IntegrityError, msg: + logger.debug('IntegrityError: %s', msg) # There is already an existing lock # Check it's expiration date and if expired, reset it try: @@ -32,9 +33,10 @@ class LockManager(models.Manager): if datetime.datetime.now() > lock.creation_datetime + datetime.timedelta(seconds=lock.timeout): logger.debug('reseting deleting stale lock: %s' % name) - lock.timeout=timeout - logger.debug('try to reacquire stale lock: %s' % name) + lock.timeout = timeout + logger.debug('trying to reacquire stale lock: %s' % name) lock.save() + logger.debug('reacquired stale lock: %s' % name) return lock else: logger.debug('unable to acquire lock: %s' % name) From 29f547ee48941f3ca558659b63a3035f04123ee7 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 04:44:38 -0400 Subject: [PATCH 16/92] Update the queue signal processor to only trigger ocr queue processing on newly submitted documents and not requeued documents --- apps/ocr/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/ocr/__init__.py b/apps/ocr/__init__.py index cb60eadc57..def0d7064b 100644 --- a/apps/ocr/__init__.py +++ b/apps/ocr/__init__.py @@ -110,8 +110,9 @@ post_save.connect(document_post_save, sender=Document) @receiver(post_save, dispatch_uid='call_queue', sender=QueueDocument) def call_queue(sender, **kwargs): - logger.debug('got call_queue signal') - task_process_document_queues() + if kwargs.get('created', False): + logger.debug('got call_queue signal: %s' % kwargs) + task_process_document_queues() create_default_queue() From 31ea558b6033cfcee71aaed3fb38d7952573d559 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 04:46:00 -0400 Subject: [PATCH 17/92] Update the REPLICATION_DELAY default to be 0 seconds --- apps/ocr/conf/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ocr/conf/settings.py b/apps/ocr/conf/settings.py index bfc285f9b2..ff2f7ca04d 100644 --- a/apps/ocr/conf/settings.py +++ b/apps/ocr/conf/settings.py @@ -10,7 +10,7 @@ register_settings( settings=[ {'name': u'TESSERACT_PATH', 'global_name': u'OCR_TESSERACT_PATH', 'default': u'/usr/bin/tesseract', 'exists': True}, {'name': u'TESSERACT_LANGUAGE', 'global_name': u'OCR_TESSERACT_LANGUAGE', 'default': u'eng'}, - {'name': u'REPLICATION_DELAY', 'global_name': u'OCR_REPLICATION_DELAY', 'default': 10, 'description': _(u'Amount of seconds to delay OCR of documents to allow for the node\'s storage replication overhead.')}, + {'name': u'REPLICATION_DELAY', 'global_name': u'OCR_REPLICATION_DELAY', 'default': 0, 'description': _(u'Amount of seconds to delay OCR of documents to allow for the node\'s storage replication overhead.')}, {'name': u'NODE_CONCURRENT_EXECUTION', 'global_name': u'OCR_NODE_CONCURRENT_EXECUTION', 'default': 1, 'description': _(u'Maximum amount of concurrent document OCRs a node can perform.')}, {'name': u'AUTOMATIC_OCR', 'global_name': u'OCR_AUTOMATIC_OCR', 'default': False, 'description': _(u'Automatically queue newly created documents for OCR.')}, {'name': u'QUEUE_PROCESSING_INTERVAL', 'global_name': u'OCR_QUEUE_PROCESSING_INTERVAL', 'default': 10}, From c63721cbf636437c331587f0ac975f5c6a98d9f9 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 04:47:27 -0400 Subject: [PATCH 18/92] Move OCR queue document requeueing from the view to the model and add proper exception --- apps/ocr/exceptions.py | 4 ++++ apps/ocr/models.py | 15 ++++++++++++++- apps/ocr/views.py | 34 ++++++++++++++++------------------ 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/apps/ocr/exceptions.py b/apps/ocr/exceptions.py index 41ebe0c8ca..704e0ae9c7 100644 --- a/apps/ocr/exceptions.py +++ b/apps/ocr/exceptions.py @@ -11,3 +11,7 @@ class UnpaperError(Exception): Raised by unpaper """ pass + + +class ReQueueError(Exception): + pass diff --git a/apps/ocr/models.py b/apps/ocr/models.py index 6e25ef334f..f33ee3fb35 100644 --- a/apps/ocr/models.py +++ b/apps/ocr/models.py @@ -1,4 +1,5 @@ from ast import literal_eval +from datetime import datetime from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -14,8 +15,9 @@ from sources.managers import SourceTransformationManager from ocr.literals import DOCUMENTQUEUE_STATE_STOPPED, \ DOCUMENTQUEUE_STATE_CHOICES, QUEUEDOCUMENT_STATE_PENDING, \ - QUEUEDOCUMENT_STATE_CHOICES + QUEUEDOCUMENT_STATE_CHOICES, QUEUEDOCUMENT_STATE_PROCESSING from ocr.managers import DocumentQueueManager +from ocr.exceptions import ReQueueError class DocumentQueue(models.Model): @@ -56,6 +58,17 @@ class QueueDocument(models.Model): def get_transformation_list(self): return QueueTransformation.transformations.get_for_object_as_list(self) + def requeue(self): + if self.state == QUEUEDOCUMENT_STATE_PROCESSING: + raise ReQueueError + else: + self.datetime_submitted = datetime.now() + self.state = QUEUEDOCUMENT_STATE_PENDING + self.delay = False + self.result = None + self.node_name = None + self.save() + def __unicode__(self): try: return unicode(self.document) diff --git a/apps/ocr/views.py b/apps/ocr/views.py index d6102f1200..8fbc40d444 100644 --- a/apps/ocr/views.py +++ b/apps/ocr/views.py @@ -1,4 +1,3 @@ -import datetime import socket from django.http import HttpResponseRedirect @@ -23,7 +22,7 @@ from ocr.models import DocumentQueue, QueueDocument, QueueTransformation from ocr.literals import QUEUEDOCUMENT_STATE_PENDING, \ QUEUEDOCUMENT_STATE_PROCESSING, DOCUMENTQUEUE_STATE_STOPPED, \ DOCUMENTQUEUE_STATE_ACTIVE -from ocr.exceptions import AlreadyQueued +from ocr.exceptions import AlreadyQueued, ReQueueError from ocr.api import clean_pages from ocr.forms import QueueTransformationForm, QueueTransformationForm_create @@ -122,7 +121,7 @@ def submit_document_multiple(request): for item_id in request.GET.get('id_list', '').split(','): submit_document(request, item_id) - return HttpResponseRedirect(request.META['HTTP_REFERER']) + return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/')) def submit_document(request, document_id): @@ -130,7 +129,7 @@ def submit_document(request, document_id): document = get_object_or_404(Document, pk=document_id) return submit_document_to_queue(request, document=document, - post_submit_redirect=request.META['HTTP_REFERER']) + post_submit_redirect=request.META.get('HTTP_REFERER', '/')) def submit_document_to_queue(request, document, post_submit_redirect=None): @@ -167,22 +166,21 @@ def re_queue_document(request, queue_document_id=None, queue_document_id_list=No if request.method == 'POST': for queue_document in queue_documents: try: - queue_document.document - if queue_document.state == QUEUEDOCUMENT_STATE_PROCESSING: - messages.warning(request, _(u'Document: %s is already being processed and can\'t be re-queded.') % queue_document) - else: - queue_document.datetime_submitted = datetime.datetime.now() - queue_document.state = QUEUEDOCUMENT_STATE_PENDING - queue_document.delay = False - queue_document.result = None - queue_document.node_name = None - queue_document.save() - messages.success(request, _(u'Document: %(document)s was re-queued to the OCR queue: %(queue)s') % { - 'document': queue_document.document, 'queue': queue_document.document_queue.label}) + queue_document.requeue() + messages.success( + request, + _(u'Document: %(document)s was re-queued to the OCR queue: %(queue)s') % { + 'document': queue_document.document, + 'queue': queue_document.document_queue.label + } + ) except Document.DoesNotExist: messages.error(request, _(u'Document id#: %d, no longer exists.') % queue_document.document_id) - except Exception, e: - messages.error(request, e) + except ReQueueError: + messages.warning( + request, + _(u'Document: %s is already being processed and can\'t be re-queded.') % queue_document + ) return HttpResponseRedirect(next) context = { From 6d9b6f9ada7c3684f07d1694676786082f58b95f Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 04:48:24 -0400 Subject: [PATCH 19/92] Add more debugging logging and update to only process 1 queue item per execution Previously the task_process_document_queues processed all the pending queue items it could find, this could lead to the of inexisting queue items from a stale queryset --- apps/ocr/tasks.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/ocr/tasks.py b/apps/ocr/tasks.py index 06f55bd69d..7c01a36e4d 100644 --- a/apps/ocr/tasks.py +++ b/apps/ocr/tasks.py @@ -2,6 +2,7 @@ from datetime import timedelta, datetime import platform from time import sleep from random import random +import logging from django.db.models import Q @@ -21,10 +22,15 @@ from ocr.conf.settings import QUEUE_PROCESSING_INTERVAL LOCK_EXPIRE = 60 * 10 # Lock expires in 10 minutes # TODO: Tie LOCK_EXPIRATION with hard task timeout +logger = logging.getLogger(__name__) + + def task_process_queue_document(queue_document_id): lock_id = u'task_proc_queue_doc-%d' % queue_document_id try: + logger.debug('trying to acquire lock: %s' % lock_id) lock = Lock.acquire_lock(lock_id, LOCK_EXPIRE) + logger.debug('acquired lock: %s' % lock_id) queue_document = QueueDocument.objects.get(pk=queue_document_id) queue_document.state = QUEUEDOCUMENT_STATE_PROCESSING queue_document.node_name = platform.node() @@ -39,6 +45,7 @@ def task_process_queue_document(queue_document_id): lock.release() except LockError: + logger.debug('unable to obtain lock') pass @@ -69,6 +76,7 @@ def reset_orphans(): def task_process_document_queues(): + logger.debug('executed') # reset_orphans() # Causes problems with big clusters increased latency # Disabled until better solution @@ -90,3 +98,10 @@ def task_process_document_queues(): except Exception, e: pass #print 'DocumentQueueWatcher exception: %s' % e + finally: + # Don't process anymore from this queryset, might be stale + break; + else: + logger.debug('already processing maximun') + else: + logger.debug('nothing to process') From 922971274fc3c79aef400d8b9e4f0c333dc648f4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 04:54:14 -0400 Subject: [PATCH 20/92] Add office document text extractor --- apps/ocr/parsers/__init__.py | 55 +++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/apps/ocr/parsers/__init__.py b/apps/ocr/parsers/__init__.py index 8ed4be7cb7..8fac71c084 100644 --- a/apps/ocr/parsers/__init__.py +++ b/apps/ocr/parsers/__init__.py @@ -1,20 +1,35 @@ import slate +import logging from django.utils.translation import ugettext as _ +from converter import office_converter +from converter import office_converter +from converter.office_converter import OfficeConverter +from converter.exceptions import OfficeBackendError, OfficeConversionError +from documents.utils import document_save_to_temp_dir + from ocr.parsers.exceptions import ParserError, ParserUnknownFile + mimetype_registry = {} +logger = logging.getLogger(__name__) -def register_parser(mimetype, function): - mimetype_registry[mimetype] = {'function': function} +def register_parser(function, mimetype=None, mimetypes=None): + if mimetypes: + for mimetype in mimetypes: + mimetype_registry[mimetype] = {'function': function} + else: + mimetype_registry[mimetype] = {'function': function} -def pdf_parser(document_page): - fd = document_page.document.open() - pdf_pages = slate.PDF(fd) - fd.close() +def pdf_parser(document_page, descriptor=None): + if not descriptor: + descriptor = document_page.document.open() + + pdf_pages = slate.PDF(descriptor) + descriptor.close() if pdf_pages[document_page.page_number - 1] == '\x0c': raise ParserError @@ -24,11 +39,37 @@ def pdf_parser(document_page): document_page.save() +def office_parser(document_page): + logger.debug('executing') + try: + office_converter = OfficeConverter() + document_file = document_save_to_temp_dir(document_page.document, document_page.document.checksum) + logger.debug('document_file: %s', document_file) + + office_converter.convert(document_file, mimetype=document_page.document.file_mimetype) + if office_converter.exists: + input_filepath = office_converter.output_filepath + logger.debug('office_converter.output_filepath: %s', input_filepath) + + pdf_parser(document_page, descriptor=open(input_filepath)) + else: + raise ParserError + + except OfficeConversionError, msg: + print msg + raise ParserError + + def parse_document_page(document_page): + logger.debug('executing') + logger.debug('document_page: %s' % document_page) + logger.debug('mimetype: %s' % document_page.document.file_mimetype) + try: mimetype_registry[document_page.document.file_mimetype]['function'](document_page) except KeyError: raise ParserUnknownFile -register_parser('application/pdf', pdf_parser) +register_parser(mimetype=u'application/pdf', function=pdf_parser) +register_parser(mimetypes=office_converter.CONVERTER_OFFICE_FILE_MIMETYPES, function=office_parser) From 13826a0a4a360c0cc3dd19e76a54068a55496a8b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 04:54:33 -0400 Subject: [PATCH 21/92] Update changelog --- docs/changelog.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index ca07319a67..3d1620a444 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,3 +1,33 @@ +2011-12-1 +--------- +* OCR queue processing improvements +* Office documents handling improvements +* Text extraction support for office documents +* RTF text documents are now handled as office documents + +2011-11-30 +---------- +* Added South to the requirements +* Merged documents' filename and extension database fiels into a single + filename field, filename are store as uploaded not manipulation is done + Users with existing data must install South and run the appropiate + migrate commands:: + $ pip install -r requirements/production.txt + $ ./manager syncdb + $ ./manage.py migrate documents 0001 --fake + $ ./manage.py migrate documents + +* Added new office document mimetype + * application/vnd.ms-office + +* Fixed documents not saving the file encoding + + +2011-11-28 +---------- +* Removed extra slash in ajax-loader.gif URL fixes #15, thanks to IHLeanne for finding this one + + Version 0.10 ------------ * Added a proper setup views for the document grouping functionality. From da1ab56202ae6712e0a78e6f79cec2fe35b4dffe Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 05:24:31 -0400 Subject: [PATCH 22/92] Add maintenace view to clear the document image cache --- apps/documents/__init__.py | 3 ++- apps/documents/models.py | 7 ++++++ .../static/images/icons/camera_delete.png | Bin 0 -> 2006 bytes apps/documents/urls.py | 1 + apps/documents/views.py | 21 ++++++++++++++++++ 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 apps/documents/static/images/icons/camera_delete.png diff --git a/apps/documents/__init__.py b/apps/documents/__init__.py index 5ff5a84c9a..14ff0b07a3 100644 --- a/apps/documents/__init__.py +++ b/apps/documents/__init__.py @@ -85,6 +85,7 @@ document_clear_transformations = {'text': _(u'clear transformations'), 'view': ' document_multiple_clear_transformations = {'text': _(u'clear transformations'), 'view': 'document_multiple_clear_transformations', 'famfam': 'page_paintbrush', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} document_print = {'text': _(u'print'), 'view': 'document_print', 'args': 'object.id', 'famfam': 'printer', 'permissions': [PERMISSION_DOCUMENT_VIEW]} document_history_view = {'text': _(u'history'), 'view': 'history_for_object', 'args': ['"documents"', '"document"', 'object.id'], 'famfam': 'book_go', 'permissions': [PERMISSION_DOCUMENT_VIEW]} +document_clear_image_cache = {'text': _(u'Clear the document image cache'), 'view': 'document_clear_image_cache', 'famfam': 'camera_delete', 'permissions': [PERMISSION_DOCUMENT_TOOLS], 'description': _(u'Clear the graphics representations used to speed up the documents\' display and interactive transformations results.')} document_page_transformation_list = {'text': _(u'page transformations'), 'class': 'no-parent-history', 'view': 'document_page_transformation_list', 'args': 'page.pk', 'famfam': 'pencil_go', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} document_page_transformation_create = {'text': _(u'create new transformation'), 'class': 'no-parent-history', 'view': 'document_page_transformation_create', 'args': 'page.pk', 'famfam': 'pencil_add', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} @@ -158,7 +159,7 @@ register_links(['document_page_transformation_edit', 'document_page_transformati register_diagnostic('documents', _(u'Documents'), document_missing_list) -register_maintenance_links([document_find_all_duplicates, document_update_page_count], namespace='documents', title=_(u'documents')) +register_maintenance_links([document_find_all_duplicates, document_update_page_count, document_clear_image_cache], namespace='documents', title=_(u'documents')) #def document_exists(document): # try: diff --git a/apps/documents/models.py b/apps/documents/models.py index 2e5a627a73..ed578c36b9 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -92,6 +92,13 @@ class Document(models.Model): object_id_field='object_pk' ) + @staticmethod + def clear_image_cache(): + for the_file in os.listdir(CACHE_PATH): + file_path = os.path.join(CACHE_PATH, the_file) + if os.path.isfile(file_path): + os.unlink(file_path) + class Meta: verbose_name = _(u'document') verbose_name_plural = _(u'documents') diff --git a/apps/documents/static/images/icons/camera_delete.png b/apps/documents/static/images/icons/camera_delete.png new file mode 100644 index 0000000000000000000000000000000000000000..05616882a73e1972924d02a55c5a900c9206d195 GIT binary patch literal 2006 zcmV;{2Pyc8P))fN6`cJ`HhdA$#B zyiN>p5`r72q-{`RJ3Itc5D}zmgG8%7gs3Q05mZ%6`l3Rps#2+HDQZ+`#28mq;d1$mfwvB#>Bh4@|hs0z%!u)W>M_`LOG2_rmY< zS25JUGeghqdwvI>Ns==F*koBjE?>a8u`x6@G$5DFBAH4fo=D=Z?RVn*_yn;cRfQJ# zd|tdU2?1a+PYmFufEPtgM>3toiJ_C2C#LwB&FR4{w`_x|3ai)_1CaXq{)6Mk2Vk+7 zLADpNapOiPiXs@$HEjU^4>xJ{i|`!eIuBy9GI=j9#o_{3pwWvS*RA5&-O2#UWev5c zTUf~eU^biSa0|BGx()q({o;OfbQBvlY#;y{iB9&D1pt}k^x1Q`5WRqSJboDiKEEHU zR<)qP(;(LQ0zv%9ofprNnII`rMt#^TRi~?JckF{>=;-J`G#V8L@f?K}N!1V7iH3%U zkxImcsjhEXNi4}?BURNgJ72=k;2?rcA+)uvMQkR4>FF8aORTiR>9{%oV{Gl*ioV`H zG>60F<+&<=vRc~!ICJI)N(rb*ih*0imG zUh_~sSR;^?y3oB0z=t1vxWIsg%)+Bg46qNRa#>7Ynu0fU9rpg$iBNM8cXS633cAI6 zmtt9rPR!7=7{0&ROz)nDmz>q*bO{qO09Cs(H_7!3u$JD&Mk5&r6R`dh<57fH2J!ls z6E|*bLr42{u&=Nqohu39d!D=Tb8Yxn)QRR$P^{5ea{|D^5m0Xext@Xb>(?WdOv3OD zRV@qJVly!WNF#Vx!Q*iwTbM_Fwun60KnUIQoTtH!qH7g`ey>==Ypaf|(OxQyWcTR| z)CPx+t}%rgtX3;5D_qFYVWpBPo~bqAd!Eb9mSCZnx7zK3IX+LXNa89O_#_itm4R|u z(8o??HY=D@EAY@W7wzPz6Q^W?Q?Vc?+abH$C|m3(TkTM-HfUxG%Iu7iyu>J=lq(ou z+O(SYk|sr}1Ad2tyjx$S4Q*_qq-C}vH1-j0IP?s1!-vo%DbV)PGV&vA>B8(c9)w0L zE-eZu8SyBUN>FtbW{VBe6xZENUX*8D@LNo%)B;qeRIbtcp?^GqmgD=;ynQD;-~J;^ zD;%PTX|n~S1_v;?=VxeLyJ_h@z$Pe>33Q6zK){E=lf&5AHizC+9OIcXJdznJiRK_B zfwujRq2=srXn*`EtZ>+o+rJO-J-d_z*dyD|6LizxN~n>`LP*Eif9fm#?Me{cwK=#Cm&D%WocV3xFPRP|-UlGC# zjEs!Hm@`bEQ36pE6T;0cn4@(2(!n>-<(j~o-!kK)A3Cuo|2Ks1xeulH-+~sq2-(I5 zn;}y%6T^@7a|5>R4~qFZnvAlsTm3C z2O`IM@%H;ip^|TGKk^>dZMhk^$n(bNoyiCQ@}j!yZSYdob0AMWLxHKKFoQxGMn_?y*E~J_pV)$B zc76%c#5n_=g#lF3>GaXVhYxodIeP(fEgc)z#q%r`D$yM4l1#3YPD3`^KD7xI@1#?r zJ2JXI7s+Z8e|=akahM}FuY2>0n>L7|rO(fyLMB9gyU(E6A8Z8JM80!s6t_nPoA|&v zt~DgFYi!d!iHg*ho-KXB=k-7*&lfCS=>pY;h7wjhok3RB`j~yC)}7{z9_<=N@lYc7 zbkH^PLLlHLuQ!vU>Q^Ixnw)Cd8K0WLp;-E94l>g+eB4w%PUTa#`P^&Dh5QZSP)LA~ z$>Z5K_-Z2RO*AJFfZmg*Fp|!{`tzal&(Ye{asc2KNB`%=L|>Q3xq3dEUT?Qq>HEM% ziO5P$XBN~asK5=L9>J+Zw&%OOXMV{&44YrV@(fmli)8patApM5u55hRrJ8| o)P1*k07*qoM6N<$f;KwSX8-^I literal 0 HcmV?d00001 diff --git a/apps/documents/urls.py b/apps/documents/urls.py index 0d16fb3880..e3cfbf598d 100644 --- a/apps/documents/urls.py +++ b/apps/documents/urls.py @@ -37,6 +37,7 @@ urlpatterns = patterns('documents.views', url(r'^multiple/clear_transformations/$', 'document_multiple_clear_transformations', (), 'document_multiple_clear_transformations'), url(r'^duplicates/list/$', 'document_find_all_duplicates', (), 'document_find_all_duplicates'), url(r'^maintenance/update_page_count/$', 'document_update_page_count', (), 'document_update_page_count'), + url(r'^maintenance/clear_image_cache/$', 'document_clear_image_cache', (), 'document_clear_image_cache'), url(r'^page/(?P\d+)/$', 'document_page_view', (), 'document_page_view'), url(r'^page/(?P\d+)/text/$', 'document_page_text', (), 'document_page_text'), diff --git a/apps/documents/views.py b/apps/documents/views.py index 68a70f2435..ba0e18e315 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -1121,3 +1121,24 @@ def document_type_filename_create(request, document_type_id): 'document_type': document_type, }, context_instance=RequestContext(request)) + + +def document_clear_image_cache(request): + check_permissions(request.user, [PERMISSION_DOCUMENT_TOOLS]) + + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + + if request.method == 'POST': + try: + Document.clear_image_cache() + messages.success(request, _(u'Document image cache cleared successfully')) + except Exception, msg: + messages.error(request, _(u'Error clearing document image cache; %s') % msg) + + return HttpResponseRedirect(previous) + + return render_to_response('generic_confirm.html', { + 'previous': previous, + 'title': _(u'Are you sure you wish to clear the document image cache?'), + 'form_icon': u'camera_delete.png', + }, context_instance=RequestContext(request)) From 93fdd0c5594d03233cc9d3c5e3ac360806f989d5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 05:24:53 -0400 Subject: [PATCH 23/92] Update changelog --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3d1620a444..6f5cd78643 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,8 @@ * Office documents handling improvements * Text extraction support for office documents * RTF text documents are now handled as office documents +* Added a view to delete the document image cache, useful when switching + converter backends or doing diagnostics 2011-11-30 ---------- From 60c05317b3e4fe676a5f1d9186f0282a22dd1a31 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 23:38:29 -0400 Subject: [PATCH 24/92] Fix indentation --- apps/converter/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/converter/api.py b/apps/converter/api.py index f48bf88986..993fb70f1d 100644 --- a/apps/converter/api.py +++ b/apps/converter/api.py @@ -63,7 +63,7 @@ def convert(input_filepath, output_filepath=None, cleanup_files=False, mimetype= mimetype = office_converter.mimetype except OfficeConversionError: - raise UnknownFileFormat('office converter exception') + raise UnknownFileFormat('office converter exception') if size: transformations.append( From 41de49916f950a4455f85c5b2ba2f9080123d12c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 1 Dec 2011 23:38:50 -0400 Subject: [PATCH 25/92] Pass exc_info to logger for better debugging --- apps/converter/office_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/converter/office_converter.py b/apps/converter/office_converter.py index e98b8cfdd4..a0c93aa6d9 100644 --- a/apps/converter/office_converter.py +++ b/apps/converter/office_converter.py @@ -115,4 +115,4 @@ class OfficeConverterBackendUnoconv(object): except OSError, msg: raise OfficeBackendError(msg) except Exception, msg: - logger.error('Unhandled exception: %s' % msg) + logger.error('Unhandled exception', exc_info=msg) From b38d84f663f628e7cd0ca6eb9f2dad8c3810bae7 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 2 Dec 2011 02:51:15 -0400 Subject: [PATCH 26/92] South schema and data migrations to support document versions --- ...unique_documentversion_document_mayor_m.py | 188 ++++++++++++++++ .../migrations/0005_document_versions.py | 181 ++++++++++++++++ .../migrations/0006_remove_old_file_fields.py | 202 ++++++++++++++++++ 3 files changed, 571 insertions(+) create mode 100644 apps/documents/migrations/0004_auto__add_documentversion__add_unique_documentversion_document_mayor_m.py create mode 100644 apps/documents/migrations/0005_document_versions.py create mode 100644 apps/documents/migrations/0006_remove_old_file_fields.py diff --git a/apps/documents/migrations/0004_auto__add_documentversion__add_unique_documentversion_document_mayor_m.py b/apps/documents/migrations/0004_auto__add_documentversion__add_unique_documentversion_document_mayor_m.py new file mode 100644 index 0000000000..e6b0c9bf10 --- /dev/null +++ b/apps/documents/migrations/0004_auto__add_documentversion__add_unique_documentversion_document_mayor_m.py @@ -0,0 +1,188 @@ +# 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 'DocumentVersion' + db.create_table('documents_documentversion', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('document', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.Document'], null=True, blank=True)), + ('mayor', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), + ('minor', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + ('micro', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + ('release_level', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), + ('serial', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + ('timestamp', self.gf('django.db.models.fields.DateTimeField')()), + ('file', self.gf('django.db.models.fields.files.FileField')(max_length=100)), + ('mimetype', self.gf('django.db.models.fields.CharField')(default='', max_length=64)), + ('encoding', self.gf('django.db.models.fields.CharField')(default='', max_length=64)), + ('filename', self.gf('django.db.models.fields.CharField')(default=u'', max_length=255, db_index=True)), + ('checksum', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('documents', ['DocumentVersion']) + + # Adding unique constraint on 'DocumentVersion', fields ['document', 'mayor', 'minor', 'micro', 'release_level', 'serial'] + db.create_unique('documents_documentversion', ['document_id', 'mayor', 'minor', 'micro', 'release_level', 'serial']) + + # Adding field 'DocumentPage.document_version' + db.add_column('documents_documentpage', 'document_version', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.DocumentVersion'], null=True, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Removing unique constraint on 'DocumentVersion', fields ['document', 'mayor', 'minor', 'micro', 'release_level', 'serial'] + db.delete_unique('documents_documentversion', ['document_id', 'mayor', 'minor', 'micro', 'release_level', 'serial']) + + # Deleting model 'DocumentVersion' + db.delete_table('documents_documentversion') + + # Deleting field 'DocumentPage.document_version' + db.delete_column('documents_documentpage', 'document_version_id') + + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'file_filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'file_mime_encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file_mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "u'a8389d7d-b9f4-4e51-ac24-dd9dd310fd8c'", 'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentVersion']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.documentversion': { + 'Meta': {'unique_together': "(('document', 'mayor', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']", 'null': 'True', 'blank': 'True'}), + 'encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mayor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] diff --git a/apps/documents/migrations/0005_document_versions.py b/apps/documents/migrations/0005_document_versions.py new file mode 100644 index 0000000000..738dfe9147 --- /dev/null +++ b/apps/documents/migrations/0005_document_versions.py @@ -0,0 +1,181 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + for document in orm.Document.objects.all(): + document_version = document.documentversion_set.create( + document = document, + #mayor = 1 + #minor = models.PositiveIntegerField(verbose_name=_(u'minor'), default=0) + #micro = models.PositiveIntegerField(verbose_name=_(u'micro'), default=0) + #release_level = models.PositiveIntegerField(choices=RELEASE_LEVEL_CHOICES, default=RELEASE_LEVEL_FINAL, verbose_name=_(u'release level')) + #serial = models.PositiveIntegerField(verbose_name=_(u'serial'), default=0) + timestamp = document.date_added, + file = document.file, + mimetype = document.file_mimetype, + encoding = document.file_mime_encoding, + filename = document.file_filename, + checksum = document.checksum, + ) + document_version.save() + for document_page in document.documentpage_set.all(): + document_page.document_version = document_version + document_page.save() + + def backwards(self, orm): + for document in orm.Document.objects.all(): + document_version = document.documentversion_set.all()[0] + document.date_added = document_version.timestamp + document.file = document_version.file + document.file_mimetype = document_version.mimetype + document.file_mime_encoding = document_version.encoding + document.filename = document_version.filename + document.checksum = document_version.checksum + document.save() + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'file_filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'file_mime_encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file_mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "u'6c189f1f-1d85-48b5-9b7d-e8e319603e77'", 'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentVersion']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.documentversion': { + 'Meta': {'unique_together': "(('document', 'mayor', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']", 'null': 'True', 'blank': 'True'}), + 'encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mayor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] diff --git a/apps/documents/migrations/0006_remove_old_file_fields.py b/apps/documents/migrations/0006_remove_old_file_fields.py new file mode 100644 index 0000000000..203532d012 --- /dev/null +++ b/apps/documents/migrations/0006_remove_old_file_fields.py @@ -0,0 +1,202 @@ +# 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): + + # Changing field 'DocumentVersion.document' + db.alter_column('documents_documentversion', 'document_id', self.gf('django.db.models.fields.related.ForeignKey')(default='', to=orm['documents.Document'])) + + # Deleting field 'Document.date_updated' + db.delete_column('documents_document', 'date_updated') + + # Deleting field 'Document.file' + db.delete_column('documents_document', 'file') + + # Deleting field 'Document.file_filename' + db.delete_column('documents_document', 'file_filename') + + # Deleting field 'Document.file_mimetype' + db.delete_column('documents_document', 'file_mimetype') + + # Deleting field 'Document.checksum' + db.delete_column('documents_document', 'checksum') + + # Deleting field 'Document.file_mime_encoding' + db.delete_column('documents_document', 'file_mime_encoding') + + # Deleting field 'DocumentPage.document' + db.delete_column('documents_documentpage', 'document_id') + + # Changing field 'DocumentPage.document_version' + db.alter_column('documents_documentpage', 'document_version_id', self.gf('django.db.models.fields.related.ForeignKey')(default='', to=orm['documents.DocumentVersion'])) + + + def backwards(self, orm): + + # Changing field 'DocumentVersion.document' + db.alter_column('documents_documentversion', 'document_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.Document'], null=True)) + + # Adding field 'Document.date_updated' + db.add_column('documents_document', 'date_updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, default=datetime.datetime(2011, 12, 2, 2, 17, 25, 53565), blank=True), keep_default=False) + + # Adding field 'Document.file' + db.add_column('documents_document', 'file', self.gf('django.db.models.fields.files.FileField')(default='', max_length=100), keep_default=False) + + # Adding field 'Document.file_filename' + db.add_column('documents_document', 'file_filename', self.gf('django.db.models.fields.CharField')(default=u'', max_length=255, db_index=True), keep_default=False) + + # Adding field 'Document.file_mimetype' + db.add_column('documents_document', 'file_mimetype', self.gf('django.db.models.fields.CharField')(default='', max_length=64), keep_default=False) + + # Adding field 'Document.checksum' + db.add_column('documents_document', 'checksum', self.gf('django.db.models.fields.TextField')(null=True, blank=True), keep_default=False) + + # Adding field 'Document.file_mime_encoding' + db.add_column('documents_document', 'file_mime_encoding', self.gf('django.db.models.fields.CharField')(default='', max_length=64), keep_default=False) + + # Adding field 'DocumentPage.document' + db.add_column('documents_documentpage', 'document', self.gf('django.db.models.fields.related.ForeignKey')(default='', to=orm['documents.Document']), keep_default=False) + + # Changing field 'DocumentPage.document_version' + db.alter_column('documents_documentpage', 'document_version_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.DocumentVersion'], null=True)) + + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "u'83100718-e901-4880-95f8-3618749c8a99'", 'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentVersion']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.documentversion': { + 'Meta': {'unique_together': "(('document', 'mayor', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mayor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] From d83e8b5428e0e79e7b6ab7d0b1624343f21e699e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 2 Dec 2011 02:51:59 -0400 Subject: [PATCH 27/92] Initial set of model, form and API changes to support document versions --- apps/documents/__init__.py | 2 +- apps/documents/admin.py | 33 ++++--- apps/documents/forms.py | 6 +- apps/documents/models.py | 180 +++++++++++++++++++++++++++++++------ apps/ocr/api.py | 2 +- 5 files changed, 182 insertions(+), 41 deletions(-) diff --git a/apps/documents/__init__.py b/apps/documents/__init__.py index 14ff0b07a3..f47ab4bab9 100644 --- a/apps/documents/__init__.py +++ b/apps/documents/__init__.py @@ -35,7 +35,7 @@ def is_first_page(context): def is_last_page(context): - return context['page'].page_number >= context['page'].document.documentpage_set.count() + return context['page'].page_number >= context['page'].document_version.pages.count() def is_min_zoom(context): diff --git a/apps/documents/admin.py b/apps/documents/admin.py index 71d0fab939..8ef02b1cea 100644 --- a/apps/documents/admin.py +++ b/apps/documents/admin.py @@ -2,11 +2,29 @@ from django.contrib import admin from metadata.admin import DocumentMetadataInline -from documents.models import DocumentType, Document, \ - DocumentTypeFilename, DocumentPage, \ - DocumentPageTransformation, RecentDocument +from documents.models import (DocumentType, Document, + DocumentTypeFilename, DocumentPage, + DocumentPageTransformation, RecentDocument, + DocumentVersion) +class DocumentPageInline(admin.StackedInline): + model = DocumentPage + extra = 1 + classes = ('collapse-open',) + allow_add = True + + +class DocumentVersionInline(admin.StackedInline): + model = DocumentVersion + extra = 1 + classes = ('collapse-open',) + allow_add = True + inlines = [ + DocumentPageInline, + ] + + class DocumentTypeFilenameInline(admin.StackedInline): model = DocumentTypeFilename extra = 1 @@ -24,16 +42,9 @@ class DocumentPageTransformationAdmin(admin.ModelAdmin): model = DocumentPageTransformation -class DocumentPageInline(admin.StackedInline): - model = DocumentPage - extra = 1 - classes = ('collapse-open',) - allow_add = True - - class DocumentAdmin(admin.ModelAdmin): inlines = [ - DocumentMetadataInline, DocumentPageInline + DocumentMetadataInline, DocumentVersionInline ] list_display = ('uuid', 'file_filename',) diff --git a/apps/documents/forms.py b/apps/documents/forms.py index 320b60bc71..47ac8215da 100644 --- a/apps/documents/forms.py +++ b/apps/documents/forms.py @@ -100,7 +100,7 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget): output = [] output.append(u'
') - for page in value.documentpage_set.all(): + for page in value.pages.all(): output.append(u'
') output.append( document_html_widget( @@ -128,7 +128,7 @@ class DocumentPreviewForm(forms.Form): document = kwargs.pop('document', None) super(DocumentPreviewForm, self).__init__(*args, **kwargs) self.fields['preview'].initial = document - self.fields['preview'].label = _(u'Document pages (%s)') % document.documentpage_set.count() + self.fields['preview'].label = _(u'Document pages (%s)') % document.pages.count() preview = forms.CharField(widget=DocumentPagesCarouselWidget()) @@ -198,7 +198,7 @@ class DocumentContentForm(forms.Form): super(DocumentContentForm, self).__init__(*args, **kwargs) content = [] self.fields['contents'].initial = u'' - for page in self.document.documentpage_set.all(): + for page in self.document.pages.all(): if page.content: content.append(page.content) content.append(u'\n\n\n - Page %s - \n\n\n' % page.page_number) diff --git a/apps/documents/models.py b/apps/documents/models.py index ed578c36b9..6d68e03ae6 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -72,17 +72,20 @@ class Document(models.Model): """ Defines a single document with it's fields and properties """ + # Base fields document_type = models.ForeignKey(DocumentType, verbose_name=_(u'document type'), null=True, blank=True) - file = models.FileField(upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'file')) uuid = models.CharField(max_length=48, default=UUID_FUNCTION(), blank=True, editable=False) - file_mimetype = models.CharField(max_length=64, default='', editable=False) - file_mime_encoding = models.CharField(max_length=64, default='', editable=False) - #FAT filename can be up to 255 using LFN - file_filename = models.CharField(max_length=255, default=u'', editable=False, db_index=True) - date_added = models.DateTimeField(verbose_name=_(u'added'), auto_now_add=True, db_index=True) - date_updated = models.DateTimeField(verbose_name=_(u'updated'), auto_now=True) - checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False) description = models.TextField(blank=True, null=True, verbose_name=_(u'description'), db_index=True) + date_added = models.DateTimeField(verbose_name=_(u'added'), auto_now_add=True, db_index=True) + + ## Fields to migrate + #file = models.FileField(upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'file')) + #file_mimetype = models.CharField(max_length=64, default='', editable=False) + #file_mime_encoding = models.CharField(max_length=64, default='', editable=False) + ##FAT filename can be up to 255 using LFN + #file_filename = models.CharField(max_length=255, default=u'', editable=False, db_index=True) + #date_updated = models.DateTimeField(verbose_name=_(u'updated'), auto_now=True) + #checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False) tags = TaggableManager() @@ -155,7 +158,8 @@ class Document(models.Model): Return a file descriptor to a document's file irrespective of the storage backend """ - return self.file.storage.open(self.file.path) + #return self.file.storage.open(self.file.path) + return self.get_latest_version().file.storage.open(self.get_latest_version().file.path) def update_checksum(self, save=True): """ @@ -163,11 +167,11 @@ class Document(models.Model): user provided checksum function """ if self.exists(): - source = self.open() - self.checksum = unicode(CHECKSUM_FUNCTION(source.read())) + source = self.get_latest_version().open() + self.get_latest_version().checksum = unicode(CHECKSUM_FUNCTION(source.read())) source.close() if save: - self.save() + self.get_latest_version().save() def update_page_count(self, save=True): handle, filepath = tempfile.mkstemp() @@ -204,7 +208,8 @@ class Document(models.Model): @property def page_count(self): - return self.documentpage_set.count() + #return self.documentpage_set.count() + return self.get_latest_version().documentpage_set.count() def save_to_file(self, filepath, buffer_size=1024 * 1024): """ @@ -229,13 +234,13 @@ class Document(models.Model): Returns a boolean value that indicates if the document's file exists in storage """ - return self.file.storage.exists(self.file.path) + return self.get_latest_version().file.storage.exists(self.get_latest_version().file.path) def apply_default_transformations(self, transformations): #Only apply default transformations on new documents - if reduce(lambda x, y: x + y, [page.documentpagetransformation_set.count() for page in self.documentpage_set.all()]) == 0: + if reduce(lambda x, y: x + y, [page.documentpagetransformation_set.count() for page in self.pages.all()]) == 0: for transformation in transformations: - for document_page in self.documentpage_set.all(): + for document_page in self.pages.all(): page_transformation = DocumentPageTransformation( document_page=document_page, order=0, @@ -246,7 +251,7 @@ class Document(models.Model): page_transformation.save() def get_cached_image_name(self, page): - document_page = self.documentpage_set.get(page_number=page) + document_page = self.pages.get(page_number=page) transformations, warnings = document_page.get_transformation_list() hash_value = HASH_FUNCTION(u''.join([self.checksum, unicode(page), unicode(transformations)])) return os.path.join(CACHE_PATH, hash_value), transformations @@ -300,15 +305,129 @@ class Document(models.Model): def delete(self, *args, **kwargs): super(Document, self).delete(*args, **kwargs) - return self.file.storage.delete(self.file.path) + for version in self.documentversion_set.all(): + version.file.storage.delete(version.file.path) + #return self.get_latest_version().file.storage.delete(self.get_latest_version().file.path) @property def size(self): if self.exists(): - return self.file.storage.size(self.file.path) + return self.get_latest_version().file.storage.size(self.get_latest_version().file.path) else: return None - + + # Compatibiliy methods + @property + def file(self): + return self.get_latest_version().file + + @property + def file_mimetype(self): + return self.get_latest_version().mimetype + + @property + def file_mime_encoding(self): + return self.get_latest_version().encoding + + @property + def file_filename(self): + return self.get_latest_version().filename + + @property + def date_updated(self): + return self.get_latest_version().timestamp + + #@property + #def date_added(self): + # return self.get_latest_version().timestamp + + @property + def checksum(self): + return self.get_latest_version().checksum + + @property + def pages(self): + return self.get_latest_version().pages + + + #file = models.FileField(upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'file')) + #file_mimetype = models.CharField(max_length=64, default='', editable=False) + #file_mime_encoding = models.CharField(max_length=64, default='', editable=False) + ##FAT filename can be up to 255 using LFN + #file_filename = models.CharField(max_length=255, default=u'', editable=False, db_index=True) + #date_updated = models.DateTimeField(verbose_name=_(u'updated'), auto_now=True) + #checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False) + + def get_latest_version(self): + return self.documentversion_set.order_by('-timestamp')[0] + + +RELEASE_LEVEL_FINAL = 1 +RELEASE_LEVEL_ALPHA = 2 +RELEASE_LEVEL_BETA = 3 +RELEASE_LEVEL_RC = 4 +RELEASE_LEVEL_HF = 5 + +RELEASE_LEVEL_CHOICES = ( + (RELEASE_LEVEL_FINAL, _(u'final')), + (RELEASE_LEVEL_ALPHA, _(u'alpha')), + (RELEASE_LEVEL_BETA, _(u'beta')), + (RELEASE_LEVEL_RC, _(u'release candidate')), + (RELEASE_LEVEL_HF, _(u'hotfix')), +) + +class DocumentVersion(models.Model): + ''' + Model that describes a document version and it properties + ''' + document = models.ForeignKey(Document, verbose_name=_(u'document')) + mayor = models.PositiveIntegerField(verbose_name=_(u'mayor'), default=1) + minor = models.PositiveIntegerField(verbose_name=_(u'minor'), default=0) + micro = models.PositiveIntegerField(verbose_name=_(u'micro'), default=0) + release_level = models.PositiveIntegerField(choices=RELEASE_LEVEL_CHOICES, default=RELEASE_LEVEL_FINAL, verbose_name=_(u'release level')) + serial = models.PositiveIntegerField(verbose_name=_(u'serial'), default=0) + timestamp = models.DateTimeField(verbose_name=_(u'timestamp')) + + # File related fields + file = models.FileField(upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'file')) + mimetype = models.CharField(max_length=64, default='', editable=False) + encoding = models.CharField(max_length=64, default='', editable=False) + filename = models.CharField(max_length=255, default=u'', editable=False, db_index=True) + checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False) + + class Meta: + unique_together = ('document', 'mayor', 'minor', 'micro', 'release_level', 'serial') + verbose_name = _(u'document version') + verbose_name_plural = _(u'document version') + + def __unicode__(self): + return self.get_version() + + # TODO: Update timestamp + + def get_version(): + ''' + Return the formatted version information + ''' + vers = [u'%(major)i.%(minor)i' % self, ] + + if self.micro: + vers.append(u'.%(micro)i' % self) + if self.releaselevel != RELEASE_LEVEL_FINAL: + vers.append(u'%(releaselevel)s%(serial)i' % self) + return u''.join(vers) + + @property + def pages(self): + return self.documentpage_set + + def open(self): + ''' + Return a file descriptor to a document version's file irrespective of + the storage backend + ''' + return self.file.storage.open(self.file.path) + class DocumentTypeFilename(models.Model): """ @@ -332,7 +451,13 @@ class DocumentPage(models.Model): """ Model that describes a document page including it's content """ - document = models.ForeignKey(Document, verbose_name=_(u'document')) + ## This field is to be removed + #document = models.ForeignKey(Document, verbose_name=_(u'document')) + + # New parent field + document_version = models.ForeignKey(DocumentVersion, verbose_name=_(u'document version'))#, null=True, blank=True) # TODO: Remove these after datamigration + + # Unchanged fields content = models.TextField(blank=True, null=True, verbose_name=_(u'content'), db_index=True) page_label = models.CharField(max_length=32, blank=True, null=True, verbose_name=_(u'page label')) page_number = models.PositiveIntegerField(default=1, editable=False, verbose_name=_(u'page number'), db_index=True) @@ -341,7 +466,7 @@ class DocumentPage(models.Model): return _(u'Page %(page_num)d out of %(total_pages)d of %(document)s') % { 'document': unicode(self.document), 'page_num': self.page_number, - 'total_pages': self.document.documentpage_set.count() + 'total_pages': self.document_version.documentpage_set.count() } class Meta: @@ -355,6 +480,11 @@ class DocumentPage(models.Model): @models.permalink def get_absolute_url(self): return ('document_page_view', [self.pk]) + + # Compatibility methods + @property + def document(self): + return self.document_version.document class ArgumentsValidator(object): @@ -421,10 +551,10 @@ class RecentDocument(models.Model): # Register the fields that will be searchable register('document', Document, _(u'document'), [ {'name': u'document_type__name', 'title': _(u'Document type')}, - {'name': u'file_mimetype', 'title': _(u'MIME type')}, - {'name': u'file_filename', 'title': _(u'Filename')}, + {'name': u'documentversion__mimetype', 'title': _(u'MIME type')}, + {'name': u'documentversion__filename', 'title': _(u'Filename')}, {'name': u'documentmetadata__value', 'title': _(u'Metadata value')}, - {'name': u'documentpage__content', 'title': _(u'Content')}, + {'name': u'documentversion__documentpage__content', 'title': _(u'Content')}, {'name': u'description', 'title': _(u'Description')}, {'name': u'tags__name', 'title': _(u'Tags')}, {'name': u'comments__comment', 'title': _(u'Comments')}, diff --git a/apps/ocr/api.py b/apps/ocr/api.py index 4d70443f92..e568ecf6c8 100644 --- a/apps/ocr/api.py +++ b/apps/ocr/api.py @@ -88,7 +88,7 @@ def do_document_ocr(queue_document): parser, if the parser fails or if there is no parser registered for the document mimetype do a visual OCR by calling tesseract """ - for document_page in queue_document.document.documentpage_set.all(): + for document_page in queue_document.document.pages.all(): try: # Try to extract text by means of a parser parse_document_page(document_page) From 640a1c2966f5f1f76c6374f0bb888825c1976f5d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 2 Dec 2011 05:53:12 -0400 Subject: [PATCH 28/92] Add two new migrations, one to fix a field name, the other to add a comment field to the document versions --- .../migrations/0007_fix_mayor_field_name.py | 172 ++++++++++++++++++ .../migrations/0008_add_comment_field.py | 155 ++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 apps/documents/migrations/0007_fix_mayor_field_name.py create mode 100644 apps/documents/migrations/0008_add_comment_field.py diff --git a/apps/documents/migrations/0007_fix_mayor_field_name.py b/apps/documents/migrations/0007_fix_mayor_field_name.py new file mode 100644 index 0000000000..dc540dfbec --- /dev/null +++ b/apps/documents/migrations/0007_fix_mayor_field_name.py @@ -0,0 +1,172 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Removing unique constraint on 'DocumentVersion', fields ['release_level', 'micro', 'serial', 'document', 'mayor', 'minor'] + db.delete_unique('documents_documentversion', ['release_level', 'micro', 'serial', 'document_id', 'mayor', 'minor']) + + # Deleting field 'DocumentVersion.mayor' + db.delete_column('documents_documentversion', 'mayor') + + # Adding field 'DocumentVersion.major' + db.add_column('documents_documentversion', 'major', self.gf('django.db.models.fields.PositiveIntegerField')(default=1), keep_default=False) + + # Adding unique constraint on 'DocumentVersion', fields ['major', 'release_level', 'micro', 'serial', 'document', 'minor'] + db.create_unique('documents_documentversion', ['major', 'release_level', 'micro', 'serial', 'document_id', 'minor']) + + + def backwards(self, orm): + + # Removing unique constraint on 'DocumentVersion', fields ['major', 'release_level', 'micro', 'serial', 'document', 'minor'] + db.delete_unique('documents_documentversion', ['major', 'release_level', 'micro', 'serial', 'document_id', 'minor']) + + # Adding field 'DocumentVersion.mayor' + db.add_column('documents_documentversion', 'mayor', self.gf('django.db.models.fields.PositiveIntegerField')(default=1), keep_default=False) + + # Deleting field 'DocumentVersion.major' + db.delete_column('documents_documentversion', 'major') + + # Adding unique constraint on 'DocumentVersion', fields ['release_level', 'micro', 'serial', 'document', 'mayor', 'minor'] + db.create_unique('documents_documentversion', ['release_level', 'micro', 'serial', 'document_id', 'mayor', 'minor']) + + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "u'750a3848-39cf-45a5-9a96-e948d09833d7'", 'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentVersion']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.documentversion': { + 'Meta': {'unique_together': "(('document', 'major', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'major': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] diff --git a/apps/documents/migrations/0008_add_comment_field.py b/apps/documents/migrations/0008_add_comment_field.py new file mode 100644 index 0000000000..2010270407 --- /dev/null +++ b/apps/documents/migrations/0008_add_comment_field.py @@ -0,0 +1,155 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'DocumentVersion.comment' + db.add_column('documents_documentversion', 'comment', self.gf('django.db.models.fields.TextField')(default='', blank=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'DocumentVersion.comment' + db.delete_column('documents_documentversion', 'comment') + + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "u'123068ef-26d2-45bb-8933-cb6818cd87e4'", 'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentVersion']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.documentversion': { + 'Meta': {'unique_together': "(('document', 'major', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'major': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] From 3e471f702e4fbbfd703953313512f3f44196ae7c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 2 Dec 2011 05:54:00 -0400 Subject: [PATCH 29/92] Add document version list view and document version download --- apps/documents/__init__.py | 17 ++++++++++--- apps/documents/admin.py | 6 ++--- apps/documents/models.py | 19 +++++++++------ apps/documents/urls.py | 3 +++ apps/documents/views.py | 50 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/apps/documents/__init__.py b/apps/documents/__init__.py index f47ab4bab9..1a39e01ac8 100644 --- a/apps/documents/__init__.py +++ b/apps/documents/__init__.py @@ -13,8 +13,9 @@ from history.api import register_history_type from metadata.api import get_metadata_string from project_setup.api import register_setup -from documents.models import Document, DocumentPage, \ - DocumentPageTransformation, DocumentType, DocumentTypeFilename +from documents.models import (Document, DocumentPage, + DocumentPageTransformation, DocumentType, DocumentTypeFilename, + DocumentVersion) from documents.literals import PERMISSION_DOCUMENT_CREATE, \ PERMISSION_DOCUMENT_PROPERTIES_EDIT, PERMISSION_DOCUMENT_VIEW, \ PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_DOWNLOAD, \ @@ -78,6 +79,7 @@ document_multiple_delete = {'text': _(u'delete'), 'view': 'document_multiple_del document_edit = {'text': _(u'edit'), 'view': 'document_edit', 'args': 'object.id', 'famfam': 'page_edit', 'permissions': [PERMISSION_DOCUMENT_PROPERTIES_EDIT]} document_preview = {'text': _(u'preview'), 'class': 'fancybox', 'view': 'document_preview', 'args': 'object.id', 'famfam': 'magnifier', 'permissions': [PERMISSION_DOCUMENT_VIEW]} document_download = {'text': _(u'download'), 'view': 'document_download', 'args': 'object.id', 'famfam': 'page_save', 'permissions': [PERMISSION_DOCUMENT_DOWNLOAD]} +document_version_download = {'text': _(u'download'), 'view': 'document_version_download', 'args': 'object.pk', 'famfam': 'page_save', 'permissions': [PERMISSION_DOCUMENT_DOWNLOAD]} document_find_duplicates = {'text': _(u'find duplicates'), 'view': 'document_find_duplicates', 'args': 'object.id', 'famfam': 'page_refresh', 'permissions': [PERMISSION_DOCUMENT_VIEW]} document_find_all_duplicates = {'text': _(u'find all duplicates'), 'view': 'document_find_all_duplicates', 'famfam': 'page_refresh', 'permissions': [PERMISSION_DOCUMENT_VIEW], 'description': _(u'Search all the documents\' checksums and return a list of the exact matches.')} document_update_page_count = {'text': _(u'update office documents\' page count'), 'view': 'document_update_page_count', 'famfam': 'page_white_csharp', 'permissions': [PERMISSION_DOCUMENT_TOOLS], 'description': _(u'Update the page count of the office type documents. This is useful when enabling office document support after there were already office type documents in the database.')} @@ -85,8 +87,12 @@ document_clear_transformations = {'text': _(u'clear transformations'), 'view': ' document_multiple_clear_transformations = {'text': _(u'clear transformations'), 'view': 'document_multiple_clear_transformations', 'famfam': 'page_paintbrush', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} document_print = {'text': _(u'print'), 'view': 'document_print', 'args': 'object.id', 'famfam': 'printer', 'permissions': [PERMISSION_DOCUMENT_VIEW]} document_history_view = {'text': _(u'history'), 'view': 'history_for_object', 'args': ['"documents"', '"document"', 'object.id'], 'famfam': 'book_go', 'permissions': [PERMISSION_DOCUMENT_VIEW]} +document_missing_list = {'text': _(u'Find missing document files'), 'view': 'document_missing_list', 'famfam': 'folder_page', 'permissions': [PERMISSION_DOCUMENT_VIEW]} + +# Tools document_clear_image_cache = {'text': _(u'Clear the document image cache'), 'view': 'document_clear_image_cache', 'famfam': 'camera_delete', 'permissions': [PERMISSION_DOCUMENT_TOOLS], 'description': _(u'Clear the graphics representations used to speed up the documents\' display and interactive transformations results.')} +# Document pages document_page_transformation_list = {'text': _(u'page transformations'), 'class': 'no-parent-history', 'view': 'document_page_transformation_list', 'args': 'page.pk', 'famfam': 'pencil_go', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} document_page_transformation_create = {'text': _(u'create new transformation'), 'class': 'no-parent-history', 'view': 'document_page_transformation_create', 'args': 'page.pk', 'famfam': 'pencil_add', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} document_page_transformation_edit = {'text': _(u'edit'), 'class': 'no-parent-history', 'view': 'document_page_transformation_edit', 'args': 'transformation.pk', 'famfam': 'pencil_go', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} @@ -105,7 +111,8 @@ document_page_rotate_right = {'text': _(u'rotate right'), 'class': 'no-parent-hi document_page_rotate_left = {'text': _(u'rotate left'), 'class': 'no-parent-history', 'view': 'document_page_rotate_left', 'args': 'page.pk', 'famfam': 'arrow_turn_left', 'permissions': [PERMISSION_DOCUMENT_VIEW]} document_page_view_reset = {'text': _(u'reset view'), 'class': 'no-parent-history', 'view': 'document_page_view_reset', 'args': 'page.pk', 'famfam': 'page_white', 'permissions': [PERMISSION_DOCUMENT_VIEW]} -document_missing_list = {'text': _(u'Find missing document files'), 'view': 'document_missing_list', 'famfam': 'folder_page', 'permissions': [PERMISSION_DOCUMENT_VIEW]} +# Document versions +document_version_list = {'text': _(u'versions'), 'view': 'document_version_list', 'args': 'object.pk', 'famfam': 'page_world', 'permissions': [PERMISSION_DOCUMENT_VIEW]} # Document type related links document_type_list = {'text': _(u'document type list'), 'view': 'document_type_list', 'famfam': 'layout', 'permissions': [PERMISSION_DOCUMENT_VIEW]} @@ -133,6 +140,9 @@ register_links(['document_type_filename_create', 'document_type_filename_list', register_links(Document, [document_edit, document_print, document_delete, document_download, document_find_duplicates, document_clear_transformations, document_create_siblings]) register_multi_item_links(['document_find_duplicates', 'folder_view', 'index_instance_list', 'document_type_document_list', 'search', 'results', 'document_group_view', 'document_list', 'document_list_recent'], [document_multiple_clear_transformations, document_multiple_delete]) +# Document Version links +register_links(DocumentVersion, [document_version_download]) + secondary_menu_links = [document_list_recent, document_list, document_create_multiple] register_links(['document_list_recent', 'document_list', 'document_create', 'document_create_multiple', 'upload_interactive', 'staging_file_delete'], secondary_menu_links, menu_name='secondary_menu') @@ -198,6 +208,7 @@ register_sidebar_template(['document_type_list'], 'document_types_help.html') register_links(Document, [document_view_simple], menu_name='form_header', position=0) register_links(Document, [document_view_advanced], menu_name='form_header', position=1) register_links(Document, [document_history_view], menu_name='form_header') +register_links(Document, [document_version_list], menu_name='form_header') if (validate_path(document_settings.CACHE_PATH) == False) or (not document_settings.CACHE_PATH): setattr(document_settings, 'CACHE_PATH', tempfile.mkdtemp()) diff --git a/apps/documents/admin.py b/apps/documents/admin.py index 8ef02b1cea..8fc0e838bb 100644 --- a/apps/documents/admin.py +++ b/apps/documents/admin.py @@ -20,9 +20,9 @@ class DocumentVersionInline(admin.StackedInline): extra = 1 classes = ('collapse-open',) allow_add = True - inlines = [ - DocumentPageInline, - ] + #inlines = [ + # DocumentPageInline, + #] class DocumentTypeFilenameInline(admin.StackedInline): diff --git a/apps/documents/models.py b/apps/documents/models.py index 6d68e03ae6..ab1165bf5f 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -361,6 +361,10 @@ class Document(models.Model): def get_latest_version(self): return self.documentversion_set.order_by('-timestamp')[0] + @property + def versions(self): + return self.documentversion_set + RELEASE_LEVEL_FINAL = 1 RELEASE_LEVEL_ALPHA = 2 @@ -381,12 +385,13 @@ class DocumentVersion(models.Model): Model that describes a document version and it properties ''' document = models.ForeignKey(Document, verbose_name=_(u'document')) - mayor = models.PositiveIntegerField(verbose_name=_(u'mayor'), default=1) + major = models.PositiveIntegerField(verbose_name=_(u'mayor'), default=1) minor = models.PositiveIntegerField(verbose_name=_(u'minor'), default=0) micro = models.PositiveIntegerField(verbose_name=_(u'micro'), default=0) release_level = models.PositiveIntegerField(choices=RELEASE_LEVEL_CHOICES, default=RELEASE_LEVEL_FINAL, verbose_name=_(u'release level')) serial = models.PositiveIntegerField(verbose_name=_(u'serial'), default=0) timestamp = models.DateTimeField(verbose_name=_(u'timestamp')) + comment = models.TextField(blank=True, verbose_name=_(u'comment')) # File related fields file = models.FileField(upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'file')) @@ -396,7 +401,7 @@ class DocumentVersion(models.Model): checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False) class Meta: - unique_together = ('document', 'mayor', 'minor', 'micro', 'release_level', 'serial') + unique_together = ('document', 'major', 'minor', 'micro', 'release_level', 'serial') verbose_name = _(u'document version') verbose_name_plural = _(u'document version') @@ -405,16 +410,16 @@ class DocumentVersion(models.Model): # TODO: Update timestamp - def get_version(): + def get_version(self): ''' Return the formatted version information ''' - vers = [u'%(major)i.%(minor)i' % self, ] + vers = [u'%i.%i' % (self.major, self.minor), ] if self.micro: - vers.append(u'.%(micro)i' % self) - if self.releaselevel != RELEASE_LEVEL_FINAL: - vers.append(u'%(releaselevel)s%(serial)i' % self) + vers.append(u'.%i' % self.micro) + if self.release_level != RELEASE_LEVEL_FINAL: + vers.append(u'%s%i' % (self.release_level, self.serial)) return u''.join(vers) @property diff --git a/apps/documents/urls.py b/apps/documents/urls.py index e3cfbf598d..88650155a7 100644 --- a/apps/documents/urls.py +++ b/apps/documents/urls.py @@ -33,6 +33,9 @@ urlpatterns = patterns('documents.views', url(r'^(?P\d+)/create/siblings/$', 'document_create_siblings', (), 'document_create_siblings'), url(r'^(?P\d+)/find_duplicates/$', 'document_find_duplicates', (), 'document_find_duplicates'), url(r'^(?P\d+)/clear_transformations/$', 'document_clear_transformations', (), 'document_clear_transformations'), + + url(r'^(?P\d+)/version/all/$', 'document_version_list', (), 'document_version_list'), + url(r'^document/version/(?P\d+)/download/$', 'document_version_download', (), 'document_version_download'), url(r'^multiple/clear_transformations/$', 'document_multiple_clear_transformations', (), 'document_multiple_clear_transformations'), url(r'^duplicates/list/$', 'document_find_all_duplicates', (), 'document_find_all_duplicates'), diff --git a/apps/documents/views.py b/apps/documents/views.py index ba0e18e315..efaaf3e2da 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -53,8 +53,9 @@ from documents.forms import DocumentTypeSelectForm, \ DocumentPageForm_text, PrintForm, DocumentTypeForm, \ DocumentTypeFilenameForm, DocumentTypeFilenameForm_create from documents.wizards import DocumentCreateWizard -from documents.models import Document, DocumentType, DocumentPage, \ - DocumentPageTransformation, RecentDocument, DocumentTypeFilename +from documents.models import (Document, DocumentType, DocumentPage, + DocumentPageTransformation, RecentDocument, DocumentTypeFilename, + DocumentVersion) # Document type permissions from documents.literals import PERMISSION_DOCUMENT_TYPE_EDIT, \ @@ -294,6 +295,10 @@ def get_document_image(request, document_id, size=PREVIEW_SIZE, base64_version=F return sendfile.sendfile(request, document.get_image(size=size, page=page, zoom=zoom, rotation=rotation), mimetype=DEFAULT_FILE_FORMAT_MIMETYPE) +def document_version_download(request, document_version_pk): + document_version = get_object_or_404(DocumentVersion, pk=document_version_pk) + return document_download(request, document_version.document.pk) + def document_download(request, document_id): check_permissions(request.user, [PERMISSION_DOCUMENT_DOWNLOAD]) @@ -1142,3 +1147,44 @@ def document_clear_image_cache(request): 'title': _(u'Are you sure you wish to clear the document image cache?'), 'form_icon': u'camera_delete.png', }, context_instance=RequestContext(request)) + + +def document_version_list(request, document_pk): + check_permissions(request.user, [PERMISSION_DOCUMENT_VIEW]) + document = get_object_or_404(Document, pk=document_pk) + + context = { + 'object_list': document.versions.all(), + 'title': _(u'versions for document: %s') % document, + 'hide_object': True, + 'object': document, + 'extra_columns': [ + { + 'name': _(u'version'), + 'attribute': 'get_version', + }, + { + 'name': _(u'time and date'), + 'attribute': 'timestamp', + }, + { + 'name': _(u'mimetype'), + 'attribute': 'mimetype', + }, + { + 'name': _(u'encoding'), + 'attribute': 'encoding', + }, + { + 'name': _(u'filename'), + 'attribute': 'filename', + }, + { + 'name': _(u'comment'), + 'attribute': 'comment', + }, + ] + } + + return render_to_response('generic_list.html', context, + context_instance=RequestContext(request)) From 5905dbd3e0786ffca4df987c2d9ee491b5f05bad Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 2 Dec 2011 05:54:36 -0400 Subject: [PATCH 30/92] Updated changelog --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6f5cd78643..17106351dd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,3 +1,7 @@ +2011-12-2 +--------- +* Added migrations and model updated to support document versions + 2011-12-1 --------- * OCR queue processing improvements From e92a0f79964477dfb18a5dc1c3aa4ded9ad05778 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 2 Dec 2011 05:55:01 -0400 Subject: [PATCH 31/92] Updated version to 0.11beta2 and add python authoring structures --- apps/main/__init__.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/apps/main/__init__.py b/apps/main/__init__.py index 0532245481..582747e0d5 100644 --- a/apps/main/__init__.py +++ b/apps/main/__init__.py @@ -8,24 +8,33 @@ from project_tools.api import register_tool from main.conf.settings import SIDE_BAR_SEARCH from main.conf.settings import DISABLE_HOME_VIEW +__author__ = 'Roberto Rosario' +__copyright__ = 'Copyright 2011 Roberto Rosario' +__credits__ = ['Roberto Rosario',] +__license__ = 'GPL' +__maintainer__ = 'Roberto Rosario' +__email__ = 'roberto.rosario.gonzalez@gmail.com' +__status__ = 'Production' + +__version_info__ = { + 'major': 0, + 'minor': 11, + 'micro': 0, + 'releaselevel': 'beta', + 'serial': 2 +} + def is_superuser(context): return context['request'].user.is_staff or context['request'].user.is_superuser + maintenance_menu = {'text': _(u'maintenance'), 'view': 'maintenance_menu', 'famfam': 'wrench', 'icon': 'wrench.png'} statistics = {'text': _(u'statistics'), 'view': 'statistics', 'famfam': 'table', 'icon': 'blackboard_sum.png'} diagnostics = {'text': _(u'diagnostics'), 'view': 'diagnostics', 'famfam': 'pill', 'icon': 'pill.png'} sentry = {'text': _(u'sentry'), 'view': 'sentry', 'famfam': 'bug', 'icon': 'bug.png', 'condition': is_superuser} admin_site = {'text': _(u'admin site'), 'view': 'admin:index', 'famfam': 'keyboard', 'icon': 'keyboard.png', 'condition': is_superuser} -__version_info__ = { - 'major': 0, - 'minor': 10, - 'micro': 0, - 'releaselevel': 'final', - 'serial': 0 -} - if not DISABLE_HOME_VIEW: register_top_menu('home', link={'text': _(u'home'), 'view': 'home', 'famfam': 'house'}, position=0) if not SIDE_BAR_SEARCH: @@ -36,14 +45,15 @@ def get_version(): """ Return the formatted version information """ - vers = ["%(major)i.%(minor)i" % __version_info__, ] + vers = ['%(major)i.%(minor)i' % __version_info__, ] if __version_info__['micro']: - vers.append(".%(micro)i" % __version_info__) + vers.append('.%(micro)i' % __version_info__) if __version_info__['releaselevel'] != 'final': vers.append('%(releaselevel)s%(serial)i' % __version_info__) return ''.join(vers) + __version__ = get_version() register_setup(admin_site) From 8c02c4c426d436e635aea5fcd976583e66039dbf Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 2 Dec 2011 05:55:42 -0400 Subject: [PATCH 32/92] Disable ocr document queue signal It appears Django executes signals using the same process as the caller, effectively blocking the view until the OCR process completes which could take several minutes :/ --- apps/ocr/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/ocr/__init__.py b/apps/ocr/__init__.py index def0d7064b..b07fa07d72 100644 --- a/apps/ocr/__init__.py +++ b/apps/ocr/__init__.py @@ -108,11 +108,14 @@ def document_post_save(sender, instance, **kwargs): post_save.connect(document_post_save, sender=Document) -@receiver(post_save, dispatch_uid='call_queue', sender=QueueDocument) -def call_queue(sender, **kwargs): - if kwargs.get('created', False): - logger.debug('got call_queue signal: %s' % kwargs) - task_process_document_queues() +# Disabled because it appears Django execute signals using the same +# process effectively blocking the view until the OCR process completes +# which could take several minutes :/ +#@receiver(post_save, dispatch_uid='call_queue', sender=QueueDocument) +#def call_queue(sender, **kwargs): +# if kwargs.get('created', False): +# logger.debug('got call_queue signal: %s' % kwargs) +# task_process_document_queues() create_default_queue() From 1e38369919e71945aae04819b63a51f36a3f0531 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 2 Dec 2011 05:56:34 -0400 Subject: [PATCH 33/92] Update parser to use the latest version of a document when extracting text --- apps/ocr/parsers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ocr/parsers/__init__.py b/apps/ocr/parsers/__init__.py index 8fac71c084..3d5a39635e 100644 --- a/apps/ocr/parsers/__init__.py +++ b/apps/ocr/parsers/__init__.py @@ -26,7 +26,7 @@ def register_parser(function, mimetype=None, mimetypes=None): def pdf_parser(document_page, descriptor=None): if not descriptor: - descriptor = document_page.document.open() + descriptor = document_page.document_version.open() pdf_pages = slate.PDF(descriptor) descriptor.close() From d4a70c0fc12c6434428f024a8506fbfcedf53af2 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 19:31:59 -0400 Subject: [PATCH 34/92] Cleanup remarks from migration --- apps/documents/migrations/0005_document_versions.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/documents/migrations/0005_document_versions.py b/apps/documents/migrations/0005_document_versions.py index 738dfe9147..7244593da3 100644 --- a/apps/documents/migrations/0005_document_versions.py +++ b/apps/documents/migrations/0005_document_versions.py @@ -10,11 +10,6 @@ class Migration(DataMigration): for document in orm.Document.objects.all(): document_version = document.documentversion_set.create( document = document, - #mayor = 1 - #minor = models.PositiveIntegerField(verbose_name=_(u'minor'), default=0) - #micro = models.PositiveIntegerField(verbose_name=_(u'micro'), default=0) - #release_level = models.PositiveIntegerField(choices=RELEASE_LEVEL_CHOICES, default=RELEASE_LEVEL_FINAL, verbose_name=_(u'release level')) - #serial = models.PositiveIntegerField(verbose_name=_(u'serial'), default=0) timestamp = document.date_added, file = document.file, mimetype = document.file_mimetype, From 9f7e5de5ac154e7838745aaafbd9082c789933f7 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 19:32:40 -0400 Subject: [PATCH 35/92] Add migration to remove old file fields --- apps/documents/migrations/0006_remove_old_file_fields.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/documents/migrations/0006_remove_old_file_fields.py b/apps/documents/migrations/0006_remove_old_file_fields.py index 203532d012..7abf68d028 100644 --- a/apps/documents/migrations/0006_remove_old_file_fields.py +++ b/apps/documents/migrations/0006_remove_old_file_fields.py @@ -9,7 +9,7 @@ class Migration(SchemaMigration): def forwards(self, orm): # Changing field 'DocumentVersion.document' - db.alter_column('documents_documentversion', 'document_id', self.gf('django.db.models.fields.related.ForeignKey')(default='', to=orm['documents.Document'])) + db.alter_column('documents_documentversion', 'document_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['documents.Document'])) # Deleting field 'Document.date_updated' db.delete_column('documents_document', 'date_updated') @@ -33,7 +33,8 @@ class Migration(SchemaMigration): db.delete_column('documents_documentpage', 'document_id') # Changing field 'DocumentPage.document_version' - db.alter_column('documents_documentpage', 'document_version_id', self.gf('django.db.models.fields.related.ForeignKey')(default='', to=orm['documents.DocumentVersion'])) + #db.alter_column('documents_documentpage', 'document_version_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['documents.DocumentVersion'])) + db.alter_column('documents_documentpage', 'document_version_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.DocumentVersion'])) def backwards(self, orm): @@ -60,7 +61,7 @@ class Migration(SchemaMigration): db.add_column('documents_document', 'file_mime_encoding', self.gf('django.db.models.fields.CharField')(default='', max_length=64), keep_default=False) # Adding field 'DocumentPage.document' - db.add_column('documents_documentpage', 'document', self.gf('django.db.models.fields.related.ForeignKey')(default='', to=orm['documents.Document']), keep_default=False) + db.add_column('documents_documentpage', 'document', self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['documents.Document']), keep_default=False) # Changing field 'DocumentPage.document_version' db.alter_column('documents_documentpage', 'document_version_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.DocumentVersion'], null=True)) From 11725c943b70938b016ad32e4a44d885a5450c98 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 19:35:03 -0400 Subject: [PATCH 36/92] Update documents app to support multiple versions --- apps/documents/forms.py | 55 ++++- apps/documents/literals.py | 18 ++ apps/documents/models.py | 490 +++++++++++++++++++++---------------- apps/documents/urls.py | 2 +- apps/documents/views.py | 48 ++-- 5 files changed, 373 insertions(+), 240 deletions(-) diff --git a/apps/documents/forms.py b/apps/documents/forms.py index 47ac8215da..1033692bfe 100644 --- a/apps/documents/forms.py +++ b/apps/documents/forms.py @@ -11,9 +11,11 @@ from common.conf.settings import DEFAULT_PAPER_SIZE from common.conf.settings import DEFAULT_PAGE_ORIENTATION from common.widgets import TextAreaDiv -from documents.models import Document, DocumentType, \ - DocumentPage, DocumentPageTransformation, DocumentTypeFilename +from documents.models import (Document, DocumentType, + DocumentPage, DocumentPageTransformation, DocumentTypeFilename, + DocumentVersion) from documents.widgets import document_html_widget +from documents.literals import (RELEASE_LEVEL_FINAL, RELEASE_LEVEL_CHOICES) # Document page forms class DocumentPageTransformationForm(forms.ModelForm): @@ -147,6 +149,8 @@ class DocumentForm(forms.ModelForm): instance = kwargs.pop('instance', None) super(DocumentForm, self).__init__(*args, **kwargs) + if instance: + self.version_fields(instance) if 'document_type' in self.fields: # To allow merging with DocumentForm_edit @@ -164,11 +168,51 @@ class DocumentForm(forms.ModelForm): queryset=filenames_qs, required=False, label=_(u'Quick document rename')) + + def version_fields(self, document): + self.fields['comment'] = forms.CharField( + label=_(u'Comment'), + required=False, + widget=forms.widgets.Textarea(attrs={'rows': 4}), + ) + + self.fields['version_update'] = forms.ChoiceField( + label=_(u'Version update'), + #widget=forms.widgets.RadioSelect(), + choices=DocumentVersion.get_version_update_choices(document.latest_version) + ) + + self.fields['release_level'] = forms.ChoiceField( + label=_(u'Release level'), + choices=RELEASE_LEVEL_CHOICES, + initial=RELEASE_LEVEL_FINAL, + #required=False, + ) + + self.fields['serial'] = forms.IntegerField( + label=_(u'Release level serial'), + initial=0, + widget=forms.widgets.TextInput( + attrs = {'style': 'width: auto;'} + ), + #required=False + ) new_filename = forms.CharField( label=_('New document filename'), required=False ) + + def clean(self): + cleaned_data = self.cleaned_data + cleaned_data['new_version_data'] = { + 'comment': self.cleaned_data.get('comment'), + 'version_update': self.cleaned_data.get('version_update'), + 'release_level': self.cleaned_data.get('release_level'), + 'serial': self.cleaned_data.get('serial'), + } + # Always return the full collection of cleaned data. + return cleaned_data class DocumentForm_edit(DocumentForm): """ @@ -177,6 +221,13 @@ class DocumentForm_edit(DocumentForm): class Meta: model = Document exclude = ('file', 'document_type', 'tags') + + def __init__(self, *args, **kwargs): + super(DocumentForm_edit, self).__init__(*args, **kwargs) + self.fields.pop('serial') + self.fields.pop('release_level') + self.fields.pop('version_update') + self.fields.pop('comment') class DocumentPropertiesForm(DetailForm): diff --git a/apps/documents/literals.py b/apps/documents/literals.py index cbb3b919d9..30b0284f5f 100644 --- a/apps/documents/literals.py +++ b/apps/documents/literals.py @@ -44,3 +44,21 @@ HISTORY_DOCUMENT_DELETED = { 'details': _(u'Document "%(document)s" deleted on %(datetime)s by %(fullname)s.'), 'expressions': {'fullname': 'user.get_full_name() if user.get_full_name() else user.username'} } + +RELEASE_LEVEL_FINAL = 1 +RELEASE_LEVEL_ALPHA = 2 +RELEASE_LEVEL_BETA = 3 +RELEASE_LEVEL_RC = 4 +RELEASE_LEVEL_HF = 5 + +RELEASE_LEVEL_CHOICES = ( + (RELEASE_LEVEL_FINAL, _(u'final')), + (RELEASE_LEVEL_ALPHA, _(u'alpha')), + (RELEASE_LEVEL_BETA, _(u'beta')), + (RELEASE_LEVEL_RC, _(u'release candidate')), + (RELEASE_LEVEL_HF, _(u'hotfix')), +) + +VERSION_UPDATE_MAJOR = u'major' +VERSION_UPDATE_MINOR = u'minor' +VERSION_UPDATE_MICRO = u'micro' diff --git a/apps/documents/models.py b/apps/documents/models.py index ab1165bf5f..04c633556f 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -4,6 +4,8 @@ import hashlib from ast import literal_eval import base64 from StringIO import StringIO +import datetime +import logging from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -21,6 +23,8 @@ from converter.api import convert from converter.exceptions import UnknownFileFormat, UnkownConvertError from mimetype.api import get_mimetype, get_icon_file_path, \ get_error_icon_file_path +from converter.literals import (DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION, + DEFAULT_PAGE_NUMBER) from documents.conf.settings import CHECKSUM_FUNCTION from documents.conf.settings import UUID_FUNCTION @@ -30,26 +34,25 @@ from documents.conf.settings import DISPLAY_SIZE from documents.conf.settings import CACHE_PATH from documents.conf.settings import ZOOM_MAX_LEVEL from documents.conf.settings import ZOOM_MIN_LEVEL - from documents.managers import RecentDocumentManager, \ DocumentPageTransformationManager from documents.utils import document_save_to_temp_dir -from converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION, \ - DEFAULT_PAGE_NUMBER +from documents.literals import (RELEASE_LEVEL_FINAL, RELEASE_LEVEL_CHOICES, + VERSION_UPDATE_MAJOR, VERSION_UPDATE_MINOR, VERSION_UPDATE_MICRO) # document image cache name hash function HASH_FUNCTION = lambda x: hashlib.sha256(x).hexdigest() +logger = logging.getLogger(__name__) + def get_filename_from_uuid(instance, filename): """ Store the orignal filename of the uploaded file and replace it with a UUID """ - instance.file_filename = filename - uuid = UUID_FUNCTION() - instance.uuid = uuid - return uuid + instance.filename = filename + return UUID_FUNCTION() class DocumentType(models.Model): @@ -69,24 +72,15 @@ class DocumentType(models.Model): class Document(models.Model): - """ + ''' Defines a single document with it's fields and properties - """ - # Base fields + ''' + uuid = models.CharField(max_length=48, blank=True, editable=False) document_type = models.ForeignKey(DocumentType, verbose_name=_(u'document type'), null=True, blank=True) - uuid = models.CharField(max_length=48, default=UUID_FUNCTION(), blank=True, editable=False) description = models.TextField(blank=True, null=True, verbose_name=_(u'description'), db_index=True) + #TODO: remove date_added, it is the timestamp of the first version date_added = models.DateTimeField(verbose_name=_(u'added'), auto_now_add=True, db_index=True) - ## Fields to migrate - #file = models.FileField(upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'file')) - #file_mimetype = models.CharField(max_length=64, default='', editable=False) - #file_mime_encoding = models.CharField(max_length=64, default='', editable=False) - ##FAT filename can be up to 255 using LFN - #file_filename = models.CharField(max_length=255, default=u'', editable=False, db_index=True) - #date_updated = models.DateTimeField(verbose_name=_(u'updated'), auto_now=True) - #checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False) - tags = TaggableManager() comments = generic.GenericRelation( @@ -108,147 +102,16 @@ class Document(models.Model): ordering = ['-date_added'] def __unicode__(self): - return self.get_fullname() - - def save(self, *args, **kwargs): - """ - Overloaded save method that updates the document's checksum, - mimetype, page count and transformation when originally created - """ - new_document = not self.pk - transformations = kwargs.pop('transformations', None) - super(Document, self).save(*args, **kwargs) - - if new_document: - #Only do this for new documents - self.update_checksum(save=False) - self.update_mimetype(save=False) - self.save() - self.update_page_count(save=False) - if transformations: - self.apply_default_transformations(transformations) + return self.latest_version.filename @models.permalink def get_absolute_url(self): return ('document_view_simple', [self.pk]) - def get_fullname(self): - """ - Return the fullname of the document's file - """ - return self.file_filename - - def update_mimetype(self, save=True): - """ - Read a document's file and determine the mimetype by calling the - get_mimetype wrapper - """ - if self.exists(): - try: - self.file_mimetype, self.file_mime_encoding = get_mimetype(self.open(), self.get_fullname()) - except: - self.file_mimetype = u'' - self.file_mime_encoding = u'' - finally: - if save: - self.save() - - def open(self): - """ - Return a file descriptor to a document's file irrespective of - the storage backend - """ - #return self.file.storage.open(self.file.path) - return self.get_latest_version().file.storage.open(self.get_latest_version().file.path) - - def update_checksum(self, save=True): - """ - Open a document's file and update the checksum field using the - user provided checksum function - """ - if self.exists(): - source = self.get_latest_version().open() - self.get_latest_version().checksum = unicode(CHECKSUM_FUNCTION(source.read())) - source.close() - if save: - self.get_latest_version().save() - - def update_page_count(self, save=True): - handle, filepath = tempfile.mkstemp() - # Just need the filepath, close the file description - os.close(handle) - - self.save_to_file(filepath) - try: - detected_pages = get_page_count(filepath) - except UnknownFileFormat: - # If converter backend doesn't understand the format, - # use 1 as the total page count - detected_pages = 1 - self.description = ugettext(u'This document\'s file format is not known, the page count has therefore defaulted to 1.') - self.save() - try: - os.remove(filepath) - except OSError: - pass - - current_pages = DocumentPage.objects.filter(document=self).order_by('page_number',) - if current_pages.count() > detected_pages: - for page in current_pages[detected_pages:]: - page.delete() - - for page_number in range(detected_pages): - DocumentPage.objects.get_or_create( - document=self, page_number=page_number + 1) - - if save: - self.save() - - return detected_pages - - @property - def page_count(self): - #return self.documentpage_set.count() - return self.get_latest_version().documentpage_set.count() - - def save_to_file(self, filepath, buffer_size=1024 * 1024): - """ - Save a copy of the document from the document storage backend - to the local filesystem - """ - input_descriptor = self.open() - output_descriptor = open(filepath, 'wb') - while True: - copy_buffer = input_descriptor.read(buffer_size) - if copy_buffer: - output_descriptor.write(copy_buffer) - else: - break - - output_descriptor.close() - input_descriptor.close() - return filepath - - def exists(self): - """ - Returns a boolean value that indicates if the document's file - exists in storage - """ - return self.get_latest_version().file.storage.exists(self.get_latest_version().file.path) - - def apply_default_transformations(self, transformations): - #Only apply default transformations on new documents - if reduce(lambda x, y: x + y, [page.documentpagetransformation_set.count() for page in self.pages.all()]) == 0: - for transformation in transformations: - for document_page in self.pages.all(): - page_transformation = DocumentPageTransformation( - document_page=document_page, - order=0, - transformation=transformation.get('transformation'), - arguments=transformation.get('arguments') - ) - - page_transformation.save() + def save(self, *args, **kwargs): + if not self.pk: + self.uuid = UUID_FUNCTION() + super(Document, self).save(*args, **kwargs) def get_cached_image_name(self, page): document_page = self.pages.get(page_number=page) @@ -302,95 +165,154 @@ class Document(models.Model): def add_as_recent_document_for_user(self, user): RecentDocument.objects.add_document_for_user(user, self) - - def delete(self, *args, **kwargs): - super(Document, self).delete(*args, **kwargs) - for version in self.documentversion_set.all(): - version.file.storage.delete(version.file.path) - #return self.get_latest_version().file.storage.delete(self.get_latest_version().file.path) + + # TODO: investigate if Document's save method calls all of it + # DocumentVersion's delete methods + #def delete(self, *args, **kwargs): + # super(Document, self).delete(*args, **kwargs) + # for version in self.documentversion_set.all(): + # version.file.storage.delete(version.file.path) @property def size(self): if self.exists(): - return self.get_latest_version().file.storage.size(self.get_latest_version().file.path) + return self.latest_version.exists() else: return None + + def new_version(self, file, comment=None, version_update=None, release_level=None, serial=None): + logger.debug('creating new document version') + if version_update: + new_version_dict = self.latest_version.get_new_version_dict(version_update) + new_version = DocumentVersion( + document=self, + file=file, + major = new_version_dict.get('major'), + minor = new_version_dict.get('minor'), + micro = new_version_dict.get('micro'), + release_level = release_level, + serial = serial, + comment = comment, + ) + new_version.save() + else: + new_version_dict = {} + new_version = DocumentVersion( + document=self, + file=file, + #major = new_version_dict.get('major'), + #minor = new_version_dict.get('minor'), + #micro = new_version_dict.get('micro'), + #release_level = release_level, + #serial = serial, + #comment = comment, + ) + new_version.save() - # Compatibiliy methods + + logger.debug('new_version_dict: %s' % new_version_dict) + + logger.debug('new_version saved') + return new_version + + # Proxy methods + def open(self): + ''' + Return a file descriptor to a document's file irrespective of + the storage backend + ''' + return self.latest_version.open() + + def save_to_file(self, *args, **kwargs): + return self.latest_version.save_to_file(*args, **kwargs) + + def exists(self): + ''' + Returns a boolean value that indicates if the document's + latest version file exists in storage + ''' + return self.latest_version.exists() + + # Compatibility methods @property def file(self): - return self.get_latest_version().file + return self.latest_version.file @property def file_mimetype(self): - return self.get_latest_version().mimetype + return self.latest_version.mimetype @property def file_mime_encoding(self): - return self.get_latest_version().encoding + return self.latest_version.encoding @property def file_filename(self): - return self.get_latest_version().filename + return self.latest_version.filename @property def date_updated(self): - return self.get_latest_version().timestamp + return self.latest_version.timestamp + # TODO: uncomment when date_added is removed #@property #def date_added(self): - # return self.get_latest_version().timestamp + # return self.first_version.timestamp @property def checksum(self): - return self.get_latest_version().checksum + return self.latest_version.checksum @property def pages(self): - return self.get_latest_version().pages + return self.latest_version.pages - - #file = models.FileField(upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'file')) - #file_mimetype = models.CharField(max_length=64, default='', editable=False) - #file_mime_encoding = models.CharField(max_length=64, default='', editable=False) - ##FAT filename can be up to 255 using LFN - #file_filename = models.CharField(max_length=255, default=u'', editable=False, db_index=True) - #date_updated = models.DateTimeField(verbose_name=_(u'updated'), auto_now=True) - #checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False) + @property + def page_count(self): + return self.pages.count() - def get_latest_version(self): + @property + def latest_version(self): return self.documentversion_set.order_by('-timestamp')[0] + @property + def first_version(self): + return self.documentversion_set.order_by('timestamp')[0] + @property def versions(self): return self.documentversion_set + def _get_filename(self): + return self.latest_version.filename -RELEASE_LEVEL_FINAL = 1 -RELEASE_LEVEL_ALPHA = 2 -RELEASE_LEVEL_BETA = 3 -RELEASE_LEVEL_RC = 4 -RELEASE_LEVEL_HF = 5 + def _set_filename(self, value): + version = self.latest_version + version.filename = value + return version.save() -RELEASE_LEVEL_CHOICES = ( - (RELEASE_LEVEL_FINAL, _(u'final')), - (RELEASE_LEVEL_ALPHA, _(u'alpha')), - (RELEASE_LEVEL_BETA, _(u'beta')), - (RELEASE_LEVEL_RC, _(u'release candidate')), - (RELEASE_LEVEL_HF, _(u'hotfix')), -) + filename = property(_get_filename, _set_filename) + class DocumentVersion(models.Model): ''' - Model that describes a document version and it properties + Model that describes a document version and its properties ''' - document = models.ForeignKey(Document, verbose_name=_(u'document')) - major = models.PositiveIntegerField(verbose_name=_(u'mayor'), default=1) - minor = models.PositiveIntegerField(verbose_name=_(u'minor'), default=0) - micro = models.PositiveIntegerField(verbose_name=_(u'micro'), default=0) - release_level = models.PositiveIntegerField(choices=RELEASE_LEVEL_CHOICES, default=RELEASE_LEVEL_FINAL, verbose_name=_(u'release level')) - serial = models.PositiveIntegerField(verbose_name=_(u'serial'), default=0) - timestamp = models.DateTimeField(verbose_name=_(u'timestamp')) + @staticmethod + def get_version_update_choices(document_version): + return ( + (VERSION_UPDATE_MAJOR, _(u'Major %(major)i.%(minor)i, (new release)') % document_version.get_new_version_dict(VERSION_UPDATE_MAJOR)), + (VERSION_UPDATE_MINOR, _(u'Minor %(major)i.%(minor)i, (some updates)') % document_version.get_new_version_dict(VERSION_UPDATE_MINOR)), + (VERSION_UPDATE_MICRO, _(u'Micro %(major)i.%(minor)i.%(micro)i, (fixes)') % document_version.get_new_version_dict(VERSION_UPDATE_MICRO)) + ) + + document = models.ForeignKey(Document, verbose_name=_(u'document'), editable=False) + major = models.PositiveIntegerField(verbose_name=_(u'mayor'), default=1, editable=False) + minor = models.PositiveIntegerField(verbose_name=_(u'minor'), default=0, editable=False) + micro = models.PositiveIntegerField(verbose_name=_(u'micro'), default=0, editable=False) + release_level = models.PositiveIntegerField(choices=RELEASE_LEVEL_CHOICES, default=RELEASE_LEVEL_FINAL, verbose_name=_(u'release level'), editable=False) + serial = models.PositiveIntegerField(verbose_name=_(u'serial'), default=0, editable=False) + timestamp = models.DateTimeField(verbose_name=_(u'timestamp'), editable=False) comment = models.TextField(blank=True, verbose_name=_(u'comment')) # File related fields @@ -406,11 +328,31 @@ class DocumentVersion(models.Model): verbose_name_plural = _(u'document version') def __unicode__(self): - return self.get_version() + return self.get_formated_version() - # TODO: Update timestamp + def get_new_version_dict(self, version_update_type): + logger.debug('version_update_type: %s' % version_update_type) - def get_version(self): + if version_update_type == VERSION_UPDATE_MAJOR: + return { + 'major': self.major + 1, + 'minor': 0, + 'micro': 0, + } + elif version_update_type == VERSION_UPDATE_MINOR: + return { + 'major': self.major, + 'minor': self.minor + 1, + 'micro': 0, + } + elif version_update_type == VERSION_UPDATE_MICRO: + return { + 'major': self.major, + 'minor': self.minor, + 'micro': self.micro + 1, + } + + def get_formated_version(self): ''' Return the formatted version information ''' @@ -419,26 +361,151 @@ class DocumentVersion(models.Model): if self.micro: vers.append(u'.%i' % self.micro) if self.release_level != RELEASE_LEVEL_FINAL: - vers.append(u'%s%i' % (self.release_level, self.serial)) + vers.append(u'%s%i' % (self.get_release_level_display(), self.serial)) return u''.join(vers) @property def pages(self): return self.documentpage_set + def save(self, *args, **kwargs): + ''' + Overloaded save method that updates the document version's checksum, + mimetype, page count and transformation when created + ''' + new_document = not self.pk + if not self.pk: + self.timestamp = datetime.datetime.now() + + #Only do this for new documents + transformations = kwargs.pop('transformations', None) + super(DocumentVersion, self).save(*args, **kwargs) + + if new_document: + #Only do this for new documents + self.update_checksum(save=False) + self.update_mimetype(save=False) + self.save() + self.update_page_count(save=False) + if transformations: + self.apply_default_transformations(transformations) + + def update_checksum(self, save=True): + ''' + Open a document version's file and update the checksum field using the + user provided checksum function + ''' + if self.exists(): + source = self.open() + self.checksum = unicode(CHECKSUM_FUNCTION(source.read())) + source.close() + if save: + self.save() + + def update_page_count(self, save=True): + handle, filepath = tempfile.mkstemp() + # Just need the filepath, close the file description + os.close(handle) + + self.save_to_file(filepath) + try: + detected_pages = get_page_count(filepath) + except UnknownFileFormat: + # If converter backend doesn't understand the format, + # use 1 as the total page count + detected_pages = 1 + self.description = ugettext(u'This document\'s file format is not known, the page count has therefore defaulted to 1.') + self.save() + try: + os.remove(filepath) + except OSError: + pass + + current_pages = self.documentpage_set.order_by('page_number',) + if current_pages.count() > detected_pages: + for page in current_pages[detected_pages:]: + page.delete() + + for page_number in range(detected_pages): + DocumentPage.objects.get_or_create( + document_version=self, page_number=page_number + 1) + + if save: + self.save() + + return detected_pages + + def apply_default_transformations(self, transformations): + #Only apply default transformations on new documents + if reduce(lambda x, y: x + y, [page.documentpagetransformation_set.count() for page in self.pages.all()]) == 0: + for transformation in transformations: + for document_page in self.pages.all(): + page_transformation = DocumentPageTransformation( + document_page=document_page, + order=0, + transformation=transformation.get('transformation'), + arguments=transformation.get('arguments') + ) + + page_transformation.save() + + def update_mimetype(self, save=True): + ''' + Read a document verions's file and determine the mimetype by calling the + get_mimetype wrapper + ''' + if self.exists(): + try: + self.mimetype, self.encoding = get_mimetype(self.open(), self.filename) + except: + self.mimetype = u'' + self.encoding = u'' + finally: + if save: + self.save() + + def delete(self, *args, **kwargs): + super(Document, self).delete(*args, **kwargs) + return self.file.storage.delete(self.file.path) + + def exists(self): + ''' + Returns a boolean value that indicates if the document's file + exists in storage + ''' + return self.file.storage.exists(self.file.path) + def open(self): ''' Return a file descriptor to a document version's file irrespective of the storage backend ''' return self.file.storage.open(self.file.path) + + def save_to_file(self, filepath, buffer_size=1024 * 1024): + ''' + Save a copy of the document from the document storage backend + to the local filesystem + ''' + input_descriptor = self.open() + output_descriptor = open(filepath, 'wb') + while True: + copy_buffer = input_descriptor.read(buffer_size) + if copy_buffer: + output_descriptor.write(copy_buffer) + else: + break + + output_descriptor.close() + input_descriptor.close() + return filepath class DocumentTypeFilename(models.Model): - """ + ''' List of filenames available to a specific document type for the quick rename functionality - """ + ''' document_type = models.ForeignKey(DocumentType, verbose_name=_(u'document type')) filename = models.CharField(max_length=128, verbose_name=_(u'filename'), db_index=True) enabled = models.BooleanField(default=True, verbose_name=_(u'enabled')) @@ -453,12 +520,9 @@ class DocumentTypeFilename(models.Model): class DocumentPage(models.Model): - """ - Model that describes a document page including it's content - """ - ## This field is to be removed - #document = models.ForeignKey(Document, verbose_name=_(u'document')) - + ''' + Model that describes a document version page including it's content + ''' # New parent field document_version = models.ForeignKey(DocumentVersion, verbose_name=_(u'document version'))#, null=True, blank=True) # TODO: Remove these after datamigration diff --git a/apps/documents/urls.py b/apps/documents/urls.py index 88650155a7..239ec62df4 100644 --- a/apps/documents/urls.py +++ b/apps/documents/urls.py @@ -35,7 +35,7 @@ urlpatterns = patterns('documents.views', url(r'^(?P\d+)/clear_transformations/$', 'document_clear_transformations', (), 'document_clear_transformations'), url(r'^(?P\d+)/version/all/$', 'document_version_list', (), 'document_version_list'), - url(r'^document/version/(?P\d+)/download/$', 'document_version_download', (), 'document_version_download'), + url(r'^document/version/(?P\d+)/download/$', 'document_download', (), 'document_version_download'), url(r'^multiple/clear_transformations/$', 'document_multiple_clear_transformations', (), 'document_multiple_clear_transformations'), url(r'^duplicates/list/$', 'document_find_all_duplicates', (), 'document_find_all_duplicates'), diff --git a/apps/documents/views.py b/apps/documents/views.py index efaaf3e2da..bb74f6c62a 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -45,13 +45,13 @@ from documents.literals import PERMISSION_DOCUMENT_CREATE, \ from documents.literals import HISTORY_DOCUMENT_CREATED, \ HISTORY_DOCUMENT_EDITED, HISTORY_DOCUMENT_DELETED -from documents.forms import DocumentTypeSelectForm, \ - DocumentForm_edit, DocumentPropertiesForm, \ - DocumentPreviewForm, \ - DocumentPageForm, DocumentPageTransformationForm, \ - DocumentContentForm, DocumentPageForm_edit, \ - DocumentPageForm_text, PrintForm, DocumentTypeForm, \ - DocumentTypeFilenameForm, DocumentTypeFilenameForm_create +from documents.forms import (DocumentTypeSelectForm, + DocumentForm_edit, DocumentPropertiesForm, + DocumentPreviewForm, DocumentPageForm, + DocumentPageTransformationForm, DocumentContentForm, + DocumentPageForm_edit, DocumentPageForm_text, PrintForm, + DocumentTypeForm, DocumentTypeFilenameForm, + DocumentTypeFilenameForm_create) from documents.wizards import DocumentCreateWizard from documents.models import (Document, DocumentType, DocumentPage, DocumentPageTransformation, RecentDocument, DocumentTypeFilename, @@ -242,12 +242,12 @@ def document_edit(request, document_id): for warning in warnings: messages.warning(request, warning) - document.file_filename = form.cleaned_data['new_filename'] + document.filename = form.cleaned_data['new_filename'] document.description = form.cleaned_data['description'] if 'document_type_available_filenames' in form.cleaned_data: if form.cleaned_data['document_type_available_filenames']: - document.file_filename = form.cleaned_data['document_type_available_filenames'].filename + document.filename = form.cleaned_data['document_type_available_filenames'].filename document.save() create_history(HISTORY_DOCUMENT_EDITED, document, {'user': request.user, 'diff': return_diff(old_document, document, ['file_filename', 'description'])}) @@ -291,27 +291,27 @@ def get_document_image(request, document_id, size=PREVIEW_SIZE, base64_version=F if base64_version: return HttpResponse(u'' % document.get_image(size=size, page=page, zoom=zoom, rotation=rotation, as_base64=True)) else: - # TODO: hardcoded MIMETYPE + # TODO: fix hardcoded MIMETYPE return sendfile.sendfile(request, document.get_image(size=size, page=page, zoom=zoom, rotation=rotation), mimetype=DEFAULT_FILE_FORMAT_MIMETYPE) -def document_version_download(request, document_version_pk): - document_version = get_object_or_404(DocumentVersion, pk=document_version_pk) - return document_download(request, document_version.document.pk) - - -def document_download(request, document_id): +def document_download(request, document_id=None, document_version_pk=None): check_permissions(request.user, [PERMISSION_DOCUMENT_DOWNLOAD]) - document = get_object_or_404(Document, pk=document_id) + if document_version_pk: + document_version = get_object_or_404(DocumentVersion, pk=document_version_pk) + else: + document_version = get_object_or_404(Document, pk=document_id).latest_version + try: - #Test permissions and trigger exception - document.open() + # Test permissions and trigger exception + fd = document_version.open() + fd.close() return serve_file( request, - document.file, - save_as=u'"%s"' % document.get_fullname(), - content_type=document.file_mimetype if document.file_mimetype else 'application/octet-stream' + document_version.file, + save_as=u'"%s"' % document_version.filename, + content_type=document_version.mimetype if document_version.mimetype else 'application/octet-stream' ) except Exception, e: messages.error(request, e) @@ -1154,14 +1154,14 @@ def document_version_list(request, document_pk): document = get_object_or_404(Document, pk=document_pk) context = { - 'object_list': document.versions.all(), + 'object_list': document.versions.order_by('-timestamp'), 'title': _(u'versions for document: %s') % document, 'hide_object': True, 'object': document, 'extra_columns': [ { 'name': _(u'version'), - 'attribute': 'get_version', + 'attribute': 'get_formated_version', }, { 'name': _(u'time and date'), From 28208c4a2abc2dce72e95e1b1deb6b421522b003 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 19:35:36 -0400 Subject: [PATCH 37/92] Update sources app to support multiple versions --- apps/sources/__init__.py | 7 ++ apps/sources/forms.py | 8 +- apps/sources/models.py | 48 +++++---- apps/sources/urls.py | 7 +- apps/sources/views.py | 212 +++++++++++++++++++++++++-------------- 5 files changed, 183 insertions(+), 99 deletions(-) diff --git a/apps/sources/__init__.py b/apps/sources/__init__.py index ad34940468..3257c12c1e 100644 --- a/apps/sources/__init__.py +++ b/apps/sources/__init__.py @@ -5,6 +5,8 @@ from navigation.api import register_links, \ from permissions.api import register_permission, set_namespace_title from common.utils import encapsulate from project_setup.api import register_setup +from documents.models import Document +from documents import PERMISSION_DOCUMENT_CREATE from sources.staging import StagingFile from sources.models import WebForm, StagingFolder, SourceTransformation, \ @@ -41,6 +43,8 @@ setup_source_transformation_delete = {'text': _(u'delete'), 'view': 'setup_sourc source_list = {'text': _(u'Document sources'), 'view': 'setup_web_form_list', 'famfam': 'page_add', 'children_url_regex': [r'sources/setup']} +upload_interactive_version = {'text': _(u'upload new version'), 'view': 'upload_interactive_version', 'args': 'object.pk', 'famfam': 'page_add', 'permissions': [PERMISSION_DOCUMENT_CREATE]} + register_links(StagingFile, [staging_file_delete]) register_links(SourceTransformation, [setup_source_transformation_edit, setup_source_transformation_delete]) @@ -61,6 +65,9 @@ register_links(StagingFolder, [setup_source_transformation_list, setup_source_ed register_links(WatchFolder, [setup_web_form_list, setup_staging_folder_list, setup_watch_folder_list], menu_name='form_header') register_links(WatchFolder, [setup_source_transformation_list, setup_source_edit, setup_source_delete]) +# Document version +register_links(Document, [upload_interactive_version]) + register_links(['setup_source_transformation_create', 'setup_source_transformation_edit', 'setup_source_transformation_delete', 'setup_source_transformation_list'], [setup_source_transformation_create], menu_name='sidebar') source_views = ['setup_web_form_list', 'setup_staging_folder_list', 'setup_watch_folder_list', 'setup_source_edit', 'setup_source_delete', 'setup_source_create', 'setup_source_transformation_list', 'setup_source_transformation_edit', 'setup_source_transformation_delete', 'setup_source_transformation_create'] diff --git a/apps/sources/forms.py b/apps/sources/forms.py index de9b7ee1ce..4b046994cf 100644 --- a/apps/sources/forms.py +++ b/apps/sources/forms.py @@ -37,7 +37,7 @@ class StagingDocumentForm(DocumentForm): staging_list_index = self.fields.keyOrder.index('staging_file_id') staging_list = self.fields.keyOrder.pop(staging_list_index) self.fields.keyOrder.insert(0, staging_list) - + staging_file_id = forms.ChoiceField(label=_(u'Staging file')) class Meta(DocumentForm.Meta): @@ -45,6 +45,8 @@ class StagingDocumentForm(DocumentForm): class WebFormForm(DocumentForm): + file = forms.FileField(label=_(u'File')) + def __init__(self, *args, **kwargs): show_expand = kwargs.pop('show_expand', False) self.source = kwargs.pop('source') @@ -56,6 +58,10 @@ class WebFormForm(DocumentForm): help_text=ugettext(u'Upload a compressed file\'s contained files as individual documents') ) + # Move the file filed to the top + self.fields.keyOrder.remove('file') + self.fields.keyOrder.insert(0, 'file') + def clean_file(self): data = self.cleaned_data['file'] validate_whitelist_blacklist(data.name, self.source.whitelist.split(','), self.source.blacklist.split(',')) diff --git a/apps/sources/models.py b/apps/sources/models.py index c97f2f4ca4..44822ee0ba 100644 --- a/apps/sources/models.py +++ b/apps/sources/models.py @@ -52,7 +52,7 @@ class BaseModel(models.Model): def get_transformation_list(self): return SourceTransformation.transformations.get_for_object_as_list(self) - def upload_file(self, file_object, filename=None, document_type=None, expand=False, metadata_dict_list=None, user=None): + def upload_file(self, file_object, filename=None, document_type=None, expand=False, metadata_dict_list=None, user=None, document=None, new_version_data=None): if expand: try: cf = CompressedFile(file_object) @@ -63,31 +63,39 @@ class BaseModel(models.Model): except NotACompressedFile: self.upload_single_file(file_object, filename, document_type, metadata_dict_list, user) else: - self.upload_single_file(file_object, filename, document_type, metadata_dict_list, user) + self.upload_single_file(file_object, filename, document_type, metadata_dict_list, user, document, new_version_data) file_object.close() - def upload_single_file(self, file_object, filename=None, document_type=None, metadata_dict_list=None, user=None): - transformations, errors = self.get_transformation_list() - document = Document(file=file_object) - if document_type: - document.document_type = document_type - document.save() + def upload_single_file(self, file_object, filename=None, document_type=None, metadata_dict_list=None, user=None, document=None, new_version_data=None): + if not document: + document = Document() + if document_type: + document.document_type = document_type + document.save() + + if metadata_dict_list: + save_metadata_list(metadata_dict_list, document, create=True) + warnings = update_indexes(document) + + if user: + document.add_as_recent_document_for_user(user) + create_history(HISTORY_DOCUMENT_CREATED, document, {'user': user}) + else: + create_history(HISTORY_DOCUMENT_CREATED, document) + + if not new_version_data: + new_version_data = {} + + new_version = document.new_version(file=file_object, **new_version_data) if filename: - document.file_filename = filename - document.save() + new_version.filename = filename + new_version.save() - document.apply_default_transformations(transformations) + transformations, errors = self.get_transformation_list() - if metadata_dict_list: - save_metadata_list(metadata_dict_list, document, create=True) - warnings = update_indexes(document) - - if user: - document.add_as_recent_document_for_user(user) - create_history(HISTORY_DOCUMENT_CREATED, document, {'user': user}) - else: - create_history(HISTORY_DOCUMENT_CREATED, document) + new_version.apply_default_transformations(transformations) + #TODO: new HISTORY for version updates class Meta: ordering = ('title',) diff --git a/apps/sources/urls.py b/apps/sources/urls.py index b75573382d..cd42328138 100644 --- a/apps/sources/urls.py +++ b/apps/sources/urls.py @@ -8,8 +8,11 @@ urlpatterns = patterns('sources.views', url(r'^staging_file/type/(?P\w+)/(?P\d+)/(?P\w+)/delete/$', 'staging_file_delete', (), 'staging_file_delete'), url(r'^staging_file/type/staging_folder/(?P\d+)/(?P\w+)/thumbnail/$', 'staging_file_thumbnail', (), 'staging_file_thumbnail'), - url(r'^upload/interactive/(?P\w+)/(?P\d+)/$', 'upload_interactive', (), 'upload_interactive'), - url(r'^upload/interactive/$', 'upload_interactive', (), 'upload_interactive'), + url(r'^upload/document/new/interactive/(?P\w+)/(?P\d+)/$', 'upload_interactive', (), 'upload_interactive'), + url(r'^upload/document/new/interactive/$', 'upload_interactive', (), 'upload_interactive'), + + url(r'^upload/document/(?P\d+)/version/interactive/(?P\w+)/(?P\d+)/$', 'upload_interactive', (), 'upload_interactive_version'), + url(r'^upload/document/(?P\d+)/version/interactive/$', 'upload_interactive', (), 'upload_interactive_version'), #Setup views diff --git a/apps/sources/views.py b/apps/sources/views.py index c2cde5e7d8..c668b23909 100644 --- a/apps/sources/views.py +++ b/apps/sources/views.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext from django.utils.safestring import mark_safe from documents.literals import PERMISSION_DOCUMENT_CREATE -from documents.models import DocumentType +from documents.models import DocumentType, Document from documents.conf.settings import THUMBNAIL_SIZE from metadata.api import decode_metadata_from_url, metadata_repr_as_list from permissions.api import check_permissions @@ -35,30 +35,34 @@ def return_function(obj): return lambda context: context['source'].source_type == obj.source_type and context['source'].pk == obj.pk -def get_active_tab_links(): - tab_links = [] +def get_tab_link_for_source(source, document=None): + if document: + view = u'upload_interactive_version' + args = [document.pk, u'"%s"' % source.source_type, source.pk] + else: + view = u'upload_interactive' + args = [u'"%s"' % source.source_type, source.pk] + + return { + 'text': source.title, + 'view': view, + 'args': args, + 'famfam': source.icon, + 'keep_query': True, + 'conditional_highlight': return_function(source), + } + +def get_active_tab_links(document=None): + tab_links = [] + web_forms = WebForm.objects.filter(enabled=True) for web_form in web_forms: - tab_links.append({ - 'text': web_form.title, - 'view': 'upload_interactive', - 'args': [u'"%s"' % web_form.source_type, web_form.pk], - 'famfam': web_form.icon, - 'keep_query': True, - 'conditional_highlight': return_function(web_form), - }) + tab_links.append(get_tab_link_for_source(web_form, document)) staging_folders = StagingFolder.objects.filter(enabled=True) for staging_folder in staging_folders: - tab_links.append({ - 'text': staging_folder.title, - 'view': 'upload_interactive', - 'args': [u'"%s"' % staging_folder.source_type, staging_folder.pk], - 'famfam': staging_folder.icon, - 'keep_query': True, - 'conditional_highlight': return_function(staging_folder), - }) + tab_links.append(get_tab_link_for_source(staging_folder, document)) return { 'tab_links': tab_links, @@ -66,15 +70,19 @@ def get_active_tab_links(): SOURCE_CHOICE_STAGING: staging_folders } - -def upload_interactive(request, source_type=None, source_id=None): +def upload_interactive(request, source_type=None, source_id=None, document_pk=None): check_permissions(request.user, [PERMISSION_DOCUMENT_CREATE]) subtemplates_list = [] - context = {} + if document_pk: + document = get_object_or_404(Document, pk=document_pk) + results = get_active_tab_links(document) + else: + document = None + results = get_active_tab_links() - results = get_active_tab_links() + context = {} if results[SOURCE_CHOICE_WEB_FORM].count() == 0 and results[SOURCE_CHOICE_STAGING].count() == 0: source_setup_link = mark_safe('%s' % (reverse('setup_web_form_list'), ugettext(u'here'))) @@ -113,43 +121,57 @@ def upload_interactive(request, source_type=None, source_id=None): if request.method == 'POST': form = WebFormForm(request.POST, request.FILES, document_type=document_type, - show_expand=(web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK), - source=web_form + show_expand=(web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK) and not document, + source=web_form, + instance=document ) if form.is_valid(): - try: + #try: + if document: + expand = False + else: if web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK: - expand = form.cleaned_data['expand'] + expand = form.cleaned_data.get('expand') else: if web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y: expand = True else: expand = False - - new_filename = get_form_filename(form) - web_form.upload_file(request.FILES['file'], - new_filename, document_type=document_type, - expand=expand, - metadata_dict_list=decode_metadata_from_url(request.GET), - user=request.user - ) + + new_filename = get_form_filename(form) + web_form.upload_file(request.FILES['file'], + new_filename, document_type=document_type, + expand=expand, + metadata_dict_list=decode_metadata_from_url(request.GET), + user=request.user, + document=document, + new_version_data=form.cleaned_data.get('new_version_data') + ) + #except Exception, e: + # messages.error(request, _(u'Unhandled exception: %s') % e) + if document: + messages.success(request, _(u'Document version uploaded successfully.')) + return HttpResponseRedirect(reverse('document_view_simple', args=[document.pk])) + else: messages.success(request, _(u'Document uploaded successfully.')) - except Exception, e: - messages.error(request, e) - - return HttpResponseRedirect(request.get_full_path()) + return HttpResponseRedirect(request.get_full_path()) else: form = WebFormForm( - show_expand=(web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK), + show_expand=(web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK) and not document, document_type=document_type, - source=web_form + source=web_form, + instance=document ) - + if document: + title = _(u'upload a new version from source: %s') % web_form.title + else: + title = _(u'upload a local document from source: %s') % web_form.title + subtemplates_list.append({ 'name': 'generic_form_subtemplate.html', 'context': { 'form': form, - 'title': _(u'upload a local document from source: %s') % web_form.title, + 'title': title, }, }) elif source_type == SOURCE_CHOICE_STAGING: @@ -159,41 +181,54 @@ def upload_interactive(request, source_type=None, source_id=None): if request.method == 'POST': form = StagingDocumentForm(request.POST, request.FILES, cls=StagingFile, document_type=document_type, - show_expand=(staging_folder.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK), - source=staging_folder + show_expand=(staging_folder.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK) and not document, + source=staging_folder, + instance=document ) if form.is_valid(): - try: - staging_file = StagingFile.get(form.cleaned_data['staging_file_id']) + #try: + staging_file = StagingFile.get(form.cleaned_data['staging_file_id']) + if document: + expand = False + else: if staging_folder.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK: - expand = form.cleaned_data['expand'] + expand = form.cleaned_dataget('expand') else: if staging_folder.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y: expand = True else: expand = False - new_filename = get_form_filename(form) - staging_folder.upload_file(staging_file.upload(), - new_filename, document_type=document_type, - expand=expand, - metadata_dict_list=decode_metadata_from_url(request.GET), - user=request.user - ) + + new_filename = get_form_filename(form) + staging_folder.upload_file(staging_file.upload(), + new_filename, document_type=document_type, + expand=expand, + metadata_dict_list=decode_metadata_from_url(request.GET), + user=request.user, + document=document, + new_version_data=form.cleaned_data.get('new_version_data') + ) + if document: + messages.success(request, _(u'Document version from staging file: %s, uploaded successfully.') % staging_file.filename) + else: messages.success(request, _(u'Staging file: %s, uploaded successfully.') % staging_file.filename) - if staging_folder.delete_after_upload: - transformations, errors = staging_folder.get_transformation_list() - staging_file.delete(preview_size=staging_folder.get_preview_size(), transformations=transformations) - messages.success(request, _(u'Staging file: %s, deleted successfully.') % staging_file.filename) - except Exception, e: - messages.error(request, e) - - return HttpResponseRedirect(request.get_full_path()) + if staging_folder.delete_after_upload: + transformations, errors = staging_folder.get_transformation_list() + staging_file.delete(preview_size=staging_folder.get_preview_size(), transformations=transformations) + messages.success(request, _(u'Staging file: %s, deleted successfully.') % staging_file.filename) + #except Exception, e: + # messages.error(request, _(u'Unhandled exception: %s') % e) + if document: + return HttpResponseRedirect(reverse('document_view_simple', args=[document.pk])) + else: + return HttpResponseRedirect(request.get_full_path()) else: form = StagingDocumentForm(cls=StagingFile, document_type=document_type, - show_expand=(staging_folder.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK), - source=staging_folder + show_expand=(staging_folder.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK) and not document, + source=staging_folder, + instance=document ) try: staging_filelist = StagingFile.get_all() @@ -201,12 +236,17 @@ def upload_interactive(request, source_type=None, source_id=None): messages.error(request, e) staging_filelist = [] finally: + if document: + title = _(u'upload a new version from staging source: %s') % staging_folder.title + else: + title = _(u'upload a document from staging source: %s') % staging_folder.title + subtemplates_list = [ { 'name': 'generic_form_subtemplate.html', 'context': { 'form': form, - 'title': _(u'upload a document from staging source: %s') % staging_folder.title, + 'title': title, } }, { @@ -219,20 +259,40 @@ def upload_interactive(request, source_type=None, source_id=None): }, ] + if document: + context['object'] = document + context.update({ 'document_type_id': document_type_id, 'subtemplates_list': subtemplates_list, - 'sidebar_subtemplates_list': [ - { - 'name': 'generic_subtemplate.html', - 'context': { - 'title': _(u'Current metadata'), - 'paragraphs': metadata_repr_as_list(decode_metadata_from_url(request.GET)), - 'side_bar': True, - } - }], - 'temporary_navigation_links': {'form_header': {'upload_interactive': {'links': results['tab_links']}}}, + 'temporary_navigation_links': { + 'form_header': { + 'upload_interactive_version': { + 'links': results['tab_links'] + }, + 'upload_interactive': { + 'links': results['tab_links'] + } + } + }, }) + + if not document: + context.update( + { + 'sidebar_subtemplates_list': [ + { + 'name': 'generic_subtemplate.html', + 'context': { + 'title': _(u'Current metadata'), + 'paragraphs': metadata_repr_as_list(decode_metadata_from_url(request.GET)), + 'side_bar': True, + } + } + ], + } + ) + return render_to_response('generic_form.html', context, context_instance=RequestContext(request)) From a3befc6b7d7ce793e4d8ec75293e3c2cb8fed50a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 20:09:44 -0400 Subject: [PATCH 38/92] Remove remarked code --- apps/documents/models.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index 04c633556f..a9c0b8f917 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -200,12 +200,6 @@ class Document(models.Model): new_version = DocumentVersion( document=self, file=file, - #major = new_version_dict.get('major'), - #minor = new_version_dict.get('minor'), - #micro = new_version_dict.get('micro'), - #release_level = release_level, - #serial = serial, - #comment = comment, ) new_version.save() From d81e734f592afea647a1bf4ebdf6890599feb41e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 20:10:08 -0400 Subject: [PATCH 39/92] Update REST API to new document multi version methods and structures --- apps/rest_api/resources.py | 39 ++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/rest_api/resources.py b/apps/rest_api/resources.py index 99a5c26613..869c78e407 100644 --- a/apps/rest_api/resources.py +++ b/apps/rest_api/resources.py @@ -8,15 +8,46 @@ from converter.exceptions import UnknownFileFormat, UnkownConvertError class DocumentResourceSimple(ModelResource): model = Document - fields = ('url', 'pk', 'document_type', 'uuid', 'date_added', 'description', 'tags', 'comments', 'expensive_methods', 'files') + fields = ('url', 'pk', 'document_type', 'uuid', 'date_added', 'description', 'tags', 'comments', 'expensive_methods', 'versions') - def files(self, instance): + def versions(self, instance): + return [ + { + 'version': version.get_formated_version(), + 'major': version.major, + 'minor': version.minor, + 'micro': version.micro, + 'release_level': version.release_level, + 'serial': version.serial, + 'timestamp': version.timestamp, + 'comment': version.comment, + 'mimetype': version.mimetype, + 'encoding': version.encoding, + 'filename': version.filename, + 'checksum': version.checksum, + 'download': reverse('document_version_download', args=[version.pk]), + 'stored_filename': version.file.name, + # TODO: Add transformations + 'pages': [ + { + 'content': page.content, + 'page_label': page.page_label, + 'page_number': page.page_number, + } + for page in version.pages.all() + ] + + } + for version in instance.versions.all() + ] + + ''' return [ { 'version': 1, 'mimetype': instance.file_mimetype, 'encoding': instance.file_mime_encoding, - 'filename': instance.get_fullname(), + 'filename': instance.filename, 'date_updated': instance.date_updated, 'checksum': instance.checksum, 'size': instance.size, @@ -33,6 +64,6 @@ class DocumentResourceSimple(ModelResource): ] } ] - + ''' def expensive_methods(self, instance): return [] From 0cf8d68db0f0e8c084f7852f8352e741a7326f14 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 20:30:44 -0400 Subject: [PATCH 40/92] Add the page number on top of the page widget in the multiple page carousel widget --- apps/documents/forms.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/documents/forms.py b/apps/documents/forms.py index 1033692bfe..e680121bb2 100644 --- a/apps/documents/forms.py +++ b/apps/documents/forms.py @@ -103,7 +103,9 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget): output.append(u'
') for page in value.pages.all(): + output.append(u'
') + output.append(u'
%(page_string)s %(page)s
' % {'page_string': ugettext(u'Page'), 'page': page.page_number}) output.append( document_html_widget( page.document, @@ -178,7 +180,6 @@ class DocumentForm(forms.ModelForm): self.fields['version_update'] = forms.ChoiceField( label=_(u'Version update'), - #widget=forms.widgets.RadioSelect(), choices=DocumentVersion.get_version_update_choices(document.latest_version) ) @@ -186,7 +187,6 @@ class DocumentForm(forms.ModelForm): label=_(u'Release level'), choices=RELEASE_LEVEL_CHOICES, initial=RELEASE_LEVEL_FINAL, - #required=False, ) self.fields['serial'] = forms.IntegerField( @@ -195,7 +195,6 @@ class DocumentForm(forms.ModelForm): widget=forms.widgets.TextInput( attrs = {'style': 'width: auto;'} ), - #required=False ) new_filename = forms.CharField( From 4bbf5d3514aa63287759d88d44766281b0c5e97b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 20:43:30 -0400 Subject: [PATCH 41/92] Add 'siblings' convenience method to the DocumentPage model --- apps/documents/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/documents/models.py b/apps/documents/models.py index a9c0b8f917..c5104dee7c 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -543,6 +543,10 @@ class DocumentPage(models.Model): @models.permalink def get_absolute_url(self): return ('document_page_view', [self.pk]) + + @property + def siblings(self): + return DocumentPage.objects.filter(document_version=self.document_version) # Compatibility methods @property From 699f7e7caa7515e8d505fc4a8199b66f3ab41faf Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 20:44:04 -0400 Subject: [PATCH 42/92] Update document page navigation views to use the new 'siblings' DocumentPage method --- apps/documents/views.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/documents/views.py b/apps/documents/views.py index bb74f6c62a..894818547c 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -531,7 +531,7 @@ def document_clear_transformations(request, document_id=None, document_id_list=N if request.method == 'POST': for document in documents: try: - for document_page in document.documentpage_set.all(): + for document_page in document.pages.all(): document_page.document.invalidate_cached_image(document_page.page_number) for transformation in document_page.documentpagetransformation_set.all(): transformation.delete() @@ -667,11 +667,11 @@ def document_page_navigation_next(request, document_page_id): view = resolve_to_name(urlparse.urlparse(request.META.get('HTTP_REFERER', u'/')).path) document_page = get_object_or_404(DocumentPage, pk=document_page_id) - if document_page.page_number >= document_page.document.documentpage_set.count(): + if document_page.page_number >= document_page.siblings.count(): messages.warning(request, _(u'There are no more pages in this document')) return HttpResponseRedirect(request.META.get('HTTP_REFERER', u'/')) else: - document_page = get_object_or_404(DocumentPage, document=document_page.document, page_number=document_page.page_number + 1) + document_page = get_object_or_404(document_page.siblings, page_number=document_page.page_number + 1) return HttpResponseRedirect(reverse(view, args=[document_page.pk])) @@ -684,7 +684,7 @@ def document_page_navigation_previous(request, document_page_id): messages.warning(request, _(u'You are already at the first page of this document')) return HttpResponseRedirect(request.META.get('HTTP_REFERER', u'/')) else: - document_page = get_object_or_404(DocumentPage, document=document_page.document, page_number=document_page.page_number - 1) + document_page = get_object_or_404(document_page.siblings, page_number=document_page.page_number - 1) return HttpResponseRedirect(reverse(view, args=[document_page.pk])) @@ -693,7 +693,7 @@ def document_page_navigation_first(request, document_page_id): view = resolve_to_name(urlparse.urlparse(request.META.get('HTTP_REFERER', u'/')).path) document_page = get_object_or_404(DocumentPage, pk=document_page_id) - document_page = get_object_or_404(DocumentPage, document=document_page.document, page_number=1) + document_page = get_object_or_404(document_page.siblings, page_number=1) return HttpResponseRedirect(reverse(view, args=[document_page.pk])) @@ -702,7 +702,7 @@ def document_page_navigation_last(request, document_page_id): view = resolve_to_name(urlparse.urlparse(request.META.get('HTTP_REFERER', u'/')).path) document_page = get_object_or_404(DocumentPage, pk=document_page_id) - document_page = get_object_or_404(DocumentPage, document=document_page.document, page_number=document_page.document.documentpage_set.count()) + document_page = get_object_or_404(document_page.siblings, page_number=document_page.siblings.count()) return HttpResponseRedirect(reverse(view, args=[document_page.pk])) @@ -856,9 +856,9 @@ def document_hard_copy(request, document_id): if page_range: page_range = parse_range(page_range) - pages = document.documentpage_set.filter(page_number__in=page_range) + pages = document.pages.filter(page_number__in=page_range) else: - pages = document.documentpage_set.all() + pages = document.pages.all() return render_to_response('document_print.html', { 'object': document, From 90863025d8b2e793ff418c8403ee8b2b0d677daa Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 20:54:35 -0400 Subject: [PATCH 43/92] Fix permission import --- apps/sources/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sources/__init__.py b/apps/sources/__init__.py index 3257c12c1e..94d2727c9c 100644 --- a/apps/sources/__init__.py +++ b/apps/sources/__init__.py @@ -6,7 +6,7 @@ from permissions.api import register_permission, set_namespace_title from common.utils import encapsulate from project_setup.api import register_setup from documents.models import Document -from documents import PERMISSION_DOCUMENT_CREATE +from documents.literals import PERMISSION_DOCUMENT_CREATE from sources.staging import StagingFile from sources.models import WebForm, StagingFolder, SourceTransformation, \ From e81a02b18d7896f04fc95adce8b5c55e58f38c30 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 23:29:52 -0400 Subject: [PATCH 44/92] Improve document multi version migrations --- ...06_fix_invalid_document_version_id_keys.py | 159 ++++++++++++++++++ ...elds.py => 0007_remove_old_file_fields.py} | 3 +- ...d_name.py => 0008_fix_mayor_field_name.py} | 0 ...ent_field.py => 0009_add_comment_field.py} | 0 4 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 apps/documents/migrations/0006_fix_invalid_document_version_id_keys.py rename apps/documents/migrations/{0006_remove_old_file_fields.py => 0007_remove_old_file_fields.py} (98%) rename apps/documents/migrations/{0007_fix_mayor_field_name.py => 0008_fix_mayor_field_name.py} (100%) rename apps/documents/migrations/{0008_add_comment_field.py => 0009_add_comment_field.py} (100%) diff --git a/apps/documents/migrations/0006_fix_invalid_document_version_id_keys.py b/apps/documents/migrations/0006_fix_invalid_document_version_id_keys.py new file mode 100644 index 0000000000..03f4ba4133 --- /dev/null +++ b/apps/documents/migrations/0006_fix_invalid_document_version_id_keys.py @@ -0,0 +1,159 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + qs = orm.DocumentPage.objects.filter(document_version=None) + #print 'Invalid document pages to delete: %s' % qs.count() + for document_page in qs: + document_page.delete() + + def backwards(self, orm): + def backwards(self, orm): + raise RuntimeError('Cannot reverse this migration.') + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'file_filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'file_mime_encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file_mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "u'6c189f1f-1d85-48b5-9b7d-e8e319603e77'", 'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentVersion']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.documentversion': { + 'Meta': {'unique_together': "(('document', 'mayor', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']", 'null': 'True', 'blank': 'True'}), + 'encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mayor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] diff --git a/apps/documents/migrations/0006_remove_old_file_fields.py b/apps/documents/migrations/0007_remove_old_file_fields.py similarity index 98% rename from apps/documents/migrations/0006_remove_old_file_fields.py rename to apps/documents/migrations/0007_remove_old_file_fields.py index 7abf68d028..02c074fcfd 100644 --- a/apps/documents/migrations/0006_remove_old_file_fields.py +++ b/apps/documents/migrations/0007_remove_old_file_fields.py @@ -9,7 +9,7 @@ class Migration(SchemaMigration): def forwards(self, orm): # Changing field 'DocumentVersion.document' - db.alter_column('documents_documentversion', 'document_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['documents.Document'])) + db.alter_column('documents_documentversion', 'document_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.Document'])) # Deleting field 'Document.date_updated' db.delete_column('documents_document', 'date_updated') @@ -33,7 +33,6 @@ class Migration(SchemaMigration): db.delete_column('documents_documentpage', 'document_id') # Changing field 'DocumentPage.document_version' - #db.alter_column('documents_documentpage', 'document_version_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['documents.DocumentVersion'])) db.alter_column('documents_documentpage', 'document_version_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.DocumentVersion'])) diff --git a/apps/documents/migrations/0007_fix_mayor_field_name.py b/apps/documents/migrations/0008_fix_mayor_field_name.py similarity index 100% rename from apps/documents/migrations/0007_fix_mayor_field_name.py rename to apps/documents/migrations/0008_fix_mayor_field_name.py diff --git a/apps/documents/migrations/0008_add_comment_field.py b/apps/documents/migrations/0009_add_comment_field.py similarity index 100% rename from apps/documents/migrations/0008_add_comment_field.py rename to apps/documents/migrations/0009_add_comment_field.py From 986b7e39e4b523fc8d9a6fd2f99290e19d5153ac Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 23:37:00 -0400 Subject: [PATCH 45/92] Fix the new version fields in the upload form --- apps/documents/forms.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/documents/forms.py b/apps/documents/forms.py index e680121bb2..60d69506ac 100644 --- a/apps/documents/forms.py +++ b/apps/documents/forms.py @@ -151,8 +151,6 @@ class DocumentForm(forms.ModelForm): instance = kwargs.pop('instance', None) super(DocumentForm, self).__init__(*args, **kwargs) - if instance: - self.version_fields(instance) if 'document_type' in self.fields: # To allow merging with DocumentForm_edit @@ -170,14 +168,11 @@ class DocumentForm(forms.ModelForm): queryset=filenames_qs, required=False, label=_(u'Quick document rename')) + + if instance: + self.version_fields(instance) def version_fields(self, document): - self.fields['comment'] = forms.CharField( - label=_(u'Comment'), - required=False, - widget=forms.widgets.Textarea(attrs={'rows': 4}), - ) - self.fields['version_update'] = forms.ChoiceField( label=_(u'Version update'), choices=DocumentVersion.get_version_update_choices(document.latest_version) @@ -197,6 +192,12 @@ class DocumentForm(forms.ModelForm): ), ) + self.fields['comment'] = forms.CharField( + label=_(u'Comment'), + required=False, + widget=forms.widgets.Textarea(attrs={'rows': 4}), + ) + new_filename = forms.CharField( label=_('New document filename'), required=False ) From 43b17456d1cb4fbe14fab226cddc542dd3e0ed50 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 3 Dec 2011 23:42:29 -0400 Subject: [PATCH 46/92] Remove the auto_add_now property from the date_added document field --- ...010_auto__chg_field_document_date_added.py | 155 ++++++++++++++++++ apps/documents/models.py | 9 +- 2 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 apps/documents/migrations/0010_auto__chg_field_document_date_added.py diff --git a/apps/documents/migrations/0010_auto__chg_field_document_date_added.py b/apps/documents/migrations/0010_auto__chg_field_document_date_added.py new file mode 100644 index 0000000000..6220ef13ad --- /dev/null +++ b/apps/documents/migrations/0010_auto__chg_field_document_date_added.py @@ -0,0 +1,155 @@ +# 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): + + # Changing field 'Document.date_added' + db.alter_column('documents_document', 'date_added', self.gf('django.db.models.fields.DateTimeField')()) + + + def backwards(self, orm): + + # Changing field 'Document.date_added' + db.alter_column('documents_document', 'date_added', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True)) + + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentVersion']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.documentversion': { + 'Meta': {'unique_together': "(('document', 'major', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'major': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] diff --git a/apps/documents/models.py b/apps/documents/models.py index c5104dee7c..581e1eea4f 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -78,8 +78,7 @@ class Document(models.Model): uuid = models.CharField(max_length=48, blank=True, editable=False) document_type = models.ForeignKey(DocumentType, verbose_name=_(u'document type'), null=True, blank=True) description = models.TextField(blank=True, null=True, verbose_name=_(u'description'), db_index=True) - #TODO: remove date_added, it is the timestamp of the first version - date_added = models.DateTimeField(verbose_name=_(u'added'), auto_now_add=True, db_index=True) + date_added = models.DateTimeField(verbose_name=_(u'added'), db_index=True) tags = TaggableManager() @@ -111,6 +110,7 @@ class Document(models.Model): def save(self, *args, **kwargs): if not self.pk: self.uuid = UUID_FUNCTION() + self.date_added = datetime.datetime.now() super(Document, self).save(*args, **kwargs) def get_cached_image_name(self, page): @@ -248,11 +248,6 @@ class Document(models.Model): def date_updated(self): return self.latest_version.timestamp - # TODO: uncomment when date_added is removed - #@property - #def date_added(self): - # return self.first_version.timestamp - @property def checksum(self): return self.latest_version.checksum From e6d3453800e8ff0e9ba6d3c1c98fe9d4f45f47e1 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 00:00:07 -0400 Subject: [PATCH 47/92] Update the linking app to be compatible with the new document versioning fields --- apps/linking/forms.py | 2 +- apps/linking/managers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/linking/forms.py b/apps/linking/forms.py index 9568994b89..1e90e16c8a 100644 --- a/apps/linking/forms.py +++ b/apps/linking/forms.py @@ -45,7 +45,7 @@ class SmartLinkImageWidget(forms.widgets.Widget): for document in value['documents']: output.append(u'
' % (u'border: 5px solid black; padding: 3px;' if value['current_document'] == document else u'')) output.append(u'
%s
' % document) - output.append(u'
%s: %d
' % (ugettext(u'Pages'), document.documentpage_set.count())) + output.append(u'
%s: %d
' % (ugettext(u'Pages'), document.pages.count())) output.append(get_tags_inline_widget(document)) output.append(u'
' % document) output.append(document_html_widget(document, click_view='document_display', view='document_preview_multipage', fancybox_class='fancybox-noscaling', gallery_name=u'smart_link_%d_documents_gallery' % value['smart_link_instance'].pk)) diff --git a/apps/linking/managers.py b/apps/linking/managers.py index 03f9203b24..050ddffea2 100644 --- a/apps/linking/managers.py +++ b/apps/linking/managers.py @@ -59,7 +59,7 @@ class SmartLinkManager(models.Manager): if total_query: try: document_qs = Document.objects.filter(total_query) - result[smart_link] = {'documents': document_qs.order_by('file_filename') or []} + result[smart_link] = {'documents': document_qs.order_by('date_added') or []} except Exception, e: result[smart_link] = {'documents': []} errors.append(e) From aa660851651270c761db802028e7fb6691ac8e7b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 00:01:21 -0400 Subject: [PATCH 48/92] Update the document list view, the page update view and the remaining references of 'file_filename' to 'filename' --- apps/documents/views.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/documents/views.py b/apps/documents/views.py index 894818547c..4b08e9f5ee 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -66,7 +66,7 @@ def document_list(request, object_list=None, title=None, extra_context=None): check_permissions(request.user, [PERMISSION_DOCUMENT_VIEW]) context = { - 'object_list': object_list if not (object_list is None) else Document.objects.only('file_filename',).all(), + 'object_list': object_list if not (object_list is None) else Document.objects.all(), 'title': title if title else _(u'documents'), 'multi_select_as_buttons': True, 'hide_links': True, @@ -115,7 +115,7 @@ def document_view(request, document_id, advanced=False): if advanced: document_properties_form = DocumentPropertiesForm(instance=document, extra_fields=[ - {'label': _(u'Filename'), 'field': 'file_filename'}, + {'label': _(u'Filename'), 'field': 'filename'}, {'label': _(u'File mimetype'), 'field': 'file_mimetype'}, {'label': _(u'File mime encoding'), 'field': 'file_mime_encoding'}, {'label': _(u'File size'), 'field':lambda x: pretty_size(x.size) if x.size else '-'}, @@ -250,7 +250,7 @@ def document_edit(request, document_id): document.filename = form.cleaned_data['document_type_available_filenames'].filename document.save() - create_history(HISTORY_DOCUMENT_EDITED, document, {'user': request.user, 'diff': return_diff(old_document, document, ['file_filename', 'description'])}) + create_history(HISTORY_DOCUMENT_EDITED, document, {'user': request.user, 'diff': return_diff(old_document, document, ['filename', 'description'])}) RecentDocument.objects.add_document_for_user(request.user, document) messages.success(request, _(u'Document "%s" edited successfully.') % document) @@ -263,7 +263,7 @@ def document_edit(request, document_id): return HttpResponseRedirect(document.get_absolute_url()) else: form = DocumentForm_edit(instance=document, initial={ - 'new_filename': document.file_filename}) + 'new_filename': document.filename}) return render_to_response('generic_form.html', { 'form': form, @@ -486,16 +486,16 @@ def document_update_page_count(request): previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) office_converter = OfficeConverter() - qs = Document.objects.exclude(file_filename__iendswith='dxf').filter(file_mimetype__in=office_converter.mimetypes()) + qs = DocumentVersion.objects.exclude(filename__iendswith='dxf').filter(mimetype__in=office_converter.mimetypes()) if request.method == 'POST': updated = 0 processed = 0 - for document in qs: - old_page_count = document.page_count - document.update_page_count() + for document_version in qs: + old_page_count = document_version.pages.count() + document_version.update_page_count() processed += 1 - if old_page_count != document.page_count: + if old_page_count != document_version.pages.count(): updated += 1 messages.success(request, _(u'Page count update complete. Documents processed: %(total)d, documents with changed page count: %(change)d') % { From c3eaacb6c6badfe77414dcf4d96f658aacbd4124 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 01:10:10 -0400 Subject: [PATCH 49/92] Make document date_added not editable, fix document version delete method, add revert method to document version --- apps/documents/models.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index 581e1eea4f..ef9220b277 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -78,7 +78,7 @@ class Document(models.Model): uuid = models.CharField(max_length=48, blank=True, editable=False) document_type = models.ForeignKey(DocumentType, verbose_name=_(u'document type'), null=True, blank=True) description = models.TextField(blank=True, null=True, verbose_name=_(u'description'), db_index=True) - date_added = models.DateTimeField(verbose_name=_(u'added'), db_index=True) + date_added = models.DateTimeField(verbose_name=_(u'added'), db_index=True, editable=False) tags = TaggableManager() @@ -438,6 +438,13 @@ class DocumentVersion(models.Model): page_transformation.save() + def revert(self): + ''' + Delete the subsequent versions after this one + ''' + for version in self.document.versions.filter(timestamp__gt=self.timestamp): + version.delete() + def update_mimetype(self, save=True): ''' Read a document verions's file and determine the mimetype by calling the @@ -454,7 +461,7 @@ class DocumentVersion(models.Model): self.save() def delete(self, *args, **kwargs): - super(Document, self).delete(*args, **kwargs) + super(DocumentVesion, self).delete(*args, **kwargs) return self.file.storage.delete(self.file.path) def exists(self): From 2ec134e1728cfd12af0b1d94e4f4fb3298cf1928 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 01:11:12 -0400 Subject: [PATCH 50/92] Add document version revert support --- apps/documents/__init__.py | 26 ++++++------ apps/documents/literals.py | 1 + .../static/images/icons/page_white_copy.png | Bin 0 -> 862 bytes apps/documents/urls.py | 1 + apps/documents/views.py | 40 ++++++++++++++---- 5 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 apps/documents/static/images/icons/page_white_copy.png diff --git a/apps/documents/__init__.py b/apps/documents/__init__.py index 1a39e01ac8..e6e67f90c8 100644 --- a/apps/documents/__init__.py +++ b/apps/documents/__init__.py @@ -16,15 +16,15 @@ from project_setup.api import register_setup from documents.models import (Document, DocumentPage, DocumentPageTransformation, DocumentType, DocumentTypeFilename, DocumentVersion) -from documents.literals import PERMISSION_DOCUMENT_CREATE, \ - PERMISSION_DOCUMENT_PROPERTIES_EDIT, PERMISSION_DOCUMENT_VIEW, \ - PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_DOWNLOAD, \ - PERMISSION_DOCUMENT_TRANSFORM, PERMISSION_DOCUMENT_TOOLS, \ - PERMISSION_DOCUMENT_EDIT -from documents.literals import PERMISSION_DOCUMENT_TYPE_EDIT, \ - PERMISSION_DOCUMENT_TYPE_DELETE, PERMISSION_DOCUMENT_TYPE_CREATE -from documents.literals import HISTORY_DOCUMENT_CREATED, \ - HISTORY_DOCUMENT_EDITED, HISTORY_DOCUMENT_DELETED +from documents.literals import (PERMISSION_DOCUMENT_CREATE, + PERMISSION_DOCUMENT_PROPERTIES_EDIT, PERMISSION_DOCUMENT_VIEW, + PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_DOWNLOAD, + PERMISSION_DOCUMENT_TRANSFORM, PERMISSION_DOCUMENT_TOOLS, + PERMISSION_DOCUMENT_EDIT, PERMISSION_DOCUMENT_VERSION_REVERT) +from documents.literals import (PERMISSION_DOCUMENT_TYPE_EDIT, + PERMISSION_DOCUMENT_TYPE_DELETE, PERMISSION_DOCUMENT_TYPE_CREATE) +from documents.literals import (HISTORY_DOCUMENT_CREATED, + HISTORY_DOCUMENT_EDITED, HISTORY_DOCUMENT_DELETED) from documents.conf.settings import ZOOM_MAX_LEVEL from documents.conf.settings import ZOOM_MIN_LEVEL from documents.conf import settings as document_settings @@ -56,6 +56,7 @@ register_permission(PERMISSION_DOCUMENT_DELETE) register_permission(PERMISSION_DOCUMENT_DOWNLOAD) register_permission(PERMISSION_DOCUMENT_TRANSFORM) register_permission(PERMISSION_DOCUMENT_TOOLS) +register_permission(PERMISSION_DOCUMENT_VERSION_REVERT) # Document type permissions set_namespace_title('documents_setup', _(u'Documents setup')) @@ -80,8 +81,8 @@ document_edit = {'text': _(u'edit'), 'view': 'document_edit', 'args': 'object.id document_preview = {'text': _(u'preview'), 'class': 'fancybox', 'view': 'document_preview', 'args': 'object.id', 'famfam': 'magnifier', 'permissions': [PERMISSION_DOCUMENT_VIEW]} document_download = {'text': _(u'download'), 'view': 'document_download', 'args': 'object.id', 'famfam': 'page_save', 'permissions': [PERMISSION_DOCUMENT_DOWNLOAD]} document_version_download = {'text': _(u'download'), 'view': 'document_version_download', 'args': 'object.pk', 'famfam': 'page_save', 'permissions': [PERMISSION_DOCUMENT_DOWNLOAD]} -document_find_duplicates = {'text': _(u'find duplicates'), 'view': 'document_find_duplicates', 'args': 'object.id', 'famfam': 'page_refresh', 'permissions': [PERMISSION_DOCUMENT_VIEW]} -document_find_all_duplicates = {'text': _(u'find all duplicates'), 'view': 'document_find_all_duplicates', 'famfam': 'page_refresh', 'permissions': [PERMISSION_DOCUMENT_VIEW], 'description': _(u'Search all the documents\' checksums and return a list of the exact matches.')} +document_find_duplicates = {'text': _(u'find duplicates'), 'view': 'document_find_duplicates', 'args': 'object.id', 'famfam': 'page_white_copy', 'permissions': [PERMISSION_DOCUMENT_VIEW]} +document_find_all_duplicates = {'text': _(u'find all duplicates'), 'view': 'document_find_all_duplicates', 'famfam': 'page_white_copy', 'permissions': [PERMISSION_DOCUMENT_VIEW], 'description': _(u'Search all the documents\' checksums and return a list of the exact matches.')} document_update_page_count = {'text': _(u'update office documents\' page count'), 'view': 'document_update_page_count', 'famfam': 'page_white_csharp', 'permissions': [PERMISSION_DOCUMENT_TOOLS], 'description': _(u'Update the page count of the office type documents. This is useful when enabling office document support after there were already office type documents in the database.')} document_clear_transformations = {'text': _(u'clear transformations'), 'view': 'document_clear_transformations', 'args': 'object.id', 'famfam': 'page_paintbrush', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} document_multiple_clear_transformations = {'text': _(u'clear transformations'), 'view': 'document_multiple_clear_transformations', 'famfam': 'page_paintbrush', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} @@ -113,6 +114,7 @@ document_page_view_reset = {'text': _(u'reset view'), 'class': 'no-parent-histor # Document versions document_version_list = {'text': _(u'versions'), 'view': 'document_version_list', 'args': 'object.pk', 'famfam': 'page_world', 'permissions': [PERMISSION_DOCUMENT_VIEW]} +document_version_revert = {'text': _(u'revert'), 'view': 'document_version_revert', 'args': 'object.pk', 'famfam': 'page_refresh', 'permissions': [PERMISSION_DOCUMENT_VERSION_REVERT]} # Document type related links document_type_list = {'text': _(u'document type list'), 'view': 'document_type_list', 'famfam': 'layout', 'permissions': [PERMISSION_DOCUMENT_VIEW]} @@ -141,7 +143,7 @@ register_links(Document, [document_edit, document_print, document_delete, docume register_multi_item_links(['document_find_duplicates', 'folder_view', 'index_instance_list', 'document_type_document_list', 'search', 'results', 'document_group_view', 'document_list', 'document_list_recent'], [document_multiple_clear_transformations, document_multiple_delete]) # Document Version links -register_links(DocumentVersion, [document_version_download]) +register_links(DocumentVersion, [document_version_revert, document_version_download]) secondary_menu_links = [document_list_recent, document_list, document_create_multiple] diff --git a/apps/documents/literals.py b/apps/documents/literals.py index 30b0284f5f..54df1d03d2 100644 --- a/apps/documents/literals.py +++ b/apps/documents/literals.py @@ -13,6 +13,7 @@ PERMISSION_DOCUMENT_DELETE = {'namespace': 'documents', 'name': 'document_delete PERMISSION_DOCUMENT_DOWNLOAD = {'namespace': 'documents', 'name': 'document_download', 'label': _(u'Download documents')} PERMISSION_DOCUMENT_TRANSFORM = {'namespace': 'documents', 'name': 'document_transform', 'label': _(u'Transform documents')} PERMISSION_DOCUMENT_TOOLS = {'namespace': 'documents', 'name': 'document_tools', 'label': _(u'Execute document modifying tools')} +PERMISSION_DOCUMENT_VERSION_REVERT = {'namespace': 'documents', 'name': 'document_version_revert', 'label': _(u'Revert documents to a previous version')} PERMISSION_DOCUMENT_TYPE_EDIT = {'namespace': 'documents_setup', 'name': 'document_type_edit', 'label': _(u'Edit document types')} PERMISSION_DOCUMENT_TYPE_DELETE = {'namespace': 'documents_setup', 'name': 'document_type_delete', 'label': _(u'Delete document types')} diff --git a/apps/documents/static/images/icons/page_white_copy.png b/apps/documents/static/images/icons/page_white_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..4c21085e2bef9d3c05ad32fa6b21f575ad759894 GIT binary patch literal 862 zcmV-k1EKthP)~inKO?)P`bhjd*Nlm%HQcxWl_WhfEgs zZuh>K`R3aL0l;XTowWh6BF1A75N7fU0z(ATG(YauYBid;g9or>0wCg8uh$cYM@Of$ zmM492J7-L} z;d6UlEue8Q_zk&S4$7s{PNUiE()2p$>vb5ptBBoMNcUIy$EAPWT`L%#WYqukXR zDht`{Dvaqnm7yoBIk#?*^>?e)XEcuH0zk$g?D;kU{jF)wndTkr?d}Gy>-YQFlV-C+ z;0l;1CYK&Sf+I_F)bDbl2FWV2Z)6^jLG*_m$fwjpP#-C{Xs zE)LDtjndZE3E8*|6!=tjY<8_Z8a9S;m3qHg#|h=~psR9z$#A)@NWOdIN& z+?-R?LZH&Mdv#+XeyJHD5^xH@gOmlZQ3 ztd_N;7gG?fdo!5~)t3O!M_{}R+`g+;cW7J*;7yP%kcfPGTdCMbuw&-fRMLiku>f4l z_fhhTh-+44\d+)/version/all/$', 'document_version_list', (), 'document_version_list'), url(r'^document/version/(?P\d+)/download/$', 'document_download', (), 'document_version_download'), + url(r'^document/version/(?P\d+)/revert/$', 'document_version_revert', (), 'document_version_revert'), url(r'^multiple/clear_transformations/$', 'document_multiple_clear_transformations', (), 'document_multiple_clear_transformations'), url(r'^duplicates/list/$', 'document_find_all_duplicates', (), 'document_find_all_duplicates'), diff --git a/apps/documents/views.py b/apps/documents/views.py index 4b08e9f5ee..2bff281ef9 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -36,14 +36,15 @@ from documents.conf.settings import ROTATION_STEP from documents.conf.settings import PRINT_SIZE from documents.conf.settings import RECENT_COUNT -from documents.literals import PERMISSION_DOCUMENT_CREATE, \ - PERMISSION_DOCUMENT_PROPERTIES_EDIT, \ - PERMISSION_DOCUMENT_VIEW, \ - PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_DOWNLOAD, \ - PERMISSION_DOCUMENT_TRANSFORM, \ - PERMISSION_DOCUMENT_EDIT, PERMISSION_DOCUMENT_TOOLS -from documents.literals import HISTORY_DOCUMENT_CREATED, \ - HISTORY_DOCUMENT_EDITED, HISTORY_DOCUMENT_DELETED +from documents.literals import (PERMISSION_DOCUMENT_CREATE, + PERMISSION_DOCUMENT_PROPERTIES_EDIT, + PERMISSION_DOCUMENT_VIEW, + PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_DOWNLOAD, + PERMISSION_DOCUMENT_TRANSFORM, + PERMISSION_DOCUMENT_EDIT, PERMISSION_DOCUMENT_TOOLS, + PERMISSION_DOCUMENT_VERSION_REVERT) +from documents.literals import (HISTORY_DOCUMENT_CREATED, + HISTORY_DOCUMENT_EDITED, HISTORY_DOCUMENT_DELETED) from documents.forms import (DocumentTypeSelectForm, DocumentForm_edit, DocumentPropertiesForm, @@ -1188,3 +1189,26 @@ def document_version_list(request, document_pk): return render_to_response('generic_list.html', context, context_instance=RequestContext(request)) + + +def document_version_revert(request, document_version_pk): + check_permissions(request.user, [PERMISSION_DOCUMENT_VERSION_REVERT]) + + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + + if request.method == 'POST': + #try: + document_version = get_object_or_404(DocumentVersion, pk=document_version_pk) + document_version.revert() + messages.success(request, _(u'Document version reverted successfully')) + #except Exception, msg: + # messages.error(request, _(u'Error reverting document version; %s') % msg) + + return HttpResponseRedirect(previous) + + return render_to_response('generic_confirm.html', { + 'previous': previous, + 'title': _(u'Are you sure you wish to revert to this version?'), + 'message': _(u'All later version after this one will be deleted too.'), + 'form_icon': u'page_refresh.png', + }, context_instance=RequestContext(request)) From 7de09aae0fb465e1338adfad30507baffe6a2f04 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:00:04 -0400 Subject: [PATCH 51/92] Add version support to the document image generation code --- apps/documents/models.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index ef9220b277..a658826191 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -113,35 +113,39 @@ class Document(models.Model): self.date_added = datetime.datetime.now() super(Document, self).save(*args, **kwargs) - def get_cached_image_name(self, page): - document_page = self.pages.get(page_number=page) + def get_cached_image_name(self, page, version): + document_version = DocumentVersion.objects.get(pk=version) + document_page = document_version.documentpage_set.get(page_number=page) transformations, warnings = document_page.get_transformation_list() - hash_value = HASH_FUNCTION(u''.join([self.checksum, unicode(page), unicode(transformations)])) + hash_value = HASH_FUNCTION(u''.join([document_version.checksum, unicode(page), unicode(transformations)])) return os.path.join(CACHE_PATH, hash_value), transformations - def get_image_cache_name(self, page): - cache_file_path, transformations = self.get_cached_image_name(page) + def get_image_cache_name(self, page, version): + cache_file_path, transformations = self.get_cached_image_name(page, version) if os.path.exists(cache_file_path): return cache_file_path else: - document_file = document_save_to_temp_dir(self, self.checksum) + document_version = DocumentVersion.objects.get(pk=version) + document_file = document_save_to_temp_dir(document_version, document_version.checksum) return convert(document_file, output_filepath=cache_file_path, page=page, transformations=transformations, mimetype=self.file_mimetype) - def get_valid_image(self, size=DISPLAY_SIZE, page=DEFAULT_PAGE_NUMBER, zoom=DEFAULT_ZOOM_LEVEL, rotation=DEFAULT_ROTATION): - image_cache_name = self.get_image_cache_name(page=page) + def get_valid_image(self, size=DISPLAY_SIZE, page=DEFAULT_PAGE_NUMBER, zoom=DEFAULT_ZOOM_LEVEL, rotation=DEFAULT_ROTATION, version=None): + if not version: + version = self.latest_version.pk + image_cache_name = self.get_image_cache_name(page=page, version=version) return convert(image_cache_name, cleanup_files=False, size=size, zoom=zoom, rotation=rotation) - def get_image(self, size=DISPLAY_SIZE, page=DEFAULT_PAGE_NUMBER, zoom=DEFAULT_ZOOM_LEVEL, rotation=DEFAULT_ROTATION, as_base64=False): + def get_image(self, size=DISPLAY_SIZE, page=DEFAULT_PAGE_NUMBER, zoom=DEFAULT_ZOOM_LEVEL, rotation=DEFAULT_ROTATION, as_base64=False, version=None): if zoom < ZOOM_MIN_LEVEL: zoom = ZOOM_MIN_LEVEL if zoom > ZOOM_MAX_LEVEL: zoom = ZOOM_MAX_LEVEL - rotation = rotation % 360 + rotation = rotation % 360 try: - file_path = self.get_valid_image(size=size, page=page, zoom=zoom, rotation=rotation) + file_path = self.get_valid_image(size=size, page=page, zoom=zoom, rotation=rotation, version=version) except UnknownFileFormat: file_path = get_icon_file_path(self.file_mimetype) except UnkownConvertError: @@ -184,6 +188,7 @@ class Document(models.Model): logger.debug('creating new document version') if version_update: new_version_dict = self.latest_version.get_new_version_dict(version_update) + logger.debug('new_version_dict: %s' % new_version_dict) new_version = DocumentVersion( document=self, file=file, @@ -203,9 +208,6 @@ class Document(models.Model): ) new_version.save() - - logger.debug('new_version_dict: %s' % new_version_dict) - logger.debug('new_version saved') return new_version @@ -461,7 +463,7 @@ class DocumentVersion(models.Model): self.save() def delete(self, *args, **kwargs): - super(DocumentVesion, self).delete(*args, **kwargs) + super(DocumentVersion, self).delete(*args, **kwargs) return self.file.storage.delete(self.file.path) def exists(self): From 255cb8eaf21e9cec6f8c2de6cafe98211771adb4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:00:28 -0400 Subject: [PATCH 52/92] Updated the smart document widget to be version aware --- apps/documents/widgets.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/documents/widgets.py b/apps/documents/widgets.py index e449ffc335..031461047f 100644 --- a/apps/documents/widgets.py +++ b/apps/documents/widgets.py @@ -20,14 +20,19 @@ def document_link(document): return mark_safe(u'%s' % (reverse('document_view_simple', args=[document.pk]), document)) -def document_html_widget(document, view='document_thumbnail', click_view=None, page=DEFAULT_PAGE_NUMBER, zoom=DEFAULT_ZOOM_LEVEL, rotation=DEFAULT_ROTATION, gallery_name=None, fancybox_class='fancybox'): +def document_html_widget(document, view='document_thumbnail', click_view=None, page=DEFAULT_PAGE_NUMBER, zoom=DEFAULT_ZOOM_LEVEL, rotation=DEFAULT_ROTATION, gallery_name=None, fancybox_class='fancybox', version=None): result = [] alt_text = _(u'document page image') + + if not version: + version = document.latest_version.pk + query_dict = { 'page': page, 'zoom': zoom, 'rotation': rotation, + 'version': version, } if gallery_name: @@ -67,7 +72,7 @@ def document_html_widget(document, view='document_thumbnail', click_view=None, p }); ''' % { - 'url': reverse('documents-expensive-is_zoomable', args=[document.pk, page]), + 'url': reverse('documents-expensive-is_zoomable', args=[document.pk, version, page]), 'pk': document.pk, 'page': page if page else 1, 'plain_template': mark_safe(u''.join(plain_template)), From 2d119b853f0bc98f1677447a231d7b99c0466773 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:01:24 -0400 Subject: [PATCH 53/92] Update the 'get_document_image' view to be version aware --- apps/documents/views.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/documents/views.py b/apps/documents/views.py index 2bff281ef9..cbf3853565 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -280,6 +280,8 @@ def get_document_image(request, document_id, size=PREVIEW_SIZE, base64_version=F page = int(request.GET.get('page', DEFAULT_PAGE_NUMBER)) zoom = int(request.GET.get('zoom', DEFAULT_ZOOM_LEVEL)) + + version = int(request.GET.get('version', document.latest_version.pk)) if zoom < ZOOM_MIN_LEVEL: zoom = ZOOM_MIN_LEVEL @@ -290,10 +292,10 @@ def get_document_image(request, document_id, size=PREVIEW_SIZE, base64_version=F rotation = int(request.GET.get('rotation', DEFAULT_ROTATION)) % 360 if base64_version: - return HttpResponse(u'' % document.get_image(size=size, page=page, zoom=zoom, rotation=rotation, as_base64=True)) + return HttpResponse(u'' % document.get_image(size=size, page=page, zoom=zoom, rotation=rotation, as_base64=True, version=version)) else: # TODO: fix hardcoded MIMETYPE - return sendfile.sendfile(request, document.get_image(size=size, page=page, zoom=zoom, rotation=rotation), mimetype=DEFAULT_FILE_FORMAT_MIMETYPE) + return sendfile.sendfile(request, document.get_image(size=size, page=page, zoom=zoom, rotation=rotation, version=version), mimetype=DEFAULT_FILE_FORMAT_MIMETYPE) def document_download(request, document_id=None, document_version_pk=None): @@ -1197,12 +1199,12 @@ def document_version_revert(request, document_version_pk): previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) if request.method == 'POST': - #try: - document_version = get_object_or_404(DocumentVersion, pk=document_version_pk) - document_version.revert() - messages.success(request, _(u'Document version reverted successfully')) - #except Exception, msg: - # messages.error(request, _(u'Error reverting document version; %s') % msg) + try: + document_version = get_object_or_404(DocumentVersion, pk=document_version_pk) + document_version.revert() + messages.success(request, _(u'Document version reverted successfully')) + except Exception, msg: + messages.error(request, _(u'Error reverting document version; %s') % msg) return HttpResponseRedirect(previous) From 73950ef750755b68b1f59abe90d8c24f3ab7f7c4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:01:48 -0400 Subject: [PATCH 54/92] Disable the revert link for the current version of a document --- apps/documents/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/documents/__init__.py b/apps/documents/__init__.py index e6e67f90c8..750b2953a8 100644 --- a/apps/documents/__init__.py +++ b/apps/documents/__init__.py @@ -46,6 +46,10 @@ def is_min_zoom(context): def is_max_zoom(context): return context['zoom'] >= ZOOM_MAX_LEVEL + +def is_current_version(context): + return context['object'].document.latest_version.timestamp == context['object'].timestamp + # Permission setup set_namespace_title('documents', _(u'Documents')) register_permission(PERMISSION_DOCUMENT_CREATE) @@ -114,7 +118,7 @@ document_page_view_reset = {'text': _(u'reset view'), 'class': 'no-parent-histor # Document versions document_version_list = {'text': _(u'versions'), 'view': 'document_version_list', 'args': 'object.pk', 'famfam': 'page_world', 'permissions': [PERMISSION_DOCUMENT_VIEW]} -document_version_revert = {'text': _(u'revert'), 'view': 'document_version_revert', 'args': 'object.pk', 'famfam': 'page_refresh', 'permissions': [PERMISSION_DOCUMENT_VERSION_REVERT]} +document_version_revert = {'text': _(u'revert'), 'view': 'document_version_revert', 'args': 'object.pk', 'famfam': 'page_refresh', 'permissions': [PERMISSION_DOCUMENT_VERSION_REVERT], 'conditional_disable': is_current_version} # Document type related links document_type_list = {'text': _(u'document type list'), 'view': 'document_type_list', 'famfam': 'layout', 'permissions': [PERMISSION_DOCUMENT_VIEW]} From f0b1f9e6b3bc570e5371379d4b06d237cc33c3a5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:02:16 -0400 Subject: [PATCH 55/92] Remove removed code --- apps/rest_api/resources.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/apps/rest_api/resources.py b/apps/rest_api/resources.py index 869c78e407..d127096f3a 100644 --- a/apps/rest_api/resources.py +++ b/apps/rest_api/resources.py @@ -40,30 +40,6 @@ class DocumentResourceSimple(ModelResource): } for version in instance.versions.all() ] - - ''' - return [ - { - 'version': 1, - 'mimetype': instance.file_mimetype, - 'encoding': instance.file_mime_encoding, - 'filename': instance.filename, - 'date_updated': instance.date_updated, - 'checksum': instance.checksum, - 'size': instance.size, - 'exists': instance.exists(), - 'pages': [ - { - 'page_numer': page.page_number, - 'page_label': page.page_label, - 'is_zoomable': reverse('documents-expensive-is_zoomable', args=[instance.pk, page.page_number]), - - #'content': - } - for page in instance.documentpage_set.all() - ] - } - ] - ''' + def expensive_methods(self, instance): return [] From ae0ecb20a60be402ae5acda14fa08607b2f060bd Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:02:34 -0400 Subject: [PATCH 56/92] Update the 'is_zoomable' api call to be version aware --- apps/rest_api/urls.py | 2 +- apps/rest_api/views.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/rest_api/urls.py b/apps/rest_api/urls.py index 3265dd7574..34553951a6 100644 --- a/apps/rest_api/urls.py +++ b/apps/rest_api/urls.py @@ -12,5 +12,5 @@ urlpatterns = patterns('', # Version 0 alpha API calls url(r'^v0/document/(?P[0-9]+)/$', ReadOnlyInstanceModelView.as_view(resource=DocumentResourceSimple), name='documents-simple'), - url(r'^v0/document/(?P[0-9]+)/page/(?P[0-9]+)/expensive/is_zoomable/$', IsZoomable.as_view(), name='documents-expensive-is_zoomable'), + url(r'^v0/document/(?P[0-9]+)/version/(?P[0-9]+)/page/(?P[0-9]+)/expensive/is_zoomable/$', IsZoomable.as_view(), name='documents-expensive-is_zoomable'), ) diff --git a/apps/rest_api/views.py b/apps/rest_api/views.py index 6a1b5dfc14..d7623e4ffe 100644 --- a/apps/rest_api/views.py +++ b/apps/rest_api/views.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 from django.core.urlresolvers import reverse -from documents.models import Document +from documents.models import Document, DocumentVersion from converter.exceptions import UnknownFileFormat, UnkownConvertError from djangorestframework.views import View, ModelView, ListModelView, InstanceModelView @@ -45,11 +45,11 @@ class Version_0(View): class IsZoomable(View): - def get(self, request, pk, page_number): + def get(self, request, pk, page_number, version_pk): logger.info('received is_zoomable call from: %s' % (request.META['REMOTE_ADDR'])) - document = get_object_or_404(Document, pk=pk) + document_version = get_object_or_404(DocumentVersion, pk=version_pk) try: - document.get_image_cache_name(int(page_number)) + document_version.document.get_image_cache_name(int(page_number), version_pk) return {'result': True} except (UnknownFileFormat, UnkownConvertError): return {'result': False} From 479e3d5cec54b9fb936484f34b9532ed6d491f5e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:15:33 -0400 Subject: [PATCH 57/92] Update the REST API call 'is_zoomable' to catch more errors --- apps/rest_api/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/rest_api/views.py b/apps/rest_api/views.py index d7623e4ffe..6624e41510 100644 --- a/apps/rest_api/views.py +++ b/apps/rest_api/views.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 from django.core.urlresolvers import reverse -from documents.models import Document, DocumentVersion +from documents.models import Document, DocumentVersion, DocumentPage from converter.exceptions import UnknownFileFormat, UnkownConvertError from djangorestframework.views import View, ModelView, ListModelView, InstanceModelView @@ -51,5 +51,7 @@ class IsZoomable(View): try: document_version.document.get_image_cache_name(int(page_number), version_pk) return {'result': True} - except (UnknownFileFormat, UnkownConvertError): + except (UnknownFileFormat, UnkownConvertError, + DocumentPage.DoesNotExist, Document.DoesNotExist, + DocumentVersion.DoesNotExist): return {'result': False} From 03620f533155b2d2d34a7baa6f16f077eb772ad0 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:15:57 -0400 Subject: [PATCH 58/92] Update the 'find duplicates' view to work with document versions --- apps/documents/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/documents/views.py b/apps/documents/views.py index cbf3853565..7e5f563390 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -458,12 +458,11 @@ def _find_duplicate_list(request, source_document_list=Document.objects.all(), i duplicated = [] for document in source_document_list: if document.pk not in duplicated: - results = Document.objects.filter(checksum=document.checksum).exclude(id__in=duplicated).exclude(pk=document.pk).values_list('pk', flat=True) + results = DocumentVersion.objects.filter(checksum=document.latest_version.checksum).exclude(id__in=duplicated).exclude(pk=document.pk).values_list('document__pk', flat=True) duplicated.extend(results) if include_source and results: duplicated.append(document.pk) - context = { 'object_list': Document.objects.filter(pk__in=duplicated), 'title': _(u'duplicated documents'), From 64e73a9c9d58162657f1a961626ca00ec9ef24ff Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:20:44 -0400 Subject: [PATCH 59/92] Update the 'invalidate_cached_image' to supply a version pk as now required by the version aware 'get_cached_image_name' method --- apps/documents/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index a658826191..b4e1aa2a33 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -163,7 +163,7 @@ class Document(models.Model): def invalidate_cached_image(self, page): try: - os.unlink(self.get_cached_image_name(page)[0]) + os.unlink(self.get_cached_image_name(page, self.latest_version.pk)[0]) except OSError: pass From fafadfaca2c62b015560a6a377a47ff013dd18b8 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:37:11 -0400 Subject: [PATCH 60/92] Fix document print view --- apps/converter/api.py | 4 ++-- apps/documents/forms.py | 8 ++++---- apps/documents/templates/document_print.html | 7 ++++--- apps/documents/templatetags/printing_tags.py | 4 ++-- apps/documents/views.py | 18 +++++++++--------- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/apps/converter/api.py b/apps/converter/api.py index 993fb70f1d..ff4e9d645b 100644 --- a/apps/converter/api.py +++ b/apps/converter/api.py @@ -110,7 +110,7 @@ def get_page_count(input_filepath): return backend.get_page_count(input_filepath) - +''' def get_document_dimensions(document, *args, **kwargs): document_filepath = create_image_cache_filename(document.checksum, *args, **kwargs) if os.path.exists(document_filepath): @@ -118,7 +118,7 @@ def get_document_dimensions(document, *args, **kwargs): return [int(dimension) for dimension in backend.identify_file(unicode(document_filepath), options).split()] else: return [0, 0] - +''' def get_available_transformations_choices(): result = [] diff --git a/apps/documents/forms.py b/apps/documents/forms.py index 60d69506ac..a829516327 100644 --- a/apps/documents/forms.py +++ b/apps/documents/forms.py @@ -271,10 +271,10 @@ class DocumentTypeSelectForm(forms.Form): class PrintForm(forms.Form): - page_size = forms.ChoiceField(choices=PAGE_SIZE_CHOICES, initial=DEFAULT_PAPER_SIZE, label=_(u'Page size'), required=False) - custom_page_width = forms.CharField(label=_(u'Custom page width'), required=False) - custom_page_height = forms.CharField(label=_(u'Custom page height'), required=False) - page_orientation = forms.ChoiceField(choices=PAGE_ORIENTATION_CHOICES, initial=DEFAULT_PAGE_ORIENTATION, label=_(u'Page orientation'), required=True) + #page_size = forms.ChoiceField(choices=PAGE_SIZE_CHOICES, initial=DEFAULT_PAPER_SIZE, label=_(u'Page size'), required=False) + #custom_page_width = forms.CharField(label=_(u'Custom page width'), required=False) + #custom_page_height = forms.CharField(label=_(u'Custom page height'), required=False) + #page_orientation = forms.ChoiceField(choices=PAGE_ORIENTATION_CHOICES, initial=DEFAULT_PAGE_ORIENTATION, label=_(u'Page orientation'), required=True) page_range = forms.CharField(label=_(u'Page range'), required=False) diff --git a/apps/documents/templates/document_print.html b/apps/documents/templates/document_print.html index 06068fc31b..35f08abaf8 100644 --- a/apps/documents/templates/document_print.html +++ b/apps/documents/templates/document_print.html @@ -1,5 +1,5 @@ {% load project_tags %} -{% load printing_tags %} +{#{% load printing_tags %}#} @@ -48,9 +48,10 @@ {% for page in pages %} - {% get_document_size object %} + {#{% get_document_size object %}#}
- page_aspect %}width="97%"{% else %}height="97%"{% endif %} /> + {# page_aspect %}width="97%"{% else %}height="97%"{% endif %} />#} +
{% endfor %} diff --git a/apps/documents/templatetags/printing_tags.py b/apps/documents/templatetags/printing_tags.py index c2fc6b4e29..7dccae8085 100644 --- a/apps/documents/templatetags/printing_tags.py +++ b/apps/documents/templatetags/printing_tags.py @@ -1,6 +1,6 @@ from django.template import Library, Node, Variable -from converter.api import get_document_dimensions +from converter.api import get_dimensions from documents.conf.settings import PRINT_SIZE @@ -13,7 +13,7 @@ class GetImageSizeNode(Node): def render(self, context): document = Variable(self.document).resolve(context) - width, height = get_document_dimensions(document) + width, height = get_dimensions(document) context[u'document_width'], context['document_height'] = width, height context[u'document_aspect'] = float(width) / float(height) return u'' diff --git a/apps/documents/views.py b/apps/documents/views.py index 7e5f563390..fdd98922c6 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -799,18 +799,18 @@ def document_print(request, document_id): hard_copy_arguments['page_range'] = form.cleaned_data['page_range'] # Compute page width and height - if form.cleaned_data['custom_page_width'] and form.cleaned_data['custom_page_height']: - page_width = form.cleaned_data['custom_page_width'] - page_height = form.cleaned_data['custom_page_height'] - elif form.cleaned_data['page_size']: - page_width, page_height = dict(PAGE_SIZE_DIMENSIONS)[form.cleaned_data['page_size']] + #if form.cleaned_data['custom_page_width'] and form.cleaned_data['custom_page_height']: + # page_width = form.cleaned_data['custom_page_width'] + # page_height = form.cleaned_data['custom_page_height'] + #elif form.cleaned_data['page_size']: + # page_width, page_height = dict(PAGE_SIZE_DIMENSIONS)[form.cleaned_data['page_size']] # Page orientation - if form.cleaned_data['page_orientation'] == PAGE_ORIENTATION_LANDSCAPE: - page_width, page_height = page_height, page_width + #if form.cleaned_data['page_orientation'] == PAGE_ORIENTATION_LANDSCAPE: + # page_width, page_height = page_height, page_width - hard_copy_arguments['page_width'] = page_width - hard_copy_arguments['page_height'] = page_height + #hard_copy_arguments['page_width'] = page_width + #hard_copy_arguments['page_height'] = page_height new_url = [reverse('document_hard_copy', args=[document_id])] if hard_copy_arguments: From f5c3fa21d726fad48b8c27821ff7bfef4207ef09 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 02:44:08 -0400 Subject: [PATCH 61/92] Add a size property to the DocumentVersion model --- apps/documents/models.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index b4e1aa2a33..bc08a1945a 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -179,10 +179,7 @@ class Document(models.Model): @property def size(self): - if self.exists(): - return self.latest_version.exists() - else: - return None + return self.latest_version.size def new_version(self, file, comment=None, version_update=None, release_level=None, serial=None): logger.debug('creating new document version') @@ -498,6 +495,13 @@ class DocumentVersion(models.Model): input_descriptor.close() return filepath + @property + def size(self): + if self.exists(): + return self.file.storage.size(self.file.path) + else: + return None + class DocumentTypeFilename(models.Model): ''' From 071fed89963c784a2edcd3472217fb53fdf95493 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 03:21:34 -0400 Subject: [PATCH 62/92] Add new version upload option to use new file filename as the document filename --- apps/documents/forms.py | 7 +++ apps/sources/models.py | 11 +++- apps/sources/views.py | 124 +++++++++++++++++++++------------------- 3 files changed, 79 insertions(+), 63 deletions(-) diff --git a/apps/documents/forms.py b/apps/documents/forms.py index a829516327..991b6488e8 100644 --- a/apps/documents/forms.py +++ b/apps/documents/forms.py @@ -156,6 +156,13 @@ class DocumentForm(forms.ModelForm): # To allow merging with DocumentForm_edit self.fields['document_type'].widget = forms.HiddenInput() + if instance: + self.fields['use_file_name'] = forms.BooleanField( + label=_(u'Use the new version filename as the document filename'), + initial=False, + required=False, + ) + # Instance's document_type overrides the passed document_type if instance: if hasattr(instance, 'document_type'): diff --git a/apps/sources/models.py b/apps/sources/models.py index 44822ee0ba..3d09915617 100644 --- a/apps/sources/models.py +++ b/apps/sources/models.py @@ -52,7 +52,7 @@ class BaseModel(models.Model): def get_transformation_list(self): return SourceTransformation.transformations.get_for_object_as_list(self) - def upload_file(self, file_object, filename=None, document_type=None, expand=False, metadata_dict_list=None, user=None, document=None, new_version_data=None): + def upload_file(self, file_object, filename=None, use_file_name=False, document_type=None, expand=False, metadata_dict_list=None, user=None, document=None, new_version_data=None): if expand: try: cf = CompressedFile(file_object) @@ -63,11 +63,11 @@ class BaseModel(models.Model): except NotACompressedFile: self.upload_single_file(file_object, filename, document_type, metadata_dict_list, user) else: - self.upload_single_file(file_object, filename, document_type, metadata_dict_list, user, document, new_version_data) + self.upload_single_file(file_object, filename, use_file_name, document_type, metadata_dict_list, user, document, new_version_data) file_object.close() - def upload_single_file(self, file_object, filename=None, document_type=None, metadata_dict_list=None, user=None, document=None, new_version_data=None): + def upload_single_file(self, file_object, filename=None, use_file_name=False, document_type=None, metadata_dict_list=None, user=None, document=None, new_version_data=None): if not document: document = Document() if document_type: @@ -83,6 +83,11 @@ class BaseModel(models.Model): create_history(HISTORY_DOCUMENT_CREATED, document, {'user': user}) else: create_history(HISTORY_DOCUMENT_CREATED, document) + else: + if use_file_name: + filename = None + else: + filename = filename if filename else document.latest_version.filename if not new_version_data: new_version_data = {} diff --git a/apps/sources/views.py b/apps/sources/views.py index c668b23909..88e1d08b72 100644 --- a/apps/sources/views.py +++ b/apps/sources/views.py @@ -126,35 +126,37 @@ def upload_interactive(request, source_type=None, source_id=None, document_pk=No instance=document ) if form.is_valid(): - #try: - if document: - expand = False - else: - if web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK: - expand = form.cleaned_data.get('expand') + try: + if document: + expand = False else: - if web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y: - expand = True + if web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK: + expand = form.cleaned_data.get('expand') else: - expand = False - - new_filename = get_form_filename(form) - web_form.upload_file(request.FILES['file'], - new_filename, document_type=document_type, - expand=expand, - metadata_dict_list=decode_metadata_from_url(request.GET), - user=request.user, - document=document, - new_version_data=form.cleaned_data.get('new_version_data') - ) - #except Exception, e: - # messages.error(request, _(u'Unhandled exception: %s') % e) - if document: - messages.success(request, _(u'Document version uploaded successfully.')) - return HttpResponseRedirect(reverse('document_view_simple', args=[document.pk])) - else: - messages.success(request, _(u'Document uploaded successfully.')) - return HttpResponseRedirect(request.get_full_path()) + if web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y: + expand = True + else: + expand = False + + new_filename = get_form_filename(form) + + web_form.upload_file(request.FILES['file'], + new_filename, use_file_name=form.cleaned_data.get('use_file_name', False), + document_type=document_type, + expand=expand, + metadata_dict_list=decode_metadata_from_url(request.GET), + user=request.user, + document=document, + new_version_data=form.cleaned_data.get('new_version_data') + ) + if document: + messages.success(request, _(u'Document version uploaded successfully.')) + return HttpResponseRedirect(reverse('document_view_simple', args=[document.pk])) + else: + messages.success(request, _(u'Document uploaded successfully.')) + return HttpResponseRedirect(request.get_full_path()) + except Exception, e: + messages.error(request, _(u'Unhandled exception: %s') % e) else: form = WebFormForm( show_expand=(web_form.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK) and not document, @@ -186,43 +188,45 @@ def upload_interactive(request, source_type=None, source_id=None, document_pk=No instance=document ) if form.is_valid(): - #try: - staging_file = StagingFile.get(form.cleaned_data['staging_file_id']) - if document: - expand = False - else: - if staging_folder.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK: - expand = form.cleaned_dataget('expand') + try: + staging_file = StagingFile.get(form.cleaned_data['staging_file_id']) + if document: + expand = False else: - if staging_folder.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y: - expand = True + if staging_folder.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK: + expand = form.cleaned_dataget('expand') else: - expand = False + if staging_folder.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y: + expand = True + else: + expand = False - new_filename = get_form_filename(form) - staging_folder.upload_file(staging_file.upload(), - new_filename, document_type=document_type, - expand=expand, - metadata_dict_list=decode_metadata_from_url(request.GET), - user=request.user, - document=document, - new_version_data=form.cleaned_data.get('new_version_data') - ) - if document: - messages.success(request, _(u'Document version from staging file: %s, uploaded successfully.') % staging_file.filename) - else: - messages.success(request, _(u'Staging file: %s, uploaded successfully.') % staging_file.filename) + new_filename = get_form_filename(form) + + staging_folder.upload_file(staging_file.upload(), + new_filename, use_file_name=form.cleaned_data.get('use_file_name', False), + document_type=document_type, + expand=expand, + metadata_dict_list=decode_metadata_from_url(request.GET), + user=request.user, + document=document, + new_version_data=form.cleaned_data.get('new_version_data') + ) + if document: + messages.success(request, _(u'Document version from staging file: %s, uploaded successfully.') % staging_file.filename) + else: + messages.success(request, _(u'Staging file: %s, uploaded successfully.') % staging_file.filename) - if staging_folder.delete_after_upload: - transformations, errors = staging_folder.get_transformation_list() - staging_file.delete(preview_size=staging_folder.get_preview_size(), transformations=transformations) - messages.success(request, _(u'Staging file: %s, deleted successfully.') % staging_file.filename) - #except Exception, e: - # messages.error(request, _(u'Unhandled exception: %s') % e) - if document: - return HttpResponseRedirect(reverse('document_view_simple', args=[document.pk])) - else: - return HttpResponseRedirect(request.get_full_path()) + if staging_folder.delete_after_upload: + transformations, errors = staging_folder.get_transformation_list() + staging_file.delete(preview_size=staging_folder.get_preview_size(), transformations=transformations) + messages.success(request, _(u'Staging file: %s, deleted successfully.') % staging_file.filename) + if document: + return HttpResponseRedirect(reverse('document_view_simple', args=[document.pk])) + else: + return HttpResponseRedirect(request.get_full_path()) + except Exception, e: + messages.error(request, _(u'Unhandled exception: %s') % e) else: form = StagingDocumentForm(cls=StagingFile, document_type=document_type, From 071139c5bf9733c2611f1b0e78a25960d4223dcf Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 05:52:09 -0400 Subject: [PATCH 63/92] Add document signature verification app --- apps/django_gpg/__init__.py | 19 ++ apps/django_gpg/api.py | 267 ++++++++++++++++++ apps/django_gpg/conf/__init__.py | 0 apps/django_gpg/conf/settings.py | 15 + apps/django_gpg/exceptions.py | 31 ++ .../locale/en/LC_MESSAGES/django.po | 203 +++++++++++++ .../locale/es/LC_MESSAGES/django.mo | Bin 0 -> 4289 bytes .../locale/es/LC_MESSAGES/django.po | 217 ++++++++++++++ apps/django_gpg/models.py | 1 + apps/django_gpg/runtime.py | 4 + apps/django_gpg/static/images/icons/cross.png | Bin 0 -> 1049 bytes .../images/icons/document_signature.png | Bin 0 -> 1598 bytes .../static/images/icons/user_silhouette.png | Bin 0 -> 1087 bytes apps/django_gpg/urls.py | 8 + apps/django_gpg/views.py | 113 ++++++++ apps/documents/views.py | 2 + requirements/development.txt | 1 + requirements/production.txt | 1 + settings.py | 1 + urls.py | 1 + 20 files changed, 884 insertions(+) create mode 100644 apps/django_gpg/__init__.py create mode 100644 apps/django_gpg/api.py create mode 100644 apps/django_gpg/conf/__init__.py create mode 100644 apps/django_gpg/conf/settings.py create mode 100644 apps/django_gpg/exceptions.py create mode 100644 apps/django_gpg/locale/en/LC_MESSAGES/django.po create mode 100644 apps/django_gpg/locale/es/LC_MESSAGES/django.mo create mode 100644 apps/django_gpg/locale/es/LC_MESSAGES/django.po create mode 100644 apps/django_gpg/models.py create mode 100644 apps/django_gpg/runtime.py create mode 100644 apps/django_gpg/static/images/icons/cross.png create mode 100644 apps/django_gpg/static/images/icons/document_signature.png create mode 100644 apps/django_gpg/static/images/icons/user_silhouette.png create mode 100644 apps/django_gpg/urls.py create mode 100644 apps/django_gpg/views.py diff --git a/apps/django_gpg/__init__.py b/apps/django_gpg/__init__.py new file mode 100644 index 0000000000..de9aeeba41 --- /dev/null +++ b/apps/django_gpg/__init__.py @@ -0,0 +1,19 @@ +from django.utils.translation import ugettext_lazy as _ + +from documents.models import Document +from navigation.api import register_links, register_top_menu, \ + register_model_list_columns, register_multi_item_links, \ + register_sidebar_template +from main.api import register_diagnostic, register_maintenance_links +from permissions.api import register_permission, set_namespace_title +from project_setup.api import register_setup + +PERMISSION_DOCUMENT_VERIFY = {'namespace': 'django_gpg', 'name': 'document_verify', 'label': _(u'Verify document signatures')} + +# Permission setup +set_namespace_title('django_gpg', _(u'Signatures')) +register_permission(PERMISSION_DOCUMENT_VERIFY) + +document_verify = {'text': _(u'Signatures'), 'view': 'document_verify', 'args': 'object.pk', 'famfam': 'text_signature', 'permissions': [PERMISSION_DOCUMENT_VERIFY]} + +register_links(Document, [document_verify], menu_name='form_header') diff --git a/apps/django_gpg/api.py b/apps/django_gpg/api.py new file mode 100644 index 0000000000..504d9ba664 --- /dev/null +++ b/apps/django_gpg/api.py @@ -0,0 +1,267 @@ +import types +from StringIO import StringIO +from pickle import dumps + +import gnupg + +from django.core.files.base import File +from django.utils.translation import ugettext_lazy as _ + +from django_gpg.exceptions import GPGVerificationError, GPGSigningError, \ + GPGDecryptionError, KeyDeleteError, KeyGenerationError, \ + KeyFetchingError, KeyDoesNotExist + + +KEY_TYPES = { + 'pub': _(u'Public'), + 'sec': _(u'Secret'), +} + +KEY_CLASS_RSA = 'RSA' +KEY_CLASS_DSA = 'DSA' +KEY_CLASS_ELG = 'ELG-E' + +KEY_PRIMARY_CLASSES = ( + ((KEY_CLASS_RSA), _(u'RSA')), + ((KEY_CLASS_DSA), _(u'DSA')), +) + +KEY_SECONDARY_CLASSES = ( + ((KEY_CLASS_RSA), _(u'RSA')), + ((KEY_CLASS_ELG), _(u'Elgamal')), +) + +class Key(object): + @staticmethod + def get_key_id(fingerprint): + return fingerprint[-16:] + + @classmethod + def get_all(cls, gpg, secret=False, exclude=None): + result = [] + keys = gpg.gpg.list_keys(secret=secret) + if exclude: + excluded_id = exclude.key_id + else: + excluded_id = u'' + for key in keys: + if not key['keyid'] in excluded_id: + key_instance = Key( + fingerprint=key['fingerprint'], + uids=key['uids'], + type=key['type'], + data=gpg.gpg.export_keys([key['keyid']], secret=secret) + ) + result.append(key_instance) + + return result + + @classmethod + def get(cls, gpg, key_id, secret=False, search_keyservers=False): + if len(key_id) > 16: + # key_id is a fingerprint + key_id = Key.get_key_id(key_id) + + keys = gpg.gpg.list_keys(secret=secret) + key = next((key for key in keys if key['keyid'] == key_id), None) + if not key: + if search_keyservers and secret==False: + try: + gpg.receive_key(key_id) + return Key(gpg, key_id) + except KeyFetchingError: + raise KeyDoesNotExist + else: + raise KeyDoesNotExist + + key_instance = Key( + fingerprint=key['fingerprint'], + uids=key['uids'], + type=key['type'], + data=gpg.gpg.export_keys([key['keyid']], secret=secret) + ) + + return key_instance + + def __init__(self, fingerprint, uids, type, data): + self.fingerprint = fingerprint + self.uids = uids + self.type = type + self.data = data + + @property + def key_id(self): + return Key.get_key_id(self.fingerprint) + + @property + def user_ids(self): + return u', '.join(self.uids) + + def __str__(self): + return '%s "%s" (%s)' % (self.key_id, self.user_ids, KEY_TYPES.get(self.type, _(u'unknown'))) + + def __unicode__(self): + return unicode(self.__str__()) + + def __repr__(self): + return self.__unicode__() + + +class GPG(object): + def __init__(self, binary_path=None, home=None, keyring=None, keyservers=None): + kwargs = {} + if binary_path: + kwargs['gpgbinary'] = binary_path + + if home: + kwargs['gnupghome'] = home + + if keyring: + kwargs['keyring'] = keyring + + self.keyservers = keyservers + + self.gpg = gnupg.GPG(**kwargs) + + def verify_w_retry(self, file_input): + if isinstance(file_input, types.StringTypes): + input_descriptor = open(file_input, 'rb') + elif isinstance(file_input, types.FileType) or isinstance(file_input, File): + input_descriptor = file_input + elif issubclass(file_input.__class__, StringIO): + input_descriptor = file_input + else: + raise ValueError('Invalid file_input argument type') + + try: + verify = self.verify_file(input_descriptor) + if verify.status == 'no public key': + # Try to fetch the public key from the keyservers + try: + self.receive_key(verify.key_id) + return self.verify_w_retry(file_input) + except KeyFetchingError: + return verify + else: + return verify + except IOError: + return False + + def verify_file(self, file_input): + """ + Verify the signature of a file. + """ + if isinstance(file_input, types.StringTypes): + descriptor = open(file_input, 'rb') + elif isinstance(file_input, types.FileType) or isinstance(file_input, File) or isinstance(file_input, StringIO): + descriptor = file_input + else: + raise ValueError('Invalid file_input argument type') + + verify = self.gpg.verify_file(descriptor) + descriptor.close() + + if verify: + return verify + elif getattr(verify, 'status', None) == 'no public key': + # Exception to the rule, to be able to query the keyservers + return verify + else: + raise GPGVerificationError() + + def verify(self, data): + # TODO: try to merge with verify_file + verify = self.gpg.verify(data) + + if verify: + return verify + else: + raise GPGVerificationError(verify.status) + + def sign_file(self, file_input, key=None, destination=None, key_id=None, passphrase=None, clearsign=False): + """ + Signs a filename, storing the signature and the original file + in the destination filename provided (the destination file is + overrided if it already exists), if no destination file name is + provided the signature is returned. + """ + kwargs = {} + kwargs['clearsign'] = clearsign + + if key_id: + kwargs['keyid'] = key_id + + if key: + kwargs['keyid'] = key.key_id + + if passphrase: + kwargs['passphrase'] = passphrase + + if isinstance(file_input, types.StringTypes): + input_descriptor = open(file_input, 'rb') + elif isinstance(file_input, types.FileType) or isinstance(file_input, File): + input_descriptor = file_input + elif issubclass(file_input.__class__, StringIO): + input_descriptor = file_input + else: + raise ValueError('Invalid file_input argument type') + + if destination: + output_descriptor = open(destination, 'wb') + + signed_data = self.gpg.sign_file(input_descriptor, **kwargs) + if not signed_data.fingerprint: + raise GPGSigningError('Unable to sign file') + + if destination: + output_descriptor.write(signed_data.data) + + input_descriptor.close() + + if destination: + output_descriptor.close() + + if not destination: + return signed_data + + def decrypt_file(self, file_input): + if isinstance(file_input, types.StringTypes): + input_descriptor = open(file_input, 'rb') + elif isinstance(file_input, types.FileType) or isinstance(file_input, File) or isinstance(file_input, StringIO): + input_descriptor = file_input + else: + raise ValueError('Invalid file_input argument type') + + result = self.gpg.decrypt_file(input_descriptor) + input_descriptor.close() + if not result.status: + raise GPGDecryptionError('Unable to decrypt file') + + return result + + def create_key(self, *args, **kwargs): + if kwargs.get('passphrase') == u'': + kwargs.pop('passphrase') + + input_data = self.gpg.gen_key_input(**kwargs) + key = self.gpg.gen_key(input_data) + if not key: + raise KeyGenerationError('Unable to generate key') + + return Key.get(self, key.fingerprint) + + def delete_key(self, key): + status = self.gpg.delete_keys(key.fingerprint, key.type == 'sec').status + if status == 'Must delete secret key first': + self.delete_key(Key.get(self, key.fingerprint, secret=True)) + self.delete_key(key) + elif status != 'ok': + raise KeyDeleteError('Unable to delete key') + + def receive_key(self, key_id): + for keyserver in self.keyservers: + import_result = self.gpg.recv_keys(keyserver, key_id) + if import_result: + return Key.get(self, import_result.fingerprints[0], secret=False) + + raise KeyFetchingError diff --git a/apps/django_gpg/conf/__init__.py b/apps/django_gpg/conf/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/django_gpg/conf/settings.py b/apps/django_gpg/conf/settings.py new file mode 100644 index 0000000000..ac9256396a --- /dev/null +++ b/apps/django_gpg/conf/settings.py @@ -0,0 +1,15 @@ +''' +Configuration options for the django_gpg app +''' + +from django.utils.translation import ugettext_lazy as _ + +from smart_settings.api import register_settings + +register_settings( + namespace=u'django_gpg', + module=u'django_gpg.conf.settings', + settings=[ + {'name': u'KEYSERVERS', 'global_name': u'SIGNATURES_KEYSERVERS', 'default': ['keyserver.ubuntu.com'], 'description': _(u'List of keyservers to be queried for unknown keys.')}, + ] +) diff --git a/apps/django_gpg/exceptions.py b/apps/django_gpg/exceptions.py new file mode 100644 index 0000000000..682ad8f795 --- /dev/null +++ b/apps/django_gpg/exceptions.py @@ -0,0 +1,31 @@ +class GPGException(Exception): + pass + + +class GPGVerificationError(GPGException): + pass + + +class GPGSigningError(GPGException): + pass + + +class GPGDecryptionError(GPGException): + pass + + +class KeyDeleteError(GPGException): + pass + + +class KeyGenerationError(GPGException): + pass + + +class KeyFetchingError(GPGException): + pass + + +class KeyDoesNotExist(GPGException): + pass + diff --git a/apps/django_gpg/locale/en/LC_MESSAGES/django.po b/apps/django_gpg/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000000..1192c23608 --- /dev/null +++ b/apps/django_gpg/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,203 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-11-28 05:18-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: api.py:18 +msgid "Public" +msgstr "" + +#: api.py:19 +msgid "Secret" +msgstr "" + +#: api.py:27 api.py:32 +msgid "RSA" +msgstr "" + +#: api.py:28 +msgid "DSA" +msgstr "" + +#: api.py:33 +msgid "Elgamal" +msgstr "" + +#: api.py:89 +msgid "unknown" +msgstr "" + +#: forms.py:12 +msgid "Real name" +msgstr "" + +#: forms.py:13 +msgid "Your real name." +msgstr "" + +#: forms.py:17 +msgid "Comment" +msgstr "" + +#: forms.py:19 +msgid "A comment or a note to help identify this key." +msgstr "" + +#: forms.py:23 +msgid "Email" +msgstr "" + +#: forms.py:28 +msgid "Primary key class" +msgstr "" + +#: forms.py:29 +msgid "The key that will be used to sign uploaded content." +msgstr "" + +#: forms.py:33 +msgid "Primary key size (in bytes)" +msgstr "" + +#: forms.py:41 +msgid "Secondary key class" +msgstr "" + +#: forms.py:42 +msgid "The key that will be used to encrypt uploaded content." +msgstr "" + +#: forms.py:46 +msgid "Secondary key size (in bytes)" +msgstr "" + +#: forms.py:53 +msgid "Expiration" +msgstr "" + +#: forms.py:54 +msgid "" +"You can use 0 for a non expiring key, an ISO date in the form: -" +"- or a date difference from the current date in the forms: " +"d, m, w or y." +msgstr "" + +#: forms.py:59 +msgid "Passphrase" +msgstr "" + +#: forms.py:65 +msgid "Passphrase (verification)" +msgstr "" + +#: forms.py:72 +msgid "Both passphrase fields entries must match." +msgstr "" + +#: forms.py:80 +msgid "Key" +msgstr "" + +#: forms.py:81 +msgid "Key to be published, only the public part of the key will be sent." +msgstr "" + +#: tasks.py:27 +#, python-format +msgid "Key pair: %s, created successfully." +msgstr "" + +#: tasks.py:34 +#, python-format +msgid "Key creation error; %s" +msgstr "" + +#: views.py:27 +msgid "Private key list" +msgstr "" + +#: views.py:30 +msgid "Public key list" +msgstr "" + +#: views.py:54 +msgid "Key pair queued for creation, refresh this page to check results." +msgstr "" + +#: views.py:64 +msgid "Create a new key" +msgstr "" + +#: views.py:65 +msgid "" +"The key creation process can take quite some time to complete, please be " +"patient." +msgstr "" + +#: views.py:75 +#, python-format +msgid "Key: %s, deleted successfully." +msgstr "" + +#: views.py:82 +msgid "Delete key" +msgstr "" + +#: views.py:83 +#, python-format +msgid "" +"Are you sure you wish to delete key:%s? If you try to delete a public key " +"that is part of a public/private pair the private key will be deleted as " +"well." +msgstr "" + +#: views.py:95 +#, python-format +msgid "Key publish request for key: %s, has been sent" +msgstr "" + +#: views.py:98 +msgid "Unable to send key publish call" +msgstr "" + +#: views.py:105 +msgid "Publish a key to the OpenRelay network" +msgstr "" + +#: templates/key_list.html:10 +msgid "ID" +msgstr "" + +#: templates/key_list.html:11 +msgid "User IDs" +msgstr "" + +#: templates/key_list.html:12 +msgid "Fingerprint" +msgstr "" + +#: templates/key_list.html:13 +msgid "Links" +msgstr "" + +#: templates/key_list.html:22 +msgid "Delete" +msgstr "" + +#: templates/key_list.html:26 +msgid "There are no keys available." +msgstr "" diff --git a/apps/django_gpg/locale/es/LC_MESSAGES/django.mo b/apps/django_gpg/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..502545e74a86acadccce3447f66799e3239dca5a GIT binary patch literal 4289 zcmai$O^h5z6~_y32rT5oAp!GU0qoex%y?}QFyna1+FpkhYkTcogE(-g>8_crY)^N) zs(aUCA#vn}P^1WP0O3dogV5p*d;Q0U_PwAO9j=dYed8TUodn-_Cx49JzDubd_!{^<@L%BL;Pd;Gx)=Nf z_#pTjkXPyt;Jd-s!8!0vP}cnwya)VeJO6L+eLUa8q#U&$l>HBb9|RA8i{Mf4)8JK5 z^mqY04gMPZ1b8opajOo2a$X1gF!*_JKX?iJD42kv=Qlxq>O1^B0Dc!d1pXM5^M41v z7rX_20DJ@dIQS2cpSmAoE`g7Lp94qWXTcwU;=fzqC&9mi?*Sj+@2B7aP|o{3$Sd_H z@H)Q!Gq}L>0Oel+Z-9O9t2jsOc^-TO{5~l9-2~a9eg=wtzXCS<8X})e%O!G1XsY1fZqZ& z_#(&>wFBM)Z-M;Oj|j5Je+9(7Q76$`VkmJHE=gP^Mlw2FvR-r+e+s8Q!zJTT0}86D z$G9Hl(p)mcH^Qgsc!(Q`zYK|k_~dbUV2Iykh#w@TGQ?Kl`E-b{BoCh8!iCXSgdh&F zTWpgdToAvD&%}O-MI4JdDaK=)SGw@p=)9<`t_rr9oqXcQ)v89V$IG zRkG1jXBvyu8E@qrthU>tk~+J#sLn=1T8Oht*~pAdrk2LW@qev!-c+v0)j5}stVg@N zQp;!6myy7w6A{aMZM`r2NqsQH>1fW?+SWGJL!3Qb*3sS$y%jf#n@cl_5lA*{vdODZ zXI1FUB=iy2Y4#!~)JbAP7}i-vaOCjlLu7f&QJ5?zVOq0o18)ti&2_*naXGQUne8T< zc65WEtPZfqcn zoRMVgxNRo5>sB9jxgJa^8{+Cn5m}wNP^s0(s}`b=-D$jG;uOY;85c{NU$&W<5cg`k z@SEx~aldR$rgJm4YRx7^p5Cs^?Q?gk!&`4Fwch$@8d->2NO*K&a$T8CtFP7$!i8c? zh@Jcul76M6PDcYmT!{xxiT<5RjJYzUejSKBJ8ho$Nm=Q-%nFk-huYQu-xk~`zxy6i z+HmECcq?eL1>emevpsbs7nDPZdm5cNC0b&#OkD}q>*cdSeT~{6)}o`HkK~h@LMlks zkS?;dqgk-Lc2TF2eFRm40#C<%{q)2dztDYpj3pajY9P;)Q3gEIzxWpI%vcCUd>Qj~3?6FP~r9 zHSt()e(p>|rtbQr#DL0PtB#hLarsG|Y#1MGb?VCcx$cSGb)p~9=`Q6-krL{@J~43B z?DW32VQw|6y~(=g2sZJ_%lH^h9X|>A<;|(XxqkGNK6Ydd%1LSvS+P_Xy3S*FqDvTW zw3*aoD0$uXDC&skfXe7BGg~nB)n6`!YUc)g9o4=d@uX!0YqZVW*yXIw+O<-^Xjp3w zDoXn7St4gfdAi*Bxil^l1qoN99?&1-cv+6b}W-}BRdNz9^(?=N%C8I+s?}(c0e6Z3YRA?bv`z=)>*Ny`KVT-d|ulv z6Pw#oYrLA?%yeU>?Q-2cZ7y*;H*@u+nhwj6ZsaQZ;Bxa>fH%^@s}=FLT9Q^5b%gkF zS5#l##Rj;F!^Iw%5EEn7j?R%{88>wF7UygcTTJ$&X*|oF8zbVSOWFmz!W)<=B@z}$ z?IZ&vn0Tn^l_*8KyzRW*o|#I$rCubsT*UO$T0vM{Rj2LO#~sQ~`=$<#y*A%+goZFp zX(3uj?TghAhnS8IJ2$gJ{ECXA=#&zBiNF@P*@`3V;+E#9mKPAmUULhCC7EIy^SKAhCJUtqg56xD= ztrU|Ix->kRQ9>#W4zDYhx$8oYhN*7o5pFYtNtF>cg?a!;(zk57`_`eO+Puxy3w){b z9n+>&>OxuMj!sPm@Cr2=%OcT85((gxs(i7E6Y5Y)fep0qil8*>r^?g9qxP+Q(T|UV z*vn1ci9sD&+Cl8*lGG;laYO4BZb>7zLE5-8wDGu}79Kmeidt7@Q#1gMDkzwR`X9jL+hza& literal 0 HcmV?d00001 diff --git a/apps/django_gpg/locale/es/LC_MESSAGES/django.po b/apps/django_gpg/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000000..dcc0e1eeab --- /dev/null +++ b/apps/django_gpg/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,217 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Roberto Rosario , 2011. +msgid "" +msgstr "" +"Project-Id-Version: OpenRelay\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-11-28 05:18-0400\n" +"PO-Revision-Date: 2011-11-28 09:25+0000\n" +"Last-Translator: rosarior \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: api.py:18 +msgid "Public" +msgstr "Pública" + +#: api.py:19 +msgid "Secret" +msgstr "Secreta" + +#: api.py:27 api.py:32 +msgid "RSA" +msgstr "RSA" + +#: api.py:28 +msgid "DSA" +msgstr "DSA" + +#: api.py:33 +msgid "Elgamal" +msgstr "Elgamal" + +#: api.py:89 +msgid "unknown" +msgstr "desconocido" + +#: forms.py:12 +msgid "Real name" +msgstr "Nombre real" + +#: forms.py:13 +msgid "Your real name." +msgstr "Su nombre real." + +#: forms.py:17 +msgid "Comment" +msgstr "Comentario" + +#: forms.py:19 +msgid "A comment or a note to help identify this key." +msgstr "Un comentario o una nota para ayudar a identificar esta llave." + +#: forms.py:23 +msgid "Email" +msgstr "E-mail" + +#: forms.py:28 +msgid "Primary key class" +msgstr "Clase de llave primaria" + +#: forms.py:29 +msgid "The key that will be used to sign uploaded content." +msgstr "La llave que se utilizara para firmar el contenido subido." + +#: forms.py:33 +msgid "Primary key size (in bytes)" +msgstr "Tamaño de la llave principal (en bytes)" + +#: forms.py:41 +msgid "Secondary key class" +msgstr "Clase de llave secundaria" + +#: forms.py:42 +msgid "The key that will be used to encrypt uploaded content." +msgstr "La llave que se utilizara para cifrar el contenido subido." + +#: forms.py:46 +msgid "Secondary key size (in bytes)" +msgstr "Tamaño de la llave secundaria (en bytes)" + +#: forms.py:53 +msgid "Expiration" +msgstr "Expiración" + +#: forms.py:54 +msgid "" +"You can use 0 for a non expiring key, an ISO date in the form: " +"-- or a date difference from the current date in the " +"forms: d, m, w or y." +msgstr "" +"Usted puede utilizar 0 para llaves que no expiran, una fecha ISO en la " +"forma: -- o una diferencia de fecha con respecto a la fecha " +"actual en las formas: d, m, w o y." + +#: forms.py:59 +msgid "Passphrase" +msgstr "Frase de contraseña" + +#: forms.py:65 +msgid "Passphrase (verification)" +msgstr "Contraseña (verificación)" + +#: forms.py:72 +msgid "Both passphrase fields entries must match." +msgstr "Las entradas de los campos de contraseña deben coincidir." + +#: forms.py:80 +msgid "Key" +msgstr "Llave" + +#: forms.py:81 +msgid "Key to be published, only the public part of the key will be sent." +msgstr "" +"La llave para ser publicada, sólo la parte pública de la llave será enviada." + +#: tasks.py:27 +#, python-format +msgid "Key pair: %s, created successfully." +msgstr "Par de llaves: %s, creado correctamente." + +#: tasks.py:34 +#, python-format +msgid "Key creation error; %s" +msgstr "Error de creación de llave; %s" + +#: views.py:27 +msgid "Private key list" +msgstr "Lista de llaves privadas" + +#: views.py:30 +msgid "Public key list" +msgstr "Lista de llaves públicas" + +#: views.py:54 +msgid "Key pair queued for creation, refresh this page to check results." +msgstr "" +"Par de llaves en lista de creación, vuelva a cargar esta página " +"periodicamente para comprobar los resultados." + +#: views.py:64 +msgid "Create a new key" +msgstr "Crear una llave nueva" + +#: views.py:65 +msgid "" +"The key creation process can take quite some time to complete, please be " +"patient." +msgstr "" +"El proceso de creación de la llaves puede tomar bastante tiempo en " +"completar, por favor, sea paciente." + +#: views.py:75 +#, python-format +msgid "Key: %s, deleted successfully." +msgstr "Llave: %s, eliminada exitosamente." + +#: views.py:82 +msgid "Delete key" +msgstr "Eliminar la llave" + +#: views.py:83 +#, python-format +msgid "" +"Are you sure you wish to delete key:%s? If you try to delete a public key " +"that is part of a public/private pair the private key will be deleted as " +"well." +msgstr "" +"¿Está seguro que desea eliminar la llave: %s? Si intenta eliminar una llave" +" pública que forma parte de una pareja pública / privada de la llave privada" +" se eliminarán también." + +#: views.py:95 +#, python-format +msgid "Key publish request for key: %s, has been sent" +msgstr "Solicitud publicación de llave: %s, ha sido enviada" + +#: views.py:98 +msgid "Unable to send key publish call" +msgstr "No se puede enviar llave publica" + +#: views.py:105 +msgid "Publish a key to the OpenRelay network" +msgstr "Publicar una llave a la red OpenRelay" + +#: templates/key_list.html:10 +msgid "ID" +msgstr "Identificador" + +#: templates/key_list.html:11 +msgid "User IDs" +msgstr "ID de usuarios" + +#: templates/key_list.html:12 +msgid "Fingerprint" +msgstr "Huella digital" + +#: templates/key_list.html:13 +msgid "Links" +msgstr "Enlaces" + +#: templates/key_list.html:22 +msgid "Delete" +msgstr "Eliminar" + +#: templates/key_list.html:26 +msgid "There are no keys available." +msgstr "No hay llaves disponibles." + + diff --git a/apps/django_gpg/models.py b/apps/django_gpg/models.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/apps/django_gpg/models.py @@ -0,0 +1 @@ + diff --git a/apps/django_gpg/runtime.py b/apps/django_gpg/runtime.py new file mode 100644 index 0000000000..b3303059ff --- /dev/null +++ b/apps/django_gpg/runtime.py @@ -0,0 +1,4 @@ +from django_gpg.api import GPG +from django_gpg.conf.settings import KEYSERVERS + +gpg = GPG(keyservers=KEYSERVERS) diff --git a/apps/django_gpg/static/images/icons/cross.png b/apps/django_gpg/static/images/icons/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..4ee1253736b73693b282fff8b698b445756c0548 GIT binary patch literal 1049 zcmV+!1m^pRP)>Iyb|4W2iTaM&PCXA5iV z5aAKdYEbL@m7lo@tl?`9L_eEVJf!rVjN5lmsUAYdr+MC3oNy~4*P))Tf5gy?%sPBhw zu9!?9V!Vj9^xzgGR$K*4^xrz=EkQ~K#kGD{`>ndR9~-xb1?d(&W@bhgupAJxCAWk8ek(DlV1d*^^cv%*Iq+c zB6gf9255e@KK9&`F!rpTmz+<4vxveXU_Li_#BDj42V*Z9T#~yI;3Ojc#3>MWeG){z zoN)vcuU`_$NdlZj94`Rx-gdXhO?$IJ@wSmlj+>Z(W3&dYCLFVwO(JeJY!WD1nu#Jr#F6J50_hLs zy52%vbQ~z#T3w0|@zn+C!LpgStSs=Gn%s~?v>34slpP(sBJ2ckb%8u%Kgj+~QGwMO zE&CtrFQfWuw?u%d>x+{J#)&*V9sK8J(2i$_rVR{*$>~|v@HNdJV`6N_qg-r9o|XoI zi}UE@Air+&5*es^df-CuSfOHJX|WTaiH+53iE$Vk`yqdWQ$-|CO@$z{o;sCtj4!AL z2B54@kw2`_jp14bStL$Y@{|+^vKZ(XUJ_9C_p{3(<|-mEFH;kZ+9I+gA4IoitOATU z>!Fdg%XcM1Ftb8QRLg4>5xWO)b)xz@2x^nrP)nD^WAxa4;>JU-sH@?bI*6qJ@?#m&b?Q&Z5wQwrkO3Rtr6*bYiBaZ zj=oQ$qy}BnU|H5LJGO7%E(7VjNY2e4$B05P!2Xb}udmNOcmDik(QQm+fa&`0&v~P# z_aA!X{njr!D$1AQY;!Yy*tH8Ao0`6FzId^1*Up_^=ktW_>T5Z->$Zyp|yhNT%IEkFebDraGAQ-^Fz#zimFurPN z2>;yD()*?nQyMVUq`GOs@cUsF7EU6LX~NTu*W2y$dQnhVFyV>y>((ujID6_g!pv`g zJ~;+x0G0cE&_t;<7Q=N>TzTr0Vs{@nfYpP8FpEo&Nsg&TD%y4%fxJpr2la1Rv1M^NcQ%k z_})FNs;^h{X3Jy)-J9P7P81rB+b32Y2*L>XnAa$M^a$~>F|0X!Sn-|E$Db!oz%ND) zcsxkRP;4xYjNt6FCG3oXg~K6*bvJXm7yvUt_O@&7YFMIZ_`w5gI&uWocoO~X9q8-4 zrSJ>uzrd;;J7M^|xP1ILI+xYJFm=?mU(Xt0y*2O53{a6R`|gvx_IKUIhC_#-7Z%}< z<3}g54>iUF~qF~nmrSi>Xe z`0*$LB36H95gH#)J2^$H^8;}-zCL-H*es}ZQBrk_FSUJK+TyD z0=2Tbp^PH-IYu5a#8QT`Dsyyw%F6oAI#7N3G@gx&!X6*TumqqfDePZO*NRJ14oRP5 z`HRBl2bteN{LJTHDR^p|I+E06O`q)rBVu*b^o@V6%!zo5OqI>sZp%fp8 z(t@&i@JL#wZpoSA)3Wu9Bz=yNM+~u)p=?eMb~bU(k7L2gIt<;5qW?fj zij0F7Egp}CQsIqDnP!=@MaCmVDLs}@vUo28WMjG02`R63O%1NzzKu=Q)hPNT3@epJ zCKk_nY)$}%$3$Xq0BzAIN>;3ZMj1!Pba#t)7{Fzx(a_l`pEQ1z6-7mHQu-?^Dxd{@ zj-46Yl<4!a(#l;ws2|r)HToL;m zBaawjDMMN6n63`wHGrofqt-M{m1TeJ-;c_Y64=ATNDd4rS8v*|0i*r>irD8EdBhM) z8Ol-zbs4j>^;~eEVAeV_9u2Vm&=DB@VoS08HU`KYgl*eKjULVu+;-W%bqPlvBC5IVr@I`pe1?m5lgw-#&$pwzi^b$r2=9yilhfv6P|gcTx+) z4U)3y*?EABCuh$hCWWM0BHrSaOIE_%Hl!5yin}Wh9#q6W$H*gwSjteAI&??p%qt_LyDryG}WbhvfCRTEsdl?5%4i2bn}=iprV zo@TW2)#XT}Ad`Er(rx+DC;O;`>tX9uto&F$c5{cEU|&s|)1`!GTZK@m+hZTNak3#k wKlv`;+-?Ir&V#{=kK2;GweytW-2V$O06QNZBly7cQ~&?~07*qoM6N<$f+&d(b^rhX literal 0 HcmV?d00001 diff --git a/apps/django_gpg/static/images/icons/user_silhouette.png b/apps/django_gpg/static/images/icons/user_silhouette.png new file mode 100644 index 0000000000000000000000000000000000000000..109d113eecfb20351acaf6ba3305b070cb592a74 GIT binary patch literal 1087 zcmV-F1i<@=P)Lblp1$)~lH{|@ui^~OGg+SJKD#nCd7gQ^ zG=kcCQvwhsr>-2?J36|Q%jJjxFu%w`{PvdtkmESCzP5Js&h+i~(2)j!OzDO)QZBz! z6qQ6#B-eG?0%Gv8oD0E#kCz6StTckfG-khg{l+c6xhhE#S(d@K^~m@AHerr+?1K;d zIF}UwO;K`ZG%e3cO!A?;2Voz4;K#W%0Ma~xDv+>GmSr+cGkk(x&+G7hu{c2B$2q1} z764>H{OxEk_f7-*@a4!qo1~wKt~GqC&AiE_=I&(A!2>*YX;*nhgR!&SeF_ zdi3zY#h*VnXkcKF0x0BX=nOy1v5tLKPTH_$q zbD02uM{O=IFFpPCSw|}_`siO6d=hl)mBzMJx9fpa&p%bV2*X{gD>LG6aX@dedy5PQ@XBG zZKE10M(pE@Io5gqB=~kK#eV}JOj@d{9!E*uWM_+&oC9a9wL zGCSJoVyVc*rbyqvev69IpG39qz<$mFEow9xn7^=X`|kYQ?Au<}*sUWH&y7!9lojPB z*LMx(1wm20RwK){yZ9kPRTLT=8VZ3S8v=~yd#39+*Jfv*+~1y~d_=qQ+J;OgiA}RSQ5OHzlP)krWka+ar^~7+j_~TBhcn&fJTEBmt1v+%x=+ zl~Sn`#<#I$giS`QFiqnc%L}cdYNY4&&??PllVw@+{45iwS4lmgE{Q@#Q+2AcnLWp8 zEA158&oE7cEwDv7wnn~?C)c(synKPFnN*H7ys>Rr-xNY#|! z#N7a*W+V6y0B|`;6nD2kub%Ip0YEPQ*6maaf>yJ+h?tF&O_l_?kiR`OcP63|e-1L0 z`z.+)/(?P\w+)/$', 'key_delete', (), 'key_delete'), + url(r'^list/private/$', 'key_list', {'secret': True}, 'key_private_list'), + url(r'^list/public/$', 'key_list', {'secret': False}, 'key_public_list'), + url(r'^verify/(?P\d+)/$', 'document_verify', (), 'document_verify'), +) diff --git a/apps/django_gpg/views.py b/apps/django_gpg/views.py new file mode 100644 index 0000000000..9f23d45004 --- /dev/null +++ b/apps/django_gpg/views.py @@ -0,0 +1,113 @@ +from datetime import datetime + +from django.utils.translation import ugettext_lazy as _ +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response, get_object_or_404 +from django.template import RequestContext +from django.contrib import messages +from django.core.urlresolvers import reverse +from django.utils.safestring import mark_safe +from django.conf import settings +from django.template.defaultfilters import force_escape + +from documents.models import Document, RecentDocument +from permissions.api import check_permissions + +from django_gpg.api import Key +from django_gpg.runtime import gpg +from django_gpg.exceptions import GPGVerificationError +from django_gpg import PERMISSION_DOCUMENT_VERIFY + + +def key_list(request, secret=True): + if secret: + object_list = Key.get_all(gpg, secret=True) + title = _(u'Private key list') + else: + object_list = Key.get_all(gpg) + title = _(u'Public key list') + + return render_to_response('key_list.html', { + 'object_list': object_list, + 'title': title, + }, context_instance=RequestContext(request)) + + +def key_delete(request, fingerprint, key_type): + if request.method == 'POST': + try: + secret = key_type == 'sec' + key = Key.get(gpg, fingerprint, secret=secret) + gpg.delete_key(key) + messages.success(request, _(u'Key: %s, deleted successfully.') % fingerprint) + return HttpResponseRedirect(reverse('home_view')) + except Exception, msg: + messages.error(request, msg) + return HttpResponseRedirect(reverse('home_view')) + + return render_to_response('generic_confirm.html', { + 'title': _(u'Delete key'), + 'message': _(u'Are you sure you wish to delete key:%s? If you try to delete a public key that is part of a public/private pair the private key will be deleted as well.') % Key.get(gpg, fingerprint) + }, context_instance=RequestContext(request)) + + +def document_verify(request, document_pk): + check_permissions(request.user, [PERMISSION_DOCUMENT_VERIFY]) + document = get_object_or_404(Document, pk=document_pk) + + RecentDocument.objects.add_document_for_user(request.user, document) + try: + signature = gpg.verify_w_retry(document.open()) + except GPGVerificationError: + signature = None + + signature_states = { + 'signature bad': { + 'text': _(u'Bad signature.'), + 'icon': 'cross.png' + }, + None: { + 'text': _(u'Document not signed or invalid signature.'), + 'icon': 'cross.png' + }, + 'signature error': { + 'text': _(u'Signature error.'), + 'icon': 'cross.png' + }, + 'no public key': { + 'text': _(u'Document is signed but no public key is available for verification.'), + 'icon': 'user_silhouette.png' + }, + 'signature good': { + 'text': _(u'Document is signed, and signature is good.'), + 'icon': 'document_signature.png' + }, + 'signature valid': { + 'text': _(u'Document is signed with a valid signature.'), + 'icon': 'document_signature.png' + }, + } + + signature_state = signature_states.get(getattr(signature, 'status', None)) + + widget = (u'' % (settings.STATIC_URL, signature_state['icon'])) + paragraphs = [ + _(u'Signature status: %s %s') % (mark_safe(widget), signature_state['text']), + ] + + if signature: + paragraphs.extend( + [ + _(u'Signature ID: %s') % signature.signature_id, + _(u'Key ID: %s') % signature.key_id, + _(u'Timestamp: %s') % datetime.fromtimestamp(int(signature.sig_timestamp)), + _(u'Signee: %s') % force_escape(getattr(signature, 'username', u'')), + ] + ) + + return render_to_response('generic_template.html', { + 'title': _(u'signature properties for: %s') % document, + 'object': document, + 'document': document, + 'paragraphs': paragraphs, + }, context_instance=RequestContext(request)) diff --git a/apps/documents/views.py b/apps/documents/views.py index fdd98922c6..2f44085934 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -1155,6 +1155,8 @@ def document_version_list(request, document_pk): check_permissions(request.user, [PERMISSION_DOCUMENT_VIEW]) document = get_object_or_404(Document, pk=document_pk) + RecentDocument.objects.add_document_for_user(request.user, document) + context = { 'object_list': document.versions.order_by('-timestamp'), 'title': _(u'versions for document: %s') % document, diff --git a/requirements/development.txt b/requirements/development.txt index 7dc2ed08bc..ad042365ab 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -20,3 +20,4 @@ django-compressor==1.1 -e git://github.com/rosarior/django-sendfile.git#egg=django-sendfile djangorestframework==0.2.3 South==0.7.3 +python-gnupg==0.2.8 diff --git a/requirements/production.txt b/requirements/production.txt index d0438944d1..9c1b2eae70 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -17,3 +17,4 @@ django-compressor==1.1 -e git://github.com/rosarior/django-sendfile.git#egg=django-sendfile djangorestframework==0.2.3 South==0.7.3 +python-gnupg==0.2.8 diff --git a/settings.py b/settings.py index c31e499fb0..25f316a74c 100644 --- a/settings.py +++ b/settings.py @@ -133,6 +133,7 @@ INSTALLED_APPS = ( 'lock_manager', 'web_theme', 'common', + 'django_gpg', 'pagination', 'dynamic_search', 'filetransfers', diff --git a/urls.py b/urls.py index 38ab8788b2..a4ccd38cef 100644 --- a/urls.py +++ b/urls.py @@ -28,6 +28,7 @@ urlpatterns = patterns('', (r'^project_setup/', include('project_setup.urls')), (r'^project_tools/', include('project_tools.urls')), (r'^api/', include('rest_api.urls')), + (r'^signatures/', include('django_gpg.urls')), ) From 24f443fe1a2fae35fd824206c586dd7d93e5daba Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 06:16:37 -0400 Subject: [PATCH 64/92] Improve document version views navigation and active menu feedback --- apps/documents/__init__.py | 3 ++- apps/documents/views.py | 3 ++- apps/main/__init__.py | 2 +- apps/sources/__init__.py | 4 ++-- apps/sources/urls.py | 4 ++-- apps/sources/views.py | 4 ++-- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/documents/__init__.py b/apps/documents/__init__.py index 750b2953a8..d69858529f 100644 --- a/apps/documents/__init__.py +++ b/apps/documents/__init__.py @@ -75,7 +75,7 @@ register_history_type(HISTORY_DOCUMENT_DELETED) document_list = {'text': _(u'all documents'), 'view': 'document_list', 'famfam': 'page', 'permissions': [PERMISSION_DOCUMENT_VIEW]} document_list_recent = {'text': _(u'recent documents'), 'view': 'document_list_recent', 'famfam': 'page', 'permissions': [PERMISSION_DOCUMENT_VIEW]} -document_create_multiple = {'text': _(u'upload new documents'), 'view': 'document_create_multiple', 'famfam': 'page_add', 'permissions': [PERMISSION_DOCUMENT_CREATE], 'children_view_regex': ['upload']} +document_create_multiple = {'text': _(u'upload new documents'), 'view': 'document_create_multiple', 'famfam': 'page_add', 'permissions': [PERMISSION_DOCUMENT_CREATE], 'children_view_regex': [r'upload_interactive']} document_create_siblings = {'text': _(u'clone metadata'), 'view': 'document_create_siblings', 'args': 'object.id', 'famfam': 'page_copy', 'permissions': [PERMISSION_DOCUMENT_CREATE]} document_view_simple = {'text': _(u'details'), 'view': 'document_view_simple', 'args': 'object.id', 'famfam': 'page', 'permissions': [PERMISSION_DOCUMENT_VIEW]} document_view_advanced = {'text': _(u'properties'), 'view': 'document_view_advanced', 'args': 'object.id', 'famfam': 'page_gear', 'permissions': [PERMISSION_DOCUMENT_VIEW]} @@ -204,6 +204,7 @@ register_top_menu( children_path_regex=[ r'^documents/[^t]', r'^metadata/[^s]', r'comments', r'tags/document', r'grouping/[^s]', r'history/list/for_object/documents' ], + children_view_regex=[r'upload'], children_views=['document_folder_list', 'folder_add_document', 'document_index_list'], position=1 ) diff --git a/apps/documents/views.py b/apps/documents/views.py index 2f44085934..3234a2014e 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -1198,10 +1198,10 @@ def document_version_revert(request, document_version_pk): check_permissions(request.user, [PERMISSION_DOCUMENT_VERSION_REVERT]) previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + document_version = get_object_or_404(DocumentVersion, pk=document_version_pk) if request.method == 'POST': try: - document_version = get_object_or_404(DocumentVersion, pk=document_version_pk) document_version.revert() messages.success(request, _(u'Document version reverted successfully')) except Exception, msg: @@ -1211,6 +1211,7 @@ def document_version_revert(request, document_version_pk): return render_to_response('generic_confirm.html', { 'previous': previous, + 'object': document_version.document, 'title': _(u'Are you sure you wish to revert to this version?'), 'message': _(u'All later version after this one will be deleted too.'), 'form_icon': u'page_refresh.png', diff --git a/apps/main/__init__.py b/apps/main/__init__.py index 582747e0d5..ce22428900 100644 --- a/apps/main/__init__.py +++ b/apps/main/__init__.py @@ -21,7 +21,7 @@ __version_info__ = { 'minor': 11, 'micro': 0, 'releaselevel': 'beta', - 'serial': 2 + 'serial': 3 } diff --git a/apps/sources/__init__.py b/apps/sources/__init__.py index 94d2727c9c..faca4f075c 100644 --- a/apps/sources/__init__.py +++ b/apps/sources/__init__.py @@ -43,7 +43,7 @@ setup_source_transformation_delete = {'text': _(u'delete'), 'view': 'setup_sourc source_list = {'text': _(u'Document sources'), 'view': 'setup_web_form_list', 'famfam': 'page_add', 'children_url_regex': [r'sources/setup']} -upload_interactive_version = {'text': _(u'upload new version'), 'view': 'upload_interactive_version', 'args': 'object.pk', 'famfam': 'page_add', 'permissions': [PERMISSION_DOCUMENT_CREATE]} +upload_version = {'text': _(u'upload new version'), 'view': 'upload_version', 'args': 'object.pk', 'famfam': 'page_add', 'permissions': [PERMISSION_DOCUMENT_CREATE]} register_links(StagingFile, [staging_file_delete]) @@ -66,7 +66,7 @@ register_links(WatchFolder, [setup_web_form_list, setup_staging_folder_list, set register_links(WatchFolder, [setup_source_transformation_list, setup_source_edit, setup_source_delete]) # Document version -register_links(Document, [upload_interactive_version]) +register_links(['document_version_list', 'upload_version', 'document_version_revert'], [upload_version], menu_name='sidebar') register_links(['setup_source_transformation_create', 'setup_source_transformation_edit', 'setup_source_transformation_delete', 'setup_source_transformation_list'], [setup_source_transformation_create], menu_name='sidebar') diff --git a/apps/sources/urls.py b/apps/sources/urls.py index cd42328138..7baf07a1a9 100644 --- a/apps/sources/urls.py +++ b/apps/sources/urls.py @@ -11,8 +11,8 @@ urlpatterns = patterns('sources.views', url(r'^upload/document/new/interactive/(?P\w+)/(?P\d+)/$', 'upload_interactive', (), 'upload_interactive'), url(r'^upload/document/new/interactive/$', 'upload_interactive', (), 'upload_interactive'), - url(r'^upload/document/(?P\d+)/version/interactive/(?P\w+)/(?P\d+)/$', 'upload_interactive', (), 'upload_interactive_version'), - url(r'^upload/document/(?P\d+)/version/interactive/$', 'upload_interactive', (), 'upload_interactive_version'), + url(r'^upload/document/(?P\d+)/version/interactive/(?P\w+)/(?P\d+)/$', 'upload_interactive', (), 'upload_version'), + url(r'^upload/document/(?P\d+)/version/interactive/$', 'upload_interactive', (), 'upload_version'), #Setup views diff --git a/apps/sources/views.py b/apps/sources/views.py index 88e1d08b72..17e8c380a6 100644 --- a/apps/sources/views.py +++ b/apps/sources/views.py @@ -37,7 +37,7 @@ def return_function(obj): def get_tab_link_for_source(source, document=None): if document: - view = u'upload_interactive_version' + view = u'upload_version' args = [document.pk, u'"%s"' % source.source_type, source.pk] else: view = u'upload_interactive' @@ -271,7 +271,7 @@ def upload_interactive(request, source_type=None, source_id=None, document_pk=No 'subtemplates_list': subtemplates_list, 'temporary_navigation_links': { 'form_header': { - 'upload_interactive_version': { + 'upload_version': { 'links': results['tab_links'] }, 'upload_interactive': { From ba5b54e1aaddb79b4ecc6b040c3ad33da05f648d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 06:51:26 -0400 Subject: [PATCH 65/92] Add migration to add the signature state field to the Document model --- ...d_field_documentversion_signature_state.py | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 apps/documents/migrations/0011_auto__add_field_documentversion_signature_state.py diff --git a/apps/documents/migrations/0011_auto__add_field_documentversion_signature_state.py b/apps/documents/migrations/0011_auto__add_field_documentversion_signature_state.py new file mode 100644 index 0000000000..54a039b91f --- /dev/null +++ b/apps/documents/migrations/0011_auto__add_field_documentversion_signature_state.py @@ -0,0 +1,156 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'DocumentVersion.signature_state' + db.add_column('documents_documentversion', 'signature_state', self.gf('django.db.models.fields.CharField')(max_length=16, null=True, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'DocumentVersion.signature_state' + db.delete_column('documents_documentversion', 'signature_state') + + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentVersion']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.documentversion': { + 'Meta': {'unique_together': "(('document', 'major', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'major': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'signature_state': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] From d8f487de7c77d7c1cfaa177bc8c6992260b16a05 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 06:52:03 -0400 Subject: [PATCH 66/92] Move human readable signature state literal from the view to the api module --- apps/django_gpg/api.py | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/apps/django_gpg/api.py b/apps/django_gpg/api.py index 504d9ba664..e5c6ca5f19 100644 --- a/apps/django_gpg/api.py +++ b/apps/django_gpg/api.py @@ -31,6 +31,41 @@ KEY_SECONDARY_CLASSES = ( ((KEY_CLASS_ELG), _(u'Elgamal')), ) +SIGNATURE_STATE_BAD = 'signature bad' +SIGNATURE_STATE_NONE = None +SIGNATURE_STATE_ERROR = 'signature error' +SIGNATURE_STATE_NO_PUBLIC_KEY = 'no public key' +SIGNATURE_STATE_GOOD = 'signature good' +SIGNATURE_STATE_VALID = 'signature valid' + +SIGNATURE_STATES = { + SIGNATURE_STATE_BAD: { + 'text': _(u'Bad signature.'), + 'icon': 'cross.png' + }, + SIGNATURE_STATE_NONE: { + 'text': _(u'Document not signed or invalid signature.'), + 'icon': 'cross.png' + }, + SIGNATURE_STATE_ERROR: { + 'text': _(u'Signature error.'), + 'icon': 'cross.png' + }, + SIGNATURE_STATE_NO_PUBLIC_KEY: { + 'text': _(u'Document is signed but no public key is available for verification.'), + 'icon': 'user_silhouette.png' + }, + SIGNATURE_STATE_GOOD: { + 'text': _(u'Document is signed, and signature is good.'), + 'icon': 'document_signature.png' + }, + SIGNATURE_STATE_VALID: { + 'text': _(u'Document is signed with a valid signature.'), + 'icon': 'document_signature.png' + }, +} + + class Key(object): @staticmethod def get_key_id(fingerprint): @@ -163,9 +198,9 @@ class GPG(object): if verify: return verify - elif getattr(verify, 'status', None) == 'no public key': - # Exception to the rule, to be able to query the keyservers - return verify + #elif getattr(verify, 'status', None) == 'no public key': + # # Exception to the rule, to be able to query the keyservers + # return verify else: raise GPGVerificationError() From 9271ded217a47a9f66ec12ab29c672fac1376a83 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 06:52:51 -0400 Subject: [PATCH 67/92] Move signatute state literals to the api module, inspect a document raw content only, display signature type (embedded, detached) --- apps/django_gpg/views.py | 39 +++++++++------------------------------ 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/apps/django_gpg/views.py b/apps/django_gpg/views.py index 9f23d45004..4a173cac0a 100644 --- a/apps/django_gpg/views.py +++ b/apps/django_gpg/views.py @@ -13,7 +13,7 @@ from django.template.defaultfilters import force_escape from documents.models import Document, RecentDocument from permissions.api import check_permissions -from django_gpg.api import Key +from django_gpg.api import Key, SIGNATURE_STATES from django_gpg.runtime import gpg from django_gpg.exceptions import GPGVerificationError from django_gpg import PERMISSION_DOCUMENT_VERIFY @@ -57,48 +57,27 @@ def document_verify(request, document_pk): RecentDocument.objects.add_document_for_user(request.user, document) try: - signature = gpg.verify_w_retry(document.open()) + signature = gpg.verify_w_retry(document.open(raw=True)) except GPGVerificationError: signature = None - - signature_states = { - 'signature bad': { - 'text': _(u'Bad signature.'), - 'icon': 'cross.png' - }, - None: { - 'text': _(u'Document not signed or invalid signature.'), - 'icon': 'cross.png' - }, - 'signature error': { - 'text': _(u'Signature error.'), - 'icon': 'cross.png' - }, - 'no public key': { - 'text': _(u'Document is signed but no public key is available for verification.'), - 'icon': 'user_silhouette.png' - }, - 'signature good': { - 'text': _(u'Document is signed, and signature is good.'), - 'icon': 'document_signature.png' - }, - 'signature valid': { - 'text': _(u'Document is signed with a valid signature.'), - 'icon': 'document_signature.png' - }, - } - signature_state = signature_states.get(getattr(signature, 'status', None)) + signature_state = SIGNATURE_STATES.get(getattr(signature, 'status', None)) widget = (u'' % (settings.STATIC_URL, signature_state['icon'])) paragraphs = [ _(u'Signature status: %s %s') % (mark_safe(widget), signature_state['text']), ] + if document.signature_state: + signature_type = _(u'embedded') + else: + signature_type = _(u'detached') + if signature: paragraphs.extend( [ _(u'Signature ID: %s') % signature.signature_id, + _(u'Signature type: %s') % signature_type, _(u'Key ID: %s') % signature.key_id, _(u'Timestamp: %s') % datetime.fromtimestamp(int(signature.sig_timestamp)), _(u'Signee: %s') % force_escape(getattr(signature, 'username', u'')), From 7ed3b329dc612c0131aa78cbecbd31b1ba5d04f4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 06:54:03 -0400 Subject: [PATCH 68/92] Fix document main menu highlighting --- apps/documents/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/documents/__init__.py b/apps/documents/__init__.py index d69858529f..26c3e9852d 100644 --- a/apps/documents/__init__.py +++ b/apps/documents/__init__.py @@ -204,8 +204,8 @@ register_top_menu( children_path_regex=[ r'^documents/[^t]', r'^metadata/[^s]', r'comments', r'tags/document', r'grouping/[^s]', r'history/list/for_object/documents' ], - children_view_regex=[r'upload'], - children_views=['document_folder_list', 'folder_add_document', 'document_index_list'], + #children_view_regex=[r'upload'], + children_views=['document_folder_list', 'folder_add_document', 'document_index_list', 'upload_version',], position=1 ) From a32b92b5fcd1d25ad9143f683b8670afde3fd445 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 06:54:41 -0400 Subject: [PATCH 69/92] Add signature state field to the Document model, update it whenever a new version is added, use it to return the raw or the decrypted content when calling the open method --- apps/documents/models.py | 46 ++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index bc08a1945a..4246da2dd5 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -21,10 +21,12 @@ from converter.api import get_page_count from converter.api import get_available_transformations_choices from converter.api import convert from converter.exceptions import UnknownFileFormat, UnkownConvertError -from mimetype.api import get_mimetype, get_icon_file_path, \ - get_error_icon_file_path +from mimetype.api import (get_mimetype, get_icon_file_path, + get_error_icon_file_path, get_encoding) from converter.literals import (DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION, DEFAULT_PAGE_NUMBER) +from django_gpg.runtime import gpg +from django_gpg.exceptions import GPGVerificationError, GPGDecryptionError from documents.conf.settings import CHECKSUM_FUNCTION from documents.conf.settings import UUID_FUNCTION @@ -209,12 +211,12 @@ class Document(models.Model): return new_version # Proxy methods - def open(self): + def open(self, *args, **kwargs): ''' Return a file descriptor to a document's file irrespective of the storage backend ''' - return self.latest_version.open() + return self.latest_version.open(*args, **kwargs) def save_to_file(self, *args, **kwargs): return self.latest_version.save_to_file(*args, **kwargs) @@ -251,6 +253,10 @@ class Document(models.Model): def checksum(self): return self.latest_version.checksum + @property + def signature_state(self): + return self.latest_version.signature_state + @property def pages(self): return self.latest_version.pages @@ -309,7 +315,8 @@ class DocumentVersion(models.Model): encoding = models.CharField(max_length=64, default='', editable=False) filename = models.CharField(max_length=255, default=u'', editable=False, db_index=True) checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False) - + signature_state = models.CharField(blank=True, null=True, max_length=16, verbose_name=_(u'signature state'), editable=False) + class Meta: unique_together = ('document', 'major', 'minor', 'micro', 'release_level', 'serial') verbose_name = _(u'document version') @@ -371,6 +378,7 @@ class DocumentVersion(models.Model): if new_document: #Only do this for new documents + self.update_signed_state(save=False) self.update_checksum(save=False) self.update_mimetype(save=False) self.save() @@ -443,6 +451,18 @@ class DocumentVersion(models.Model): ''' for version in self.document.versions.filter(timestamp__gt=self.timestamp): version.delete() + + def update_signed_state(self, save=True): + if self.exists(): + try: + self.signature_state = gpg.verify_w_retry(self.open()).status + # TODO: give use choice for auto public key fetch? + # OR maybe new config option + except GPGVerificationError: + self.signature_state = None + + if save: + self.save() def update_mimetype(self, save=True): ''' @@ -470,12 +490,24 @@ class DocumentVersion(models.Model): ''' return self.file.storage.exists(self.file.path) - def open(self): + def open(self, raw=False): ''' Return a file descriptor to a document version's file irrespective of the storage backend ''' - return self.file.storage.open(self.file.path) + if self.signature_state and not raw: + try: + result = gpg.decrypt_file(self.file.storage.open(self.file.path)) + # gpg return a string, turn it into a file like object + container = StringIO() + container.write(result.data) + container.seek(0) + return container + except GPGDecryptionError: + # At least return the original raw content + return self.file.storage.open(self.file.path) + else: + return self.file.storage.open(self.file.path) def save_to_file(self, filepath, buffer_size=1024 * 1024): ''' From 292d3ffc3d053bbd102f5acf9cfbeb8f267b7013 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 07:28:02 -0400 Subject: [PATCH 70/92] Dont import deprecated 'get_encoding', simplify decrypted data encapsulation --- apps/documents/models.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index 4246da2dd5..731b53d827 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -22,7 +22,7 @@ from converter.api import get_available_transformations_choices from converter.api import convert from converter.exceptions import UnknownFileFormat, UnkownConvertError from mimetype.api import (get_mimetype, get_icon_file_path, - get_error_icon_file_path, get_encoding) + get_error_icon_file_path) from converter.literals import (DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION, DEFAULT_PAGE_NUMBER) from django_gpg.runtime import gpg @@ -499,10 +499,7 @@ class DocumentVersion(models.Model): try: result = gpg.decrypt_file(self.file.storage.open(self.file.path)) # gpg return a string, turn it into a file like object - container = StringIO() - container.write(result.data) - container.seek(0) - return container + return StringIO(result.data) except GPGDecryptionError: # At least return the original raw content return self.file.storage.open(self.file.path) From f5211313b7f356d54a150c7ba27024ba67697075 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 13:35:15 -0400 Subject: [PATCH 71/92] Dont fetch public keys when creating document version --- apps/documents/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index 731b53d827..d90665d333 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -455,7 +455,7 @@ class DocumentVersion(models.Model): def update_signed_state(self, save=True): if self.exists(): try: - self.signature_state = gpg.verify_w_retry(self.open()).status + self.signature_state = gpg.verify(self.open()).status # TODO: give use choice for auto public key fetch? # OR maybe new config option except GPGVerificationError: From 7e5be301cda672381f04b45f673b0d2ea2df3057 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 13:35:56 -0400 Subject: [PATCH 72/92] Add 'python-hkp' to the requirements --- requirements/development.txt | 1 + requirements/production.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements/development.txt b/requirements/development.txt index ad042365ab..d314f11a82 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -21,3 +21,4 @@ django-compressor==1.1 djangorestframework==0.2.3 South==0.7.3 python-gnupg==0.2.8 +python-hkp==0.1.3 diff --git a/requirements/production.txt b/requirements/production.txt index 9c1b2eae70..cfac3f7573 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -18,3 +18,4 @@ django-compressor==1.1 djangorestframework==0.2.3 South==0.7.3 python-gnupg==0.2.8 +python-hkp==0.1.3 From 8b3e391e0505027ca0df3347e3ccf7585d04eb75 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 13:36:47 -0400 Subject: [PATCH 73/92] Change default keyserver --- apps/django_gpg/conf/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/django_gpg/conf/settings.py b/apps/django_gpg/conf/settings.py index ac9256396a..696447b94a 100644 --- a/apps/django_gpg/conf/settings.py +++ b/apps/django_gpg/conf/settings.py @@ -10,6 +10,6 @@ register_settings( namespace=u'django_gpg', module=u'django_gpg.conf.settings', settings=[ - {'name': u'KEYSERVERS', 'global_name': u'SIGNATURES_KEYSERVERS', 'default': ['keyserver.ubuntu.com'], 'description': _(u'List of keyservers to be queried for unknown keys.')}, + {'name': u'KEYSERVERS', 'global_name': u'SIGNATURES_KEYSERVERS', 'default': ['pool.sks-keyservers.net'], 'description': _(u'List of keyservers to be queried for unknown keys.')}, ] ) From 553d73020dae12db428d0cace7188ddf1487edb6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 13:37:24 -0400 Subject: [PATCH 74/92] Add key management view to the setup menu, add keyserver query view, add keyserver key import, add respective icons --- apps/django_gpg/__init__.py | 29 +++- apps/django_gpg/api.py | 39 ++++- apps/django_gpg/exceptions.py | 3 + apps/django_gpg/forms.py | 13 ++ apps/django_gpg/static/images/icons/key.png | Bin 0 -> 1621 bytes .../static/images/icons/key_add.png | Bin 0 -> 2085 bytes .../static/images/icons/key_delete.png | Bin 0 -> 2072 bytes apps/django_gpg/urls.py | 3 + apps/django_gpg/views.py | 159 ++++++++++++++++-- 9 files changed, 229 insertions(+), 17 deletions(-) create mode 100644 apps/django_gpg/forms.py create mode 100644 apps/django_gpg/static/images/icons/key.png create mode 100644 apps/django_gpg/static/images/icons/key_add.png create mode 100644 apps/django_gpg/static/images/icons/key_delete.png diff --git a/apps/django_gpg/__init__.py b/apps/django_gpg/__init__.py index de9aeeba41..7fe12a221c 100644 --- a/apps/django_gpg/__init__.py +++ b/apps/django_gpg/__init__.py @@ -7,13 +7,40 @@ from navigation.api import register_links, register_top_menu, \ from main.api import register_diagnostic, register_maintenance_links from permissions.api import register_permission, set_namespace_title from project_setup.api import register_setup +from hkp import Key as KeyServerKey + +from django_gpg.api import Key PERMISSION_DOCUMENT_VERIFY = {'namespace': 'django_gpg', 'name': 'document_verify', 'label': _(u'Verify document signatures')} +PERMISSION_KEY_VIEW = {'namespace': 'django_gpg', 'name': 'key_view', 'label': _(u'View keys')} +PERMISSION_KEY_DELETE = {'namespace': 'django_gpg', 'name': 'key_delete', 'label': _(u'Delete keys')} +PERMISSION_KEYSERVER_QUERY = {'namespace': 'django_gpg', 'name': 'keyserver_query', 'label': _(u'Query keyservers')} +PERMISSION_KEY_RECEIVE = {'namespace': 'django_gpg', 'name': 'key_receive', 'label': _(u'Import key from keyservers')} # Permission setup set_namespace_title('django_gpg', _(u'Signatures')) register_permission(PERMISSION_DOCUMENT_VERIFY) +register_permission(PERMISSION_KEY_VIEW) +register_permission(PERMISSION_KEY_DELETE) +register_permission(PERMISSION_KEYSERVER_QUERY) +register_permission(PERMISSION_KEY_RECEIVE) -document_verify = {'text': _(u'Signatures'), 'view': 'document_verify', 'args': 'object.pk', 'famfam': 'text_signature', 'permissions': [PERMISSION_DOCUMENT_VERIFY]} +# Setup views +private_keys = {'text': _(u'private keys'), 'view': 'key_private_list', 'args': 'object.pk', 'famfam': 'key', 'icon': 'key.png', 'permissions': [PERMISSION_KEY_VIEW]} +public_keys = {'text': _(u'public keys'), 'view': 'key_public_list', 'args': 'object.pk', 'famfam': 'key', 'icon': 'key.png', 'permissions': [PERMISSION_KEY_VIEW]} +key_delete = {'text': _(u'delete'), 'view': 'key_delete', 'args': ['object.fingerprint', 'object.type'], 'famfam': 'key_delete', 'permissions': [PERMISSION_KEY_DELETE]} +key_query = {'text': _(u'Query keyservers'), 'view': 'key_query', 'famfam': 'zoom', 'permissions': [PERMISSION_KEYSERVER_QUERY]} +key_receive = {'text': _(u'Import'), 'view': 'key_receive', 'args': 'object.keyid', 'famfam': 'key_add', 'keep_query': True, 'permissions': [PERMISSION_KEY_RECEIVE]} + +# Document views +document_verify = {'text': _(u'signatures'), 'view': 'document_verify', 'args': 'object.pk', 'famfam': 'text_signature', 'permissions': [PERMISSION_DOCUMENT_VERIFY]} register_links(Document, [document_verify], menu_name='form_header') + +register_links(['key_delete', 'key_private_list', 'key_public_list', 'key_query'], [private_keys, public_keys, key_query], menu_name='sidebar') + +register_links(Key, [key_delete]) +register_links(KeyServerKey, [key_receive]) + +register_setup(private_keys) +register_setup(public_keys) diff --git a/apps/django_gpg/api.py b/apps/django_gpg/api.py index e5c6ca5f19..50ca3d564c 100644 --- a/apps/django_gpg/api.py +++ b/apps/django_gpg/api.py @@ -1,16 +1,20 @@ import types from StringIO import StringIO from pickle import dumps - -import gnupg +import logging from django.core.files.base import File from django.utils.translation import ugettext_lazy as _ +from django.utils.http import urlquote_plus -from django_gpg.exceptions import GPGVerificationError, GPGSigningError, \ - GPGDecryptionError, KeyDeleteError, KeyGenerationError, \ - KeyFetchingError, KeyDoesNotExist +from hkp import KeyServer +import gnupg +from django_gpg.exceptions import (GPGVerificationError, GPGSigningError, + GPGDecryptionError, KeyDeleteError, KeyGenerationError, + KeyFetchingError, KeyDoesNotExist, KeyImportError) + +logger = logging.getLogger(__name__) KEY_TYPES = { 'pub': _(u'Public'), @@ -31,6 +35,8 @@ KEY_SECONDARY_CLASSES = ( ((KEY_CLASS_ELG), _(u'Elgamal')), ) +KEYSERVER_DEFAULT_PORT = 11371 + SIGNATURE_STATE_BAD = 'signature bad' SIGNATURE_STATE_NONE = None SIGNATURE_STATE_ERROR = 'signature error' @@ -300,3 +306,26 @@ class GPG(object): return Key.get(self, import_result.fingerprints[0], secret=False) raise KeyFetchingError + + def query(self, term): + results = {} + for keyserver in self.keyservers: + url = u'http://%s' % keyserver + server = KeyServer(url) + try: + key_list = server.search(term) + for key in key_list: + results[key.keyid] = key + except: + pass + + return results.values() + + def import_key(self, key_data): + import_result = self.gpg.import_keys(key_data) + logger.debug('import_result: %s' % import_result) + + if import_result: + return Key.get(self, import_result.fingerprints[0], secret=False) + + raise KeyImportError diff --git a/apps/django_gpg/exceptions.py b/apps/django_gpg/exceptions.py index 682ad8f795..52e62315b0 100644 --- a/apps/django_gpg/exceptions.py +++ b/apps/django_gpg/exceptions.py @@ -29,3 +29,6 @@ class KeyFetchingError(GPGException): class KeyDoesNotExist(GPGException): pass + +class KeyImportError(GPGException): + pass diff --git a/apps/django_gpg/forms.py b/apps/django_gpg/forms.py new file mode 100644 index 0000000000..619035fd5d --- /dev/null +++ b/apps/django_gpg/forms.py @@ -0,0 +1,13 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext +from django.core.urlresolvers import reverse +from django.utils.safestring import mark_safe +from django.conf import settings + + +class KeySearchForm(forms.Form): + term = forms.CharField( + label=_(u'Term'), + help_text=_(u'Name, e-mail, key ID or key fingerprint to look for.') + ) diff --git a/apps/django_gpg/static/images/icons/key.png b/apps/django_gpg/static/images/icons/key.png new file mode 100644 index 0000000000000000000000000000000000000000..3cf460e1126ce5c67b2fc0d4166eb4bc6f409e20 GIT binary patch literal 1621 zcmV-b2CDgqP)xa?aXJaW%DaHEUTZf}$c! zMX=xs$c1|^_g=Vgzt1`S-g7U?lr=u?=3y^;;eF3}e$Vs!J-_oFPE}Pr&Wp!mzC`wm zl({P{)+n?)Y4sj33KK}0wdCyIPXQTbo4I|8NeeS4$tksDbG#u)bD=&wa71 zaOFD49cK}9RcpjKPSq6PIT?x^gA$8Ew9kPq?{(Cl{2XnKb%$4cT>TbPVys2TeYN4a z1y-xMtZ-Q=3J&)q*MZ z@wh;;oA}L7EqVj!y$Ct%)4;*&Ac!Kot^rj4>F7Los(H(S(|5MiR=GR9Ju2j=2eRug zJiV|4Z!UiF5UU??i~&%Qb1!A*7eMj+2u{#}qaT%*!SO2YHo0)NzH9q8=kETYZPxL@ zoF`2CvnEVH)K!Ivxur-GxuvvZ7^?z0DjvNk`3O`br}1e2w+Xx;(Og6zqB*37619>u3TX@Y!o@M(*&U4WB?y>^)b%fGsB%q2K^w z;Si*>G^FX$sLrHBL6E?5EQVM#M0F5B(=`X~wfK8h?5HWG#TFV~apcU42pj~y1Nux8 z+_ig&wN)#n$Up$WU=T*xq&Y&3gjmuwr$z>%=ymtOakB$1Pq<}zPcZJlLaP=9X_`Uq5c4|7T|LR;Hd8+V2BLq;adHamJf9?JUwB%K7Wvk#XN;P29XE|!%c z&ry<=3ny8tj~vN{1uvg+*;dI3rNW_|>roz(a3dAm!9%y(cnOon` z6VHgwgb#xsEiYVSHLApXSCV;!mUFYkLd@M-A1~f@ZCR30JPF!4TJYfl;E!&YNz4@j z->rdnU)ihW++?*<2kW689)iGYMcbMLRFm?CMhpIM0Q}MQ55SF?26Qv4naoJfB<5Z> zsw%HTq!*|^tXxPIl{+QJ@@E?@@(Yr1r3$tLo7MjBj~_aKdAH<~lx?sDSNsoRuy0p3u&Dk*(;Qu34qV6;xm zgg$x(zJ_j?Y&MwE3EbI5-9#a99>u$_4>Ipg`WmSWKI=p*XC zQ2UM{rZnk}bDWlBfFnoM7!t84#N4?s%zP592j4{RwU&L4ywdaw#sr^f%>TUZ-bHr1 zbxq~+TBtYPM%>o|PBem-OuCHt;%GhVMr-pZM*JL9|7Aq3?8Kbvr(rQ0pMGjlaV}Go zsR2^1%y0b8;-3NjGf=}LIyiU(1W|-f38LwCVMPzhR8foRa7`AiK+mU*B{uXSChl1gV)qPyc7%E*ZM%c^E}wYiS5 z`YNp`fU{6WJVupAEXBueAQnp`n@Hxpk1{(RXH+CjNZNpsax$X*D#~V6Ab&)Crf;M%TA7fWYU}ILzF>#p_;&k-Ya@cnglAQiLXg zh=oHiWEo+Q3=k!W-GVy7YAl9$Bt$t7LC1w|^mmPVSH9L%O`A7JeB$&yuOe^|_-ioQ zWVo9f$ZNZvN|9iIjF_N=OF%HO1c?$GlxAc^bRq)3dkj5S`p~b0&Ofp1?6dR;H3G0c zp-#sDe*cE=*Eme%`Pyv+Zif59E*Rw;$TmBC?jg#903?Z8@C^m;x3%Gvu?uZN0%=}Z z8kDJ==Tx)y3ZtFGOA?#TZI}dSw6A|1CYuHBi@OQj21`yZfh!0H$6=t_4h{sd`^!&p z)fvRfdskyl$q!*Nm~Lk{s`^#*kDfu>&_N832X#dSl%Q3GkE1weA>R7e z77V#Pdq4gye)&yR!0%trz%dEVXt(4bJJ*R1b{!?kB?wg4^oi@5$8)sA<=p(}Ry6fA zKnb`Ip=#W@Qqz?^`w^!%{Q_$-7M47ZhQlvmsB3)5$v1FnsvO||w!X5)Zq^9Ap1{rI z`A$9N)|?#j+^zTV>iUbzQ*q?y{RS`9-B;^u?cWFAgo{|AF2jATY3gfU+G`oui{|8yel#(ulznGwY|lk!it=QdAFz;J$Kuz(*QUX@!)!BQn3bArg@q9OJ~vJu zxCD{7^oO<2Sq+o8Sz8uKu@Y+5?}Yk?wOc6N^OMQ7Uw^`3n9imMGKiBgpIw_{Of)Wj zrk=o==R06DQ^Mtt=c8V__=;^U57YSpmfUHE8I9x7USeh%gCw6kn=*h)8<1tU zNR3N3P@QLIK{@vaqTL3wH5=}sL40xOI>lXpFBCtWqJEepbaRaT;eIMR;KdUh*i9Rz zB;4`ir036nOs$%bBm!q=*x7X6rwuUM^KCFjk6`5TFl2`VvW370he&&Zz%V{<76;xb%GW0@(Dje3n+{~r8Z}BCoK5UsktX&Gdp6w%?DS(M ziG7N&u}Q*{>feE>=0lRZmTp2reiitI&k_NDwv8kCD=*HpCw&6L( zXxad#L}q8F8a`497fQd){O@c;Vr7%pl8tyZw;0tG<A7LYzQN9d|N*18bkc~OeCQXcv;d1X6_+#gBuoCVk@n;9-(RoXOZUE8%Vt&~!>E<=p9jC7ky zX`@c48$~MxN}3QxnmTF7J5FrJcASSD+xOm`@7iuaM3fpF9n0}|?stCYoZoqv({&vj z$KfVl+&`XPxzZ}(Ns-6*bzOsO5s`?e0iKU*I{*HK-ii8W4qu90^WZH2AU6GCMWbT3 zes9^@hjE*?8nCzlx|iONiUl$9*(dnoqkkfiO#W%(ug`3w1NBw`kpA_{Z}%^#TvL4K z#x3B|5u^j>p-vBhMS){tDTc|j0=neF#di;2(ARqG!QY(SM9-#g7J%^f)75`mTD@*d z`9oWw4xT_NaK><)`!5n%jdsELSS9J~pFC?Xt~!kG_yhYuXheBUPgXw4lBj6Y(Y0Epz=cZ!#Ip@oiv6HMUfPv=!|ypBF!04Lf;p8w0qzW*4? zCVsl)cI8#iym?3lPNS%_9%hkSPe+z~BVZ!($f6V>qasxrkAZFxNJzl`btuZ)XR}2*ir@m5X{V z4V+;0RqDMBo?->*6jdHYDIL3vR7%SKrtN zkW$78Oaw&7rkPqYUjTf=*2;$z6TiUjEI?v%lD-k3`y0U*ZzAu=Q%$9Gc!?7oWLGfG z898}TLW-g`&=o;%VCpDqL|?ZP@_&2cn{{r5J9yuY-$9OlfT`Go;i+s>z_YtbHnILn z0T!V8-$EuR8tG_ACMGf58$d^^e~dceS~}?YIsnM{@;aB5JGgrLi;&6t(5J7#RZ@!h z^b}HwI3%+fW|NtCCXqJd22VUmAw83zpd{dH?!{0~bgcG;vo&l5mY0N{`8s;F~W$n$M4k#Q@6pYDUK6hd)05e|+ZJ~;uwEWkf7g1&$EL+YLZHiq=WuUvzb={dF2{N-b!40 zeLte3fj@6J+ws%uRRMow2Q!XY<1Fm9LKL_?cxT`HkceoU$QgM~jwC}x-B!=tKi`GK zyGM~4>ZR{l-_KS#RRFOO(j8@xYk!Eg*Y?2|2(5XnrR%?Qae#kwM|qvos*~~EWZX)g zzhdOvURX$;2aP^nv%BSiY##Ze6^nmWR=?Sk{IH3}?GSib&a_439AgWL2z7J_$&>G) z^!^&e8sB)H4%Ug+7~g&;Gndk3i<5;}P__kT&R)+^psnZ((gFRL$4M7TooJW8HZGZ9FA<4dg@NPn0lG3y%TIvSx-FMoASGab@3o}u>~0JGC3HLTr5 zbzWdb;FI6OOkua$3lJRf{|G5+WSmFMo(nn8nemA$Cu1zFwnXEJ zDo-I@ZkCO4I|l`fYU8OoLeX)sXJAk>yJC;|$MN32le54XViHEU$h7wHoStW1$rFL- z%SBfCaFNGjOgxQ#56lq&8V{MW?DGe~o?*}C`hb)ESw!NTBW-Mx2xa^C#9aF!$?bow z_U`q%3Tmq>?#c`zx>wJl;IhuJ#i038JAC2T@kg6|Pq2vQ0I*9GwrIQGq1$ zf4s10Rc(oDt6LP7>sKHgORE=;`@?@Y+#g^iJj5z8pM~VBK(HvfNXUKIvfm*)cqX%{ zWc_36Mut&dt7\d+)/$', 'document_verify', (), 'document_verify'), + url(r'^query/$', 'key_query', (), 'key_query'), + url(r'^receive/(?P.+)/$', 'key_receive', (), 'key_receive'), + ) diff --git a/apps/django_gpg/views.py b/apps/django_gpg/views.py index 4a173cac0a..a4f43bfbcb 100644 --- a/apps/django_gpg/views.py +++ b/apps/django_gpg/views.py @@ -1,4 +1,5 @@ from datetime import datetime +import logging from django.utils.translation import ugettext_lazy as _ from django.http import HttpResponseRedirect @@ -12,45 +13,181 @@ from django.template.defaultfilters import force_escape from documents.models import Document, RecentDocument from permissions.api import check_permissions - +from common.utils import pretty_size, parse_range, urlquote, \ + return_diff, encapsulate + from django_gpg.api import Key, SIGNATURE_STATES from django_gpg.runtime import gpg -from django_gpg.exceptions import GPGVerificationError -from django_gpg import PERMISSION_DOCUMENT_VERIFY +from django_gpg.exceptions import GPGVerificationError, KeyFetchingError +from django_gpg import (PERMISSION_DOCUMENT_VERIFY, PERMISSION_KEY_VIEW, + PERMISSION_KEY_DELETE, PERMISSION_KEYSERVER_QUERY, + PERMISSION_KEY_RECEIVE) +from django_gpg.forms import KeySearchForm +logger = logging.getLogger(__name__) + + +def key_receive(request, key_id): + check_permissions(request.user, [PERMISSION_KEY_RECEIVE]) + + post_action_redirect = None + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', '/'))) + + if request.method == 'POST': + try: + term = request.GET.get('term') + results = gpg.query(term) + keys_dict = dict([(key.keyid, key) for key in results]) + key = gpg.import_key(keys_dict[key_id].key) + messages.success(request, _(u'Key: %s, imported successfully.') % key) + return HttpResponseRedirect(next) + except (KeyFetchingError, KeyError, TypeError): + messages.error(request, _(u'Unable to import key id: %s') % key_id) + return HttpResponseRedirect(previous) + + return render_to_response('generic_confirm.html', { + 'title': _(u'Import key'), + 'message': _(u'Are you sure you wish to import key id: %s?') % key_id, + 'form_icon': 'key_add.png', + 'next': next, + 'previous': previous, + 'submit_method': 'GET', + + }, context_instance=RequestContext(request)) + def key_list(request, secret=True): + check_permissions(request.user, [PERMISSION_KEY_VIEW]) + if secret: object_list = Key.get_all(gpg, secret=True) - title = _(u'Private key list') + title = _(u'private keys') else: object_list = Key.get_all(gpg) - title = _(u'Public key list') + title = _(u'public keys') - return render_to_response('key_list.html', { + return render_to_response('generic_list.html', { 'object_list': object_list, 'title': title, + 'hide_object': True, + 'extra_columns': [ + { + 'name': _(u'Key ID'), + 'attribute': 'key_id', + }, + { + 'name': _(u'Owner'), + 'attribute': encapsulate(lambda x: u', '.join(x.uids)), + }, + ] }, context_instance=RequestContext(request)) def key_delete(request, fingerprint, key_type): + check_permissions(request.user, [PERMISSION_KEY_DELETE]) + + secret = key_type == 'sec' + key = Key.get(gpg, fingerprint, secret=secret) + + post_action_redirect = None + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', '/'))) + if request.method == 'POST': try: - secret = key_type == 'sec' - key = Key.get(gpg, fingerprint, secret=secret) gpg.delete_key(key) messages.success(request, _(u'Key: %s, deleted successfully.') % fingerprint) - return HttpResponseRedirect(reverse('home_view')) + return HttpResponseRedirect(next) except Exception, msg: messages.error(request, msg) - return HttpResponseRedirect(reverse('home_view')) + return HttpResponseRedirect(previous) return render_to_response('generic_confirm.html', { 'title': _(u'Delete key'), - 'message': _(u'Are you sure you wish to delete key:%s? If you try to delete a public key that is part of a public/private pair the private key will be deleted as well.') % Key.get(gpg, fingerprint) + 'delete_view': True, + 'message': _(u'Are you sure you wish to delete key: %s? If you try to delete a public key that is part of a public/private pair the private key will be deleted as well.') % key, + 'form_icon': 'key_delete.png', + 'next': next, + 'previous': previous, }, context_instance=RequestContext(request)) +def key_query(request): + check_permissions(request.user, [PERMISSION_KEYSERVER_QUERY]) + + subtemplates_list = [] + term = request.GET.get('term') + + form = KeySearchForm(initial={'term': term}) + subtemplates_list.append( + { + 'name': 'generic_form_subtemplate.html', + 'context': { + 'title': _(u'Query key server'), + 'form': form, + 'submit_method': 'GET', + }, + } + ) + + if term: + results = gpg.query(term) + subtemplates_list.append( + { + 'name': 'generic_list_subtemplate.html', + 'context': { + 'title': _(u'results'), + 'object_list': results, + 'hide_object': True, + 'extra_columns': [ + { + 'name': _(u'ID'), + 'attribute': 'keyid', + }, + { + 'name': _(u'type'), + 'attribute': 'algo', + }, + { + 'name': _(u'creation date'), + 'attribute': 'creation_date', + }, + { + 'name': _(u'disabled'), + 'attribute': 'disabled', + }, + { + 'name': _(u'expiration date'), + 'attribute': 'expiration_date', + }, + { + 'name': _(u'expired'), + 'attribute': 'expired', + }, + { + 'name': _(u'length'), + 'attribute': 'key_length', + }, + { + 'name': _(u'revoked'), + 'attribute': 'revoked', + }, + + { + 'name': _(u'Identifies'), + 'attribute': encapsulate(lambda x: u', '.join([identity.uid for identity in x.identities])), + }, + ] + }, + } + ) + + return render_to_response('generic_form.html', { + 'subtemplates_list': subtemplates_list, + }, context_instance=RequestContext(request)) + + def document_verify(request, document_pk): check_permissions(request.user, [PERMISSION_DOCUMENT_VERIFY]) document = get_object_or_404(Document, pk=document_pk) From 0adcf584f3df523872321f2a23ddbb8afc44bb9b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 13:40:25 -0400 Subject: [PATCH 75/92] Update text mapping to avoid ugettext from complaining --- apps/django_gpg/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/django_gpg/views.py b/apps/django_gpg/views.py index a4f43bfbcb..de07ade6fc 100644 --- a/apps/django_gpg/views.py +++ b/apps/django_gpg/views.py @@ -202,7 +202,10 @@ def document_verify(request, document_pk): widget = (u'' % (settings.STATIC_URL, signature_state['icon'])) paragraphs = [ - _(u'Signature status: %s %s') % (mark_safe(widget), signature_state['text']), + _(u'Signature status: %(widget)s %(text)s') % { + 'widget': mark_safe(widget), + 'text': signature_state['text'] + ), ] if document.signature_state: From 5464ee254440be4da6cfbd72bc74cc1917942906 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 13:55:04 -0400 Subject: [PATCH 76/92] Updated language script to include django_gpg app --- misc/compilemessages_all.sh | 5 +++++ misc/makemessages_all.sh | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/misc/compilemessages_all.sh b/misc/compilemessages_all.sh index 4ad10e85ae..5e3c5cf2a7 100755 --- a/misc/compilemessages_all.sh +++ b/misc/compilemessages_all.sh @@ -107,3 +107,8 @@ cd $BASE/apps/web_theme $COMPILEMESSAGES -l pt $COMPILEMESSAGES -l ru $COMPILEMESSAGES -l es + +cd $BASE/apps/django_gpg +$COMPILEMESSAGES -l pt +$COMPILEMESSAGES -l ru +$COMPILEMESSAGES -l es diff --git a/misc/makemessages_all.sh b/misc/makemessages_all.sh index 79a1354cee..d1e0fcd79e 100755 --- a/misc/makemessages_all.sh +++ b/misc/makemessages_all.sh @@ -128,3 +128,9 @@ $MAKEMESSAGES -l en $MAKEMESSAGES -l pt $MAKEMESSAGES -l ru $MAKEMESSAGES -l es + +cd $BASE/apps/django_gpg +$MAKEMESSAGES -l en +$MAKEMESSAGES -l pt +$MAKEMESSAGES -l ru +$MAKEMESSAGES -l es From 3b943af6d81eae5a95e46fda4d3988498805ff9a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 13:55:33 -0400 Subject: [PATCH 77/92] Update translation files --- apps/common/locale/es/LC_MESSAGES/django.mo | Bin 5534 -> 5431 bytes apps/common/locale/pt/LC_MESSAGES/django.mo | Bin 5385 -> 5282 bytes .../converter/locale/pt/LC_MESSAGES/django.mo | Bin 18867 -> 18828 bytes .../locale/en/LC_MESSAGES/django.po | 292 +++++++++------ .../locale/es/LC_MESSAGES/django.mo | Bin 4289 -> 1619 bytes .../locale/es/LC_MESSAGES/django.po | 353 ++++++++++-------- .../locale/pt/LC_MESSAGES/django.mo | Bin 0 -> 524 bytes .../locale/pt/LC_MESSAGES/django.po | 255 +++++++++++++ .../locale/ru/LC_MESSAGES/django.mo | Bin 0 -> 595 bytes .../locale/ru/LC_MESSAGES/django.po | 255 +++++++++++++ .../locale/es/LC_MESSAGES/django.mo | Bin 1838 -> 1799 bytes .../locale/pt/LC_MESSAGES/django.mo | Bin 1827 -> 1788 bytes .../locale/ru/LC_MESSAGES/django.mo | Bin 2228 -> 2189 bytes .../locale/es/LC_MESSAGES/django.mo | Bin 4191 -> 4152 bytes .../locale/pt/LC_MESSAGES/django.mo | Bin 4192 -> 4153 bytes .../locale/ru/LC_MESSAGES/django.mo | Bin 5198 -> 5159 bytes .../documents/locale/pt/LC_MESSAGES/django.mo | Bin 18151 -> 18083 bytes .../locale/es/LC_MESSAGES/django.mo | Bin 2221 -> 2182 bytes .../locale/pt/LC_MESSAGES/django.mo | Bin 2173 -> 2134 bytes .../locale/ru/LC_MESSAGES/django.mo | Bin 2637 -> 2598 bytes apps/folders/locale/es/LC_MESSAGES/django.mo | Bin 4356 -> 4317 bytes apps/folders/locale/pt/LC_MESSAGES/django.mo | Bin 4254 -> 4215 bytes apps/folders/locale/ru/LC_MESSAGES/django.mo | Bin 5037 -> 4998 bytes apps/history/locale/es/LC_MESSAGES/django.mo | Bin 1525 -> 1486 bytes apps/history/locale/pt/LC_MESSAGES/django.mo | Bin 1475 -> 1436 bytes apps/history/locale/ru/LC_MESSAGES/django.mo | Bin 1678 -> 1639 bytes apps/linking/locale/pt/LC_MESSAGES/django.mo | Bin 3760 -> 3013 bytes apps/main/locale/es/LC_MESSAGES/django.mo | Bin 2616 -> 2577 bytes apps/main/locale/pt/LC_MESSAGES/django.mo | Bin 2564 -> 2525 bytes apps/main/locale/ru/LC_MESSAGES/django.mo | Bin 3196 -> 3157 bytes apps/metadata/locale/es/LC_MESSAGES/django.mo | Bin 10006 -> 9967 bytes apps/metadata/locale/pt/LC_MESSAGES/django.mo | Bin 9842 -> 9803 bytes apps/metadata/locale/ru/LC_MESSAGES/django.mo | Bin 12218 -> 12179 bytes .../locale/es/LC_MESSAGES/django.mo | Bin 717 -> 678 bytes .../locale/pt/LC_MESSAGES/django.mo | Bin 679 -> 640 bytes .../locale/ru/LC_MESSAGES/django.mo | Bin 780 -> 741 bytes apps/ocr/locale/es/LC_MESSAGES/django.mo | Bin 9303 -> 9264 bytes apps/ocr/locale/pt/LC_MESSAGES/django.mo | Bin 9087 -> 9048 bytes apps/ocr/locale/ru/LC_MESSAGES/django.mo | Bin 11891 -> 11852 bytes .../locale/pt/LC_MESSAGES/django.mo | Bin 3213 -> 2057 bytes .../locale/es/LC_MESSAGES/django.mo | Bin 634 -> 595 bytes .../locale/pt/LC_MESSAGES/django.mo | Bin 611 -> 572 bytes .../locale/ru/LC_MESSAGES/django.mo | Bin 665 -> 626 bytes .../locale/es/LC_MESSAGES/django.mo | Bin 576 -> 537 bytes .../locale/pt/LC_MESSAGES/django.mo | Bin 558 -> 519 bytes .../locale/ru/LC_MESSAGES/django.mo | Bin 494 -> 494 bytes .../locale/es/LC_MESSAGES/django.mo | Bin 681 -> 642 bytes .../locale/pt/LC_MESSAGES/django.mo | Bin 659 -> 620 bytes .../locale/ru/LC_MESSAGES/django.mo | Bin 746 -> 707 bytes apps/sources/locale/pt/LC_MESSAGES/django.mo | Bin 8265 -> 8226 bytes apps/sources/locale/ru/LC_MESSAGES/django.mo | Bin 10504 -> 10465 bytes apps/tags/locale/es/LC_MESSAGES/django.mo | Bin 4239 -> 4200 bytes apps/tags/locale/pt/LC_MESSAGES/django.mo | Bin 4183 -> 4144 bytes apps/tags/locale/ru/LC_MESSAGES/django.mo | Bin 4853 -> 4814 bytes .../locale/es/LC_MESSAGES/django.mo | Bin 4834 -> 4795 bytes .../locale/pt/LC_MESSAGES/django.mo | Bin 4722 -> 4683 bytes .../locale/ru/LC_MESSAGES/django.mo | Bin 6086 -> 6047 bytes .../web_theme/locale/es/LC_MESSAGES/django.mo | Bin 1761 -> 1722 bytes .../web_theme/locale/pt/LC_MESSAGES/django.mo | Bin 1727 -> 1688 bytes .../web_theme/locale/ru/LC_MESSAGES/django.mo | Bin 1969 -> 1930 bytes 60 files changed, 877 insertions(+), 278 deletions(-) create mode 100644 apps/django_gpg/locale/pt/LC_MESSAGES/django.mo create mode 100644 apps/django_gpg/locale/pt/LC_MESSAGES/django.po create mode 100644 apps/django_gpg/locale/ru/LC_MESSAGES/django.mo create mode 100644 apps/django_gpg/locale/ru/LC_MESSAGES/django.po diff --git a/apps/common/locale/es/LC_MESSAGES/django.mo b/apps/common/locale/es/LC_MESSAGES/django.mo index 80f426bc7a52f3e213873260684cebded649d960..00b642dc312659a9c3f6a09805787e2ee1395909 100644 GIT binary patch delta 1547 zcmX}sOGs2v9LMovIZm1HUTUS2??>01@sX*iX;2Fzu?J*rX2PT>D_U=exiN`x_z|1%2XdG?Zl>a9OveD`;%QutL#VNi;Zht&#%QK68mBQCXOIq!dqMcU ziRhw#lWZ%}kTxb8)uDp{+=8p{7V5<>Fb&_LGMd4~IExyHi(ZsSG{$2J?!kPdi8+f( z>^w%&zv<_M!(8FVi#JdWCh#&&p)x;CIw^PwOK}7><)5((zoMqxvnV{!t*8NPM_ys} z+3Ul&gzJ+S)C(_i5`)92h7V8~JVnjOJJiVEquNd5-3ViTqSpFmYl4%i&rkz;jmvNX&*68}QuL6XHrENv#{ugYY6-ug zX40Jy-Yapa>l|dP#z|oQ_2Mcj)L}D{lxabAu-|$JH3J=}0rz462T*(A57L)OO$=|& z0#riHsJ*ij_5M!OChfIe2y!w+#h`7_oD^=@Vhte6Y7Sy8cB3-5hvWDNmFXFFpl0G4 zuE&R{z3~f6@HZBr!@?`Ey-1Y_1~}2kdXU4M;zt?vVK&}DWj2X=;EVMyYDN-BM@y7~ zx}S+`4U<7Iw$OhDga)I2Ruf7@&lKwW*N$}v6(3PS=tVh1CZSPlDm5srap)66%|a!i ztk)4$go>BY_pW8tX44=wBWnqjVnR!-ja;qoU+ek5XsvSXMHyxhCHA@i>+Ly7nho}x zrJpOTVyL9g)r3#juc6u1m6mg^DBF#MrnubJYT8xw@0LfD68cO%L>-}>n>sq>%7_?^ za_@3wMK^8R;WTu#`nwJsX?HgHyILzfzGAP};qdu9UT>wZyl*h(WlW>Ly{+5d#_iV5 TzN4{wMkiw%BSxM0V^RMAV0?vE delta 1647 zcmX}sOGs2v9LMp$oDaV)@5jRayLyTS)01NOe7U4}S$5C8{Ur=-Yg-h@cGH1)5ZdthJ*CJc%8cz*RVfx?gWu4SrN+Ww;pEqb5?1N~8&Mu?>&o z3FH+vhJMDkXLR(yOXO#h9IEg=D)Vfne-rajnLof4_zG+A8>;lH7MRsy399r*P!qj~ zn!r`m`)|4PyO_x5!Xr9*;W%dDH0s75s0?OM70KX_n)yQ1eFYrer?CvEHJ{7P4cLgP z$T{Z)=Oxs8uHY`boAdEGHs37j!9u1hi%#r4SA`hJcz1j1hr@S^QgZrT;oDE4x?Ut2Q}a*lDs`Z4KU$+gPPb|)WoN-17}ft zqGMsIGZUW9H&T(Y9Z4y=L52yqR*?G@# z4QkVVM^(bl`fkTk)E;TaYCM6ed}5f6GJA?VW8pzd#V zcA_dWfLfw+?)p_^ix`FZ-!ySmxklf=2Gd#u2sLetDqf1PeRTigjtZzY5_l-iCI#uD3l*H`J)^trA{_EP_r z)m8b`Hm67)TR83}%0}+{O4EE^q%(YaWWs;cx34=MKT{VBcJ;)&`#UN-`+9@XzL*#7 z>5B$?y+JP$?1{zt!?Db^)0*i^mn^lG<8rQ7Vg=UVHVoo=976r>h4mG# z;I0%Kob?c z^xQ;rGQLT%7t)bFCL1-N7u#_!y6_R|!7ni#U!!I;kE?MJm57s3G?5sL$5d=V57Nc- zp(b_}BN*QdaKL76vGd`5REG(?iBqVVpJh6!cmoS@7*+B)EW*#IlDk(fCE9>Wpb2?| zId0FpaSi7eF{lS#;~*A?P#s55GZ;lxG=3~y zRyXRonOKEh%*XCk)L%2ZZ96`(K1G#u6!pVNd;SpvoPS5XiX%)Iv&K>IZYE3Vg+#_yd(_QIautu@aTgG-@J0 zu?~|dpVmezR$u@#@D3``XQ%{%V;r!VNp|{tzMwL8utKuXh3Z&?x_=PW{{$+L3#i2V zkj-3S$I=K7MlRw13~{yNghXi|yrXs}{-@qL9$LbraQuR&H8o%9-H^ zMMfQVX2mq_Kjf`F-4W>RI@#mh9q8>SbNdT?J};Z!@Amo1{KfqbVn<@@13jH*0-fAG U)!W||*Ag0wJL(AK#?Lr^1N<_DKmY&$ delta 1648 zcmX}sS!_&E9LMp0=?pDxEw$CwqFUAI%yh9-wW_rR(Fm!~;Fi&8=u9)6T0%@DeZh-F zJP;ufA|4Q_2Ne+!68rK@q$Lp#9z;A?67l`rxp6Y*e$Ku3++lIM-uXhY2jln>ZEUqn`We`3L!> z>@UYGZumx+@v}S*xi|@1a4yot4qyQu#hG{(OK<=u;Seh3UpN8(A~9S3NV9A#z{yyQ z8m}JxjBl%HsN)9rg4t%|S!+WLxEtFsh12mPs=wYc1N^9&72|j;MFmoann)w&VGC}< zoyaTf9{L&I9@EeRFOZ+T)n5V8t^IZ zz|W|OtQ?!}zXxY?z7Ms;r%+$R8PvG9QZy9F9VA$LgKap7O8L@pX;+{gSdYrUCR~c! zQGuSvi+BYU&R3`GgAmEAq1+9M~d8V*Zx~S`(?&`u^2f83<5Sw2{KpGOCtDWnmh3PpGNy zSVe2ASt+%egfgUHtEeHWz6J$dK`o(b<|+zIpSH$#i&Dxach|ZWgVb`WQoM+|jLKJL zg;e&a&7*48D*8`QX-wm0P0hL9x&9-xCl-6>LaBF4l;Z-g&5~OYRqLm+B#p78R_e8t zdKIm$HkL|L8aMrFR(j{6nYx@>+<)CSE5ql+c18B}zw~eMt&Jv=-L=7BM`tqH+g`CN z-W5#5dz?gPJQ3`24mq)4XHQRWq$j&&!=^w}BH|=F q)z(y>UNd^&bY9NZQR|#oN3YXCm+tlx$nt`AZ`~G+ykUsZ)KljUZU-xyI z8cq0YG+}ael5-}(>_(+oo|zr2GTVa3@m0KsD=}PcwhOmnDxOA-e~0;a1#9pRG zaEJLMo zJEmhCm*XMSO+UpkoJ3`!gKH<_+o*ZY0F5FVAD{vlM@4qQ8^4N6=|A$u=P{dp1`ip< zHK=v_P=UOUJ{&h7C``W|#>glIG& zXIUJTiSJPhenBo_*D)VgwYfD>i+X|>Did#^0v|#JFoM*zeTCZpHZH|9-me6+Fpf?= z4W;&L)SJzqQu!z9<{6vaw=YEn5XDuv7xjGrwf+byrQf0g`q>+wMP+6V6>!QHcYi;! z&aoC6is*IJ!jC;qqBcB_I`Ah{>Ti1EiDV4@dAFy#@6FzLu(i9dClUzPghE07!r?$D6baXj?hIZ^ ZZSRin@9*Bv=Xd%>)2m`rgH=s={{w~}-5vk{ delta 1899 zcmXZcTWC~A9LMp0ifIhd#CvzMx^7l&tTEY5+?ZseaT`-F;4LVkRj3=0m{yaJO@t`+ zV9-8P`_gShB&HzoL29fe?ZYC9R>T`hi&gs~7QD2A=AlBsAnNzWIWPN}nR905|NqaN zJ(A8EN@ty$p5xriGJ7!JEMR6Q%gk2bMQp^Jh}ksUjGM3>$Kefc{65a0{}Q8^#}7?g zgi~=BYTX|E82hmruc9B{M$NHe8oqL~8cbjz?nQ0fixcn+PR0uu!aq?P{EtB#TVXaG zi#;3AM?Z-Qa4*irV>kydU==>DaNHLI3*1O!IF|?OFpS-(h5Imu8P8!1(VxYjo;P3| zlbC~jn2Q5giD$k36I5cur~tDap3b8Y#EH1l>u*7Vv~N(A$zUzM!kHLZXjX!2QKj66 z1=x#ucnX#2FW8R{P?gw2+GFtt)O*ee8bvftqXM{vitM2`K7!fweH@&|gE$4NFoCO3 z^S(m`@+0PB26dFzaVkDW1^C)Cm#moQn4gC3KnZH%daTDLRB2Cn{*EE~k8m=o-sLa_ zRr+Q86ygD#g?*?BTtdD7%yZN;pK|)x-@-JMK`rXV)fm7e`fxw$@*G7!p2ul;4|PXg zpfW62&!C5$e({Km}5ZA#6mQaR(~kuTg>ZqVB)|I$Cg$h61>aTx=Ubo$Ee%qc%K;3gjND)bCL9!u4kSw^=1}kF5zcejIhwgY}MkMwc1TSv^HX zmb=tF>maIh)u`udF^nnH5gkQkcoCJ^FpkGD4Q{C?p}w9X)W*fAI~4bxZ*XYn7H>!P zwq8``f1@TmN0PBOI0NfGbMM4f)DiTcD)Buk@Bvf+8RVkdHPreaaBkTc!`T?eR&>6g zq0-*LAP%ESIexjDc@^sH*PsGu#*grj_xuEE{;#N#{)GzYKW}^#RhhySZopC0`Uzy7 zV_(uxMBkz&{_J@fwcuUUhX0~U|IQl^ucXZM7osw+MP;}Sb?Y~x<~QMoxCizAe$2%l z4C>7zG;$dDX~>`dDp+9x44iZ&k9jxhvVcBOYoWT2odRcn=vf=PUpK diff --git a/apps/django_gpg/locale/en/LC_MESSAGES/django.po b/apps/django_gpg/locale/en/LC_MESSAGES/django.po index 1192c23608..22a9e34631 100644 --- a/apps/django_gpg/locale/en/LC_MESSAGES/django.po +++ b/apps/django_gpg/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-11-28 05:18-0400\n" +"POT-Creation-Date: 2011-12-05 13:40-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,187 +17,237 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: api.py:18 +#: __init__.py:14 +msgid "Verify document signatures" +msgstr "" + +#: __init__.py:15 +msgid "View keys" +msgstr "" + +#: __init__.py:16 +msgid "Delete keys" +msgstr "" + +#: __init__.py:17 __init__.py:32 +msgid "Query keyservers" +msgstr "" + +#: __init__.py:18 +msgid "Import key from keyservers" +msgstr "" + +#: __init__.py:21 +msgid "Signatures" +msgstr "" + +#: __init__.py:29 views.py:65 +msgid "private keys" +msgstr "" + +#: __init__.py:30 views.py:68 +msgid "public keys" +msgstr "" + +#: __init__.py:31 +msgid "delete" +msgstr "" + +#: __init__.py:33 +msgid "Import" +msgstr "" + +#: __init__.py:36 +msgid "signatures" +msgstr "" + +#: api.py:20 msgid "Public" msgstr "" -#: api.py:19 +#: api.py:21 msgid "Secret" msgstr "" -#: api.py:27 api.py:32 +#: api.py:29 api.py:34 msgid "RSA" msgstr "" -#: api.py:28 +#: api.py:30 msgid "DSA" msgstr "" -#: api.py:33 +#: api.py:35 msgid "Elgamal" msgstr "" -#: api.py:89 +#: api.py:49 +msgid "Bad signature." +msgstr "" + +#: api.py:53 +msgid "Document not signed or invalid signature." +msgstr "" + +#: api.py:57 +msgid "Signature error." +msgstr "" + +#: api.py:61 +msgid "Document is signed but no public key is available for verification." +msgstr "" + +#: api.py:65 +msgid "Document is signed, and signature is good." +msgstr "" + +#: api.py:69 +msgid "Document is signed with a valid signature." +msgstr "" + +#: api.py:142 msgid "unknown" msgstr "" +#: forms.py:11 +msgid "Term" +msgstr "" + #: forms.py:12 -msgid "Real name" +msgid "Name, e-mail, key ID or key fingerprint to look for." msgstr "" -#: forms.py:13 -msgid "Your real name." -msgstr "" - -#: forms.py:17 -msgid "Comment" -msgstr "" - -#: forms.py:19 -msgid "A comment or a note to help identify this key." -msgstr "" - -#: forms.py:23 -msgid "Email" -msgstr "" - -#: forms.py:28 -msgid "Primary key class" -msgstr "" - -#: forms.py:29 -msgid "The key that will be used to sign uploaded content." -msgstr "" - -#: forms.py:33 -msgid "Primary key size (in bytes)" -msgstr "" - -#: forms.py:41 -msgid "Secondary key class" -msgstr "" - -#: forms.py:42 -msgid "The key that will be used to encrypt uploaded content." -msgstr "" - -#: forms.py:46 -msgid "Secondary key size (in bytes)" -msgstr "" - -#: forms.py:53 -msgid "Expiration" -msgstr "" - -#: forms.py:54 -msgid "" -"You can use 0 for a non expiring key, an ISO date in the form: -" -"- or a date difference from the current date in the forms: " -"d, m, w or y." -msgstr "" - -#: forms.py:59 -msgid "Passphrase" -msgstr "" - -#: forms.py:65 -msgid "Passphrase (verification)" -msgstr "" - -#: forms.py:72 -msgid "Both passphrase fields entries must match." -msgstr "" - -#: forms.py:80 -msgid "Key" -msgstr "" - -#: forms.py:81 -msgid "Key to be published, only the public part of the key will be sent." -msgstr "" - -#: tasks.py:27 +#: views.py:43 #, python-format -msgid "Key pair: %s, created successfully." +msgid "Key: %s, imported successfully." msgstr "" -#: tasks.py:34 +#: views.py:46 #, python-format -msgid "Key creation error; %s" +msgid "Unable to import key id: %s" msgstr "" -#: views.py:27 -msgid "Private key list" +#: views.py:50 +msgid "Import key" msgstr "" -#: views.py:30 -msgid "Public key list" +#: views.py:51 +#, python-format +msgid "Are you sure you wish to import key id: %s?" msgstr "" -#: views.py:54 -msgid "Key pair queued for creation, refresh this page to check results." +#: views.py:76 +msgid "Key ID" msgstr "" -#: views.py:64 -msgid "Create a new key" +#: views.py:80 +msgid "Owner" msgstr "" -#: views.py:65 -msgid "" -"The key creation process can take quite some time to complete, please be " -"patient." -msgstr "" - -#: views.py:75 +#: views.py:100 #, python-format msgid "Key: %s, deleted successfully." msgstr "" -#: views.py:82 +#: views.py:107 msgid "Delete key" msgstr "" -#: views.py:83 +#: views.py:109 #, python-format msgid "" -"Are you sure you wish to delete key:%s? If you try to delete a public key " +"Are you sure you wish to delete key: %s? If you try to delete a public key " "that is part of a public/private pair the private key will be deleted as " "well." msgstr "" -#: views.py:95 -#, python-format -msgid "Key publish request for key: %s, has been sent" +#: views.py:127 +msgid "Query key server" msgstr "" -#: views.py:98 -msgid "Unable to send key publish call" +#: views.py:140 +msgid "results" msgstr "" -#: views.py:105 -msgid "Publish a key to the OpenRelay network" -msgstr "" - -#: templates/key_list.html:10 +#: views.py:145 msgid "ID" msgstr "" -#: templates/key_list.html:11 -msgid "User IDs" +#: views.py:149 +msgid "type" msgstr "" -#: templates/key_list.html:12 -msgid "Fingerprint" +#: views.py:153 +msgid "creation date" msgstr "" -#: templates/key_list.html:13 -msgid "Links" +#: views.py:157 +msgid "disabled" msgstr "" -#: templates/key_list.html:22 -msgid "Delete" +#: views.py:161 +msgid "expiration date" msgstr "" -#: templates/key_list.html:26 -msgid "There are no keys available." +#: views.py:165 +msgid "expired" +msgstr "" + +#: views.py:169 +msgid "length" +msgstr "" + +#: views.py:173 +msgid "revoked" +msgstr "" + +#: views.py:178 +msgid "Identifies" +msgstr "" + +#: views.py:205 +#, python-format +msgid "Signature status: %(widget)s %(text)s" +msgstr "" + +#: views.py:212 +msgid "embedded" +msgstr "" + +#: views.py:214 +msgid "detached" +msgstr "" + +#: views.py:219 +#, python-format +msgid "Signature ID: %s" +msgstr "" + +#: views.py:220 +#, python-format +msgid "Signature type: %s" +msgstr "" + +#: views.py:221 +#, python-format +msgid "Key ID: %s" +msgstr "" + +#: views.py:222 +#, python-format +msgid "Timestamp: %s" +msgstr "" + +#: views.py:223 +#, python-format +msgid "Signee: %s" +msgstr "" + +#: views.py:228 +#, python-format +msgid "signature properties for: %s" +msgstr "" + +#: conf/settings.py:13 +msgid "List of keyservers to be queried for unknown keys." msgstr "" diff --git a/apps/django_gpg/locale/es/LC_MESSAGES/django.mo b/apps/django_gpg/locale/es/LC_MESSAGES/django.mo index 502545e74a86acadccce3447f66799e3239dca5a..578eb2056ebd7f32b40858bf2e750d814c96c4ac 100644 GIT binary patch literal 1619 zcmZvbzmFS56vrnJewiPE5GVqPCP+wkx?8V(B;wfQ2)SQyA|~O^q@Ww`KKllCXO@{+ z`y3(hCqMzTG&GcmF3m}}0?|=WAmtyRAq|x9eQW#fii|w_nVo&_``#PRyMOldHv;2j z%vUjg!@Pj``6)aw{sx!82jHXNg-3*FfS16h!DaAqa1}fcUI#CNAA^s9WAJ(K3y}T4 z0@?p-@HF@x$bR30Pl1Qvv*0fv>;3^g1KtNY-e2Go;J+a2{0Fj51LA)o$1`A)?Xb!5 z&Lau-zX-DK68I$e7RYt2gAz0#$NLgo0KWy<|8TZH0om_Y@HOyvkmLUYo&`_Anf=e< zaRz(^iFDV12HE^v9UhLG?Gt^FfXA53(#(VU7N`L z5XvNu2RiImy=H2Q*jcZfwk|H%H`d-)%D=SMajK)pnL)3N3_*@0+5;>16uQ}Tz7i@F3(;QfHSLJl~ zok5{=&Pl!vcQdHefTAv`B9VKgGJF>nHXy5Wt<0ianAJ8KjhcZ2={_Aa4TZRzo|KQv zX^|0Sg^vT2V)F4~qqEc5IzGd8v(?zJCgAUjx;W@W20DO9uC%!-(|zSUg|&~m+tKQ= z9mngFi?&Q^Gi?TmT?ZhraKt#kZ=cg4w^dnYk|3yhp5c0M;JHM znZ-q`cUZxTn{)YP*<EFGdEDGCqEfcr{+XmwU>1dA%~;) V$vy5PY)qR-We}W35kZ$${14V!ry>9V literal 4289 zcmai$O^h5z6~_y32rT5oAp!GU0qoex%y?}QFyna1+FpkhYkTcogE(-g>8_crY)^N) zs(aUCA#vn}P^1WP0O3dogV5p*d;Q0U_PwAO9j=dYed8TUodn-_Cx49JzDubd_!{^<@L%BL;Pd;Gx)=Nf z_#pTjkXPyt;Jd-s!8!0vP}cnwya)VeJO6L+eLUa8q#U&$l>HBb9|RA8i{Mf4)8JK5 z^mqY04gMPZ1b8opajOo2a$X1gF!*_JKX?iJD42kv=Qlxq>O1^B0Dc!d1pXM5^M41v z7rX_20DJ@dIQS2cpSmAoE`g7Lp94qWXTcwU;=fzqC&9mi?*Sj+@2B7aP|o{3$Sd_H z@H)Q!Gq}L>0Oel+Z-9O9t2jsOc^-TO{5~l9-2~a9eg=wtzXCS<8X})e%O!G1XsY1fZqZ& z_#(&>wFBM)Z-M;Oj|j5Je+9(7Q76$`VkmJHE=gP^Mlw2FvR-r+e+s8Q!zJTT0}86D z$G9Hl(p)mcH^Qgsc!(Q`zYK|k_~dbUV2Iykh#w@TGQ?Kl`E-b{BoCh8!iCXSgdh&F zTWpgdToAvD&%}O-MI4JdDaK=)SGw@p=)9<`t_rr9oqXcQ)v89V$IG zRkG1jXBvyu8E@qrthU>tk~+J#sLn=1T8Oht*~pAdrk2LW@qev!-c+v0)j5}stVg@N zQp;!6myy7w6A{aMZM`r2NqsQH>1fW?+SWGJL!3Qb*3sS$y%jf#n@cl_5lA*{vdODZ zXI1FUB=iy2Y4#!~)JbAP7}i-vaOCjlLu7f&QJ5?zVOq0o18)ti&2_*naXGQUne8T< zc65WEtPZfqcn zoRMVgxNRo5>sB9jxgJa^8{+Cn5m}wNP^s0(s}`b=-D$jG;uOY;85c{NU$&W<5cg`k z@SEx~aldR$rgJm4YRx7^p5Cs^?Q?gk!&`4Fwch$@8d->2NO*K&a$T8CtFP7$!i8c? zh@Jcul76M6PDcYmT!{xxiT<5RjJYzUejSKBJ8ho$Nm=Q-%nFk-huYQu-xk~`zxy6i z+HmECcq?eL1>emevpsbs7nDPZdm5cNC0b&#OkD}q>*cdSeT~{6)}o`HkK~h@LMlks zkS?;dqgk-Lc2TF2eFRm40#C<%{q)2dztDYpj3pajY9P;)Q3gEIzxWpI%vcCUd>Qj~3?6FP~r9 zHSt()e(p>|rtbQr#DL0PtB#hLarsG|Y#1MGb?VCcx$cSGb)p~9=`Q6-krL{@J~43B z?DW32VQw|6y~(=g2sZJ_%lH^h9X|>A<;|(XxqkGNK6Ydd%1LSvS+P_Xy3S*FqDvTW zw3*aoD0$uXDC&skfXe7BGg~nB)n6`!YUc)g9o4=d@uX!0YqZVW*yXIw+O<-^Xjp3w zDoXn7St4gfdAi*Bxil^l1qoN99?&1-cv+6b}W-}BRdNz9^(?=N%C8I+s?}(c0e6Z3YRA?bv`z=)>*Ny`KVT-d|ulv z6Pw#oYrLA?%yeU>?Q-2cZ7y*;H*@u+nhwj6ZsaQZ;Bxa>fH%^@s}=FLT9Q^5b%gkF zS5#l##Rj;F!^Iw%5EEn7j?R%{88>wF7UygcTTJ$&X*|oF8zbVSOWFmz!W)<=B@z}$ z?IZ&vn0Tn^l_*8KyzRW*o|#I$rCubsT*UO$T0vM{Rj2LO#~sQ~`=$<#y*A%+goZFp zX(3uj?TghAhnS8IJ2$gJ{ECXA=#&zBiNF@P*@`3V;+E#9mKPAmUULhCC7EIy^SKAhCJUtqg56xD= ztrU|Ix->kRQ9>#W4zDYhx$8oYhN*7o5pFYtNtF>cg?a!;(zk57`_`eO+Puxy3w){b z9n+>&>OxuMj!sPm@Cr2=%OcT85((gxs(i7E6Y5Y)fep0qil8*>r^?g9qxP+Q(T|UV z*vn1ci9sD&+Cl8*lGG;laYO4BZb>7zLE5-8wDGu}79Kmeidt7@Q#1gMDkzwR`X9jL+hza& diff --git a/apps/django_gpg/locale/es/LC_MESSAGES/django.po b/apps/django_gpg/locale/es/LC_MESSAGES/django.po index dcc0e1eeab..9d384e4241 100644 --- a/apps/django_gpg/locale/es/LC_MESSAGES/django.po +++ b/apps/django_gpg/locale/es/LC_MESSAGES/django.po @@ -6,212 +6,251 @@ # Roberto Rosario , 2011. msgid "" msgstr "" -"Project-Id-Version: OpenRelay\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-11-28 05:18-0400\n" -"PO-Revision-Date: 2011-11-28 09:25+0000\n" +"Project-Id-Version: Mayan EDMS\n" +"Report-Msgid-Bugs-To: http://github.com/rosarior/mayan/issues\n" +"POT-Creation-Date: 2011-12-05 13:40-0400\n" +"PO-Revision-Date: 2011-12-05 17:52+0000\n" "Last-Translator: rosarior \n" -"Language-Team: LANGUAGE \n" +"Language-Team: Spanish (Castilian) (http://www.transifex.net/projects/p/mayan-edms/team/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -#: api.py:18 +#: __init__.py:14 +msgid "Verify document signatures" +msgstr "Verificar las firmas de documentos" + +#: __init__.py:15 +msgid "View keys" +msgstr "Ver llaves" + +#: __init__.py:16 +msgid "Delete keys" +msgstr "Borrar llaves" + +#: __init__.py:17 __init__.py:32 +msgid "Query keyservers" +msgstr "Hacer búsquedas en servidores de llaves" + +#: __init__.py:18 +msgid "Import key from keyservers" +msgstr "Importar llaves de los servidores de llaves" + +#: __init__.py:21 +msgid "Signatures" +msgstr "Firmas" + +#: __init__.py:29 views.py:65 +msgid "private keys" +msgstr "llaves privadas" + +#: __init__.py:30 views.py:68 +msgid "public keys" +msgstr "llaves públicas" + +#: __init__.py:31 +msgid "delete" +msgstr "borrar" + +#: __init__.py:33 +msgid "Import" +msgstr "Importar" + +#: __init__.py:36 +msgid "signatures" +msgstr "firmas" + +#: api.py:20 msgid "Public" -msgstr "Pública" +msgstr "" -#: api.py:19 +#: api.py:21 msgid "Secret" -msgstr "Secreta" +msgstr "" -#: api.py:27 api.py:32 +#: api.py:29 api.py:34 msgid "RSA" msgstr "RSA" -#: api.py:28 +#: api.py:30 msgid "DSA" msgstr "DSA" -#: api.py:33 +#: api.py:35 msgid "Elgamal" -msgstr "Elgamal" +msgstr "" -#: api.py:89 +#: api.py:49 +msgid "Bad signature." +msgstr "" + +#: api.py:53 +msgid "Document not signed or invalid signature." +msgstr "" + +#: api.py:57 +msgid "Signature error." +msgstr "" + +#: api.py:61 +msgid "Document is signed but no public key is available for verification." +msgstr "" + +#: api.py:65 +msgid "Document is signed, and signature is good." +msgstr "" + +#: api.py:69 +msgid "Document is signed with a valid signature." +msgstr "" + +#: api.py:142 msgid "unknown" -msgstr "desconocido" +msgstr "" + +#: forms.py:11 +msgid "Term" +msgstr "Término" #: forms.py:12 -msgid "Real name" -msgstr "Nombre real" - -#: forms.py:13 -msgid "Your real name." -msgstr "Su nombre real." - -#: forms.py:17 -msgid "Comment" -msgstr "Comentario" - -#: forms.py:19 -msgid "A comment or a note to help identify this key." -msgstr "Un comentario o una nota para ayudar a identificar esta llave." - -#: forms.py:23 -msgid "Email" -msgstr "E-mail" - -#: forms.py:28 -msgid "Primary key class" -msgstr "Clase de llave primaria" - -#: forms.py:29 -msgid "The key that will be used to sign uploaded content." -msgstr "La llave que se utilizara para firmar el contenido subido." - -#: forms.py:33 -msgid "Primary key size (in bytes)" -msgstr "Tamaño de la llave principal (en bytes)" - -#: forms.py:41 -msgid "Secondary key class" -msgstr "Clase de llave secundaria" - -#: forms.py:42 -msgid "The key that will be used to encrypt uploaded content." -msgstr "La llave que se utilizara para cifrar el contenido subido." - -#: forms.py:46 -msgid "Secondary key size (in bytes)" -msgstr "Tamaño de la llave secundaria (en bytes)" - -#: forms.py:53 -msgid "Expiration" -msgstr "Expiración" - -#: forms.py:54 -msgid "" -"You can use 0 for a non expiring key, an ISO date in the form: " -"-- or a date difference from the current date in the " -"forms: d, m, w or y." +msgid "Name, e-mail, key ID or key fingerprint to look for." msgstr "" -"Usted puede utilizar 0 para llaves que no expiran, una fecha ISO en la " -"forma: -- o una diferencia de fecha con respecto a la fecha " -"actual en las formas: d, m, w o y." -#: forms.py:59 -msgid "Passphrase" -msgstr "Frase de contraseña" - -#: forms.py:65 -msgid "Passphrase (verification)" -msgstr "Contraseña (verificación)" - -#: forms.py:72 -msgid "Both passphrase fields entries must match." -msgstr "Las entradas de los campos de contraseña deben coincidir." - -#: forms.py:80 -msgid "Key" -msgstr "Llave" - -#: forms.py:81 -msgid "Key to be published, only the public part of the key will be sent." -msgstr "" -"La llave para ser publicada, sólo la parte pública de la llave será enviada." - -#: tasks.py:27 +#: views.py:43 #, python-format -msgid "Key pair: %s, created successfully." -msgstr "Par de llaves: %s, creado correctamente." +msgid "Key: %s, imported successfully." +msgstr "" -#: tasks.py:34 +#: views.py:46 #, python-format -msgid "Key creation error; %s" -msgstr "Error de creación de llave; %s" - -#: views.py:27 -msgid "Private key list" -msgstr "Lista de llaves privadas" - -#: views.py:30 -msgid "Public key list" -msgstr "Lista de llaves públicas" - -#: views.py:54 -msgid "Key pair queued for creation, refresh this page to check results." +msgid "Unable to import key id: %s" msgstr "" -"Par de llaves en lista de creación, vuelva a cargar esta página " -"periodicamente para comprobar los resultados." -#: views.py:64 -msgid "Create a new key" -msgstr "Crear una llave nueva" - -#: views.py:65 -msgid "" -"The key creation process can take quite some time to complete, please be " -"patient." +#: views.py:50 +msgid "Import key" msgstr "" -"El proceso de creación de la llaves puede tomar bastante tiempo en " -"completar, por favor, sea paciente." -#: views.py:75 +#: views.py:51 +#, python-format +msgid "Are you sure you wish to import key id: %s?" +msgstr "¿Esta seguro que desea importar la llave: %s?" + +#: views.py:76 +msgid "Key ID" +msgstr "" + +#: views.py:80 +msgid "Owner" +msgstr "Dueño" + +#: views.py:100 #, python-format msgid "Key: %s, deleted successfully." -msgstr "Llave: %s, eliminada exitosamente." +msgstr "Llave: %s, borrada exitosamente." -#: views.py:82 +#: views.py:107 msgid "Delete key" -msgstr "Eliminar la llave" +msgstr "Borrar llave" -#: views.py:83 +#: views.py:109 #, python-format msgid "" -"Are you sure you wish to delete key:%s? If you try to delete a public key " +"Are you sure you wish to delete key: %s? If you try to delete a public key " "that is part of a public/private pair the private key will be deleted as " "well." msgstr "" -"¿Está seguro que desea eliminar la llave: %s? Si intenta eliminar una llave" -" pública que forma parte de una pareja pública / privada de la llave privada" -" se eliminarán también." -#: views.py:95 -#, python-format -msgid "Key publish request for key: %s, has been sent" -msgstr "Solicitud publicación de llave: %s, ha sido enviada" +#: views.py:127 +msgid "Query key server" +msgstr "" -#: views.py:98 -msgid "Unable to send key publish call" -msgstr "No se puede enviar llave publica" +#: views.py:140 +msgid "results" +msgstr "resultados" -#: views.py:105 -msgid "Publish a key to the OpenRelay network" -msgstr "Publicar una llave a la red OpenRelay" - -#: templates/key_list.html:10 +#: views.py:145 msgid "ID" -msgstr "Identificador" +msgstr "" -#: templates/key_list.html:11 -msgid "User IDs" -msgstr "ID de usuarios" +#: views.py:149 +msgid "type" +msgstr "tipo" -#: templates/key_list.html:12 -msgid "Fingerprint" -msgstr "Huella digital" +#: views.py:153 +msgid "creation date" +msgstr "fecha de creación" -#: templates/key_list.html:13 -msgid "Links" -msgstr "Enlaces" +#: views.py:157 +msgid "disabled" +msgstr "" -#: templates/key_list.html:22 -msgid "Delete" -msgstr "Eliminar" +#: views.py:161 +msgid "expiration date" +msgstr "" -#: templates/key_list.html:26 -msgid "There are no keys available." -msgstr "No hay llaves disponibles." +#: views.py:165 +msgid "expired" +msgstr "" + +#: views.py:169 +msgid "length" +msgstr "" + +#: views.py:173 +msgid "revoked" +msgstr "" + +#: views.py:178 +msgid "Identifies" +msgstr "" + +#: views.py:205 +#, python-format +msgid "Signature status: %(widget)s %(text)s" +msgstr "" + +#: views.py:212 +msgid "embedded" +msgstr "" + +#: views.py:214 +msgid "detached" +msgstr "" + +#: views.py:219 +#, python-format +msgid "Signature ID: %s" +msgstr "" + +#: views.py:220 +#, python-format +msgid "Signature type: %s" +msgstr "" + +#: views.py:221 +#, python-format +msgid "Key ID: %s" +msgstr "" + +#: views.py:222 +#, python-format +msgid "Timestamp: %s" +msgstr "" + +#: views.py:223 +#, python-format +msgid "Signee: %s" +msgstr "" + +#: views.py:228 +#, python-format +msgid "signature properties for: %s" +msgstr "" + +#: conf/settings.py:13 +msgid "List of keyservers to be queried for unknown keys." +msgstr "" diff --git a/apps/django_gpg/locale/pt/LC_MESSAGES/django.mo b/apps/django_gpg/locale/pt/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..7fae94e56b671cf8420121b6a06c89cdcdbb91eb GIT binary patch literal 524 zcmZut%T6OP5Cp+#j+{9dX%8Ui@k|IJ7=qA-AR#f@h@IWLlQfRej6Je@2=E{KV|)PL zLJ#3_*^)|rRabZUb!+qA8e^Tg&HTaKU>-0#445zcc*1L^`NAaldz=wm&*3kIXuVUS z&`;Wl@n!Klxy8l@f+89_ho7y9PdiF zqkx*&7A`fR61^<#L%)=Wp-u zF?C3?S$$BH%_\n" +"Language-Team: Portuguese (http://www.transifex.net/projects/p/mayan-edms/team/pt/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: __init__.py:14 +msgid "Verify document signatures" +msgstr "" + +#: __init__.py:15 +msgid "View keys" +msgstr "" + +#: __init__.py:16 +msgid "Delete keys" +msgstr "" + +#: __init__.py:17 __init__.py:32 +msgid "Query keyservers" +msgstr "" + +#: __init__.py:18 +msgid "Import key from keyservers" +msgstr "" + +#: __init__.py:21 +msgid "Signatures" +msgstr "" + +#: __init__.py:29 views.py:65 +msgid "private keys" +msgstr "" + +#: __init__.py:30 views.py:68 +msgid "public keys" +msgstr "" + +#: __init__.py:31 +msgid "delete" +msgstr "" + +#: __init__.py:33 +msgid "Import" +msgstr "" + +#: __init__.py:36 +msgid "signatures" +msgstr "" + +#: api.py:20 +msgid "Public" +msgstr "" + +#: api.py:21 +msgid "Secret" +msgstr "" + +#: api.py:29 api.py:34 +msgid "RSA" +msgstr "" + +#: api.py:30 +msgid "DSA" +msgstr "" + +#: api.py:35 +msgid "Elgamal" +msgstr "" + +#: api.py:49 +msgid "Bad signature." +msgstr "" + +#: api.py:53 +msgid "Document not signed or invalid signature." +msgstr "" + +#: api.py:57 +msgid "Signature error." +msgstr "" + +#: api.py:61 +msgid "Document is signed but no public key is available for verification." +msgstr "" + +#: api.py:65 +msgid "Document is signed, and signature is good." +msgstr "" + +#: api.py:69 +msgid "Document is signed with a valid signature." +msgstr "" + +#: api.py:142 +msgid "unknown" +msgstr "" + +#: forms.py:11 +msgid "Term" +msgstr "" + +#: forms.py:12 +msgid "Name, e-mail, key ID or key fingerprint to look for." +msgstr "" + +#: views.py:43 +#, python-format +msgid "Key: %s, imported successfully." +msgstr "" + +#: views.py:46 +#, python-format +msgid "Unable to import key id: %s" +msgstr "" + +#: views.py:50 +msgid "Import key" +msgstr "" + +#: views.py:51 +#, python-format +msgid "Are you sure you wish to import key id: %s?" +msgstr "" + +#: views.py:76 +msgid "Key ID" +msgstr "" + +#: views.py:80 +msgid "Owner" +msgstr "" + +#: views.py:100 +#, python-format +msgid "Key: %s, deleted successfully." +msgstr "" + +#: views.py:107 +msgid "Delete key" +msgstr "" + +#: views.py:109 +#, python-format +msgid "" +"Are you sure you wish to delete key: %s? If you try to delete a public key " +"that is part of a public/private pair the private key will be deleted as " +"well." +msgstr "" + +#: views.py:127 +msgid "Query key server" +msgstr "" + +#: views.py:140 +msgid "results" +msgstr "" + +#: views.py:145 +msgid "ID" +msgstr "" + +#: views.py:149 +msgid "type" +msgstr "" + +#: views.py:153 +msgid "creation date" +msgstr "" + +#: views.py:157 +msgid "disabled" +msgstr "" + +#: views.py:161 +msgid "expiration date" +msgstr "" + +#: views.py:165 +msgid "expired" +msgstr "" + +#: views.py:169 +msgid "length" +msgstr "" + +#: views.py:173 +msgid "revoked" +msgstr "" + +#: views.py:178 +msgid "Identifies" +msgstr "" + +#: views.py:205 +#, python-format +msgid "Signature status: %(widget)s %(text)s" +msgstr "" + +#: views.py:212 +msgid "embedded" +msgstr "" + +#: views.py:214 +msgid "detached" +msgstr "" + +#: views.py:219 +#, python-format +msgid "Signature ID: %s" +msgstr "" + +#: views.py:220 +#, python-format +msgid "Signature type: %s" +msgstr "" + +#: views.py:221 +#, python-format +msgid "Key ID: %s" +msgstr "" + +#: views.py:222 +#, python-format +msgid "Timestamp: %s" +msgstr "" + +#: views.py:223 +#, python-format +msgid "Signee: %s" +msgstr "" + +#: views.py:228 +#, python-format +msgid "signature properties for: %s" +msgstr "" + +#: conf/settings.py:13 +msgid "List of keyservers to be queried for unknown keys." +msgstr "" + + diff --git a/apps/django_gpg/locale/ru/LC_MESSAGES/django.mo b/apps/django_gpg/locale/ru/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..2541ca14bf8115e61ff6ea7c4a17ae73c7952fb8 GIT binary patch literal 595 zcmZutO-~y!5KTW$d+e=J4^yc{MBufZq-tuiv;u;J#EK~0_UOnCpg*ZW3i$9amGcYkmh+1ZNy zMjwo`3UYm|Elf}HpUE5>=OfEQX;$p0Edwi@f_03I;=D9*-7eCltGRbUd*eK>J1sXM zv>1}vMZu0e>PT(biH@jXAf#k+z{D4jLp2gijD#Q}o8w>8jemjnRgH#wf+9KBA+o}2 z8!8>0SMWF&Cf+U3N0&a_q@}Yry26_uWvxxsEqO?CTeiAHR-mpG%-awQIo*Bmv)OFY z*flqoc$Hd=yxIOv;LX+;!&M!4BsKRf?N8$3QuvQd}bE5t+!xV_yk)#>)linR^cdWet literal 0 HcmV?d00001 diff --git a/apps/django_gpg/locale/ru/LC_MESSAGES/django.po b/apps/django_gpg/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000000..54d387774b --- /dev/null +++ b/apps/django_gpg/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,255 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Mayan EDMS\n" +"Report-Msgid-Bugs-To: http://github.com/rosarior/mayan/issues\n" +"POT-Creation-Date: 2011-12-05 13:40-0400\n" +"PO-Revision-Date: 2011-12-05 17:43+0000\n" +"Last-Translator: rosarior \n" +"Language-Team: Russian (http://www.transifex.net/projects/p/mayan-edms/team/ru/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" + +#: __init__.py:14 +msgid "Verify document signatures" +msgstr "" + +#: __init__.py:15 +msgid "View keys" +msgstr "" + +#: __init__.py:16 +msgid "Delete keys" +msgstr "" + +#: __init__.py:17 __init__.py:32 +msgid "Query keyservers" +msgstr "" + +#: __init__.py:18 +msgid "Import key from keyservers" +msgstr "" + +#: __init__.py:21 +msgid "Signatures" +msgstr "" + +#: __init__.py:29 views.py:65 +msgid "private keys" +msgstr "" + +#: __init__.py:30 views.py:68 +msgid "public keys" +msgstr "" + +#: __init__.py:31 +msgid "delete" +msgstr "" + +#: __init__.py:33 +msgid "Import" +msgstr "" + +#: __init__.py:36 +msgid "signatures" +msgstr "" + +#: api.py:20 +msgid "Public" +msgstr "" + +#: api.py:21 +msgid "Secret" +msgstr "" + +#: api.py:29 api.py:34 +msgid "RSA" +msgstr "" + +#: api.py:30 +msgid "DSA" +msgstr "" + +#: api.py:35 +msgid "Elgamal" +msgstr "" + +#: api.py:49 +msgid "Bad signature." +msgstr "" + +#: api.py:53 +msgid "Document not signed or invalid signature." +msgstr "" + +#: api.py:57 +msgid "Signature error." +msgstr "" + +#: api.py:61 +msgid "Document is signed but no public key is available for verification." +msgstr "" + +#: api.py:65 +msgid "Document is signed, and signature is good." +msgstr "" + +#: api.py:69 +msgid "Document is signed with a valid signature." +msgstr "" + +#: api.py:142 +msgid "unknown" +msgstr "" + +#: forms.py:11 +msgid "Term" +msgstr "" + +#: forms.py:12 +msgid "Name, e-mail, key ID or key fingerprint to look for." +msgstr "" + +#: views.py:43 +#, python-format +msgid "Key: %s, imported successfully." +msgstr "" + +#: views.py:46 +#, python-format +msgid "Unable to import key id: %s" +msgstr "" + +#: views.py:50 +msgid "Import key" +msgstr "" + +#: views.py:51 +#, python-format +msgid "Are you sure you wish to import key id: %s?" +msgstr "" + +#: views.py:76 +msgid "Key ID" +msgstr "" + +#: views.py:80 +msgid "Owner" +msgstr "" + +#: views.py:100 +#, python-format +msgid "Key: %s, deleted successfully." +msgstr "" + +#: views.py:107 +msgid "Delete key" +msgstr "" + +#: views.py:109 +#, python-format +msgid "" +"Are you sure you wish to delete key: %s? If you try to delete a public key " +"that is part of a public/private pair the private key will be deleted as " +"well." +msgstr "" + +#: views.py:127 +msgid "Query key server" +msgstr "" + +#: views.py:140 +msgid "results" +msgstr "" + +#: views.py:145 +msgid "ID" +msgstr "" + +#: views.py:149 +msgid "type" +msgstr "" + +#: views.py:153 +msgid "creation date" +msgstr "" + +#: views.py:157 +msgid "disabled" +msgstr "" + +#: views.py:161 +msgid "expiration date" +msgstr "" + +#: views.py:165 +msgid "expired" +msgstr "" + +#: views.py:169 +msgid "length" +msgstr "" + +#: views.py:173 +msgid "revoked" +msgstr "" + +#: views.py:178 +msgid "Identifies" +msgstr "" + +#: views.py:205 +#, python-format +msgid "Signature status: %(widget)s %(text)s" +msgstr "" + +#: views.py:212 +msgid "embedded" +msgstr "" + +#: views.py:214 +msgid "detached" +msgstr "" + +#: views.py:219 +#, python-format +msgid "Signature ID: %s" +msgstr "" + +#: views.py:220 +#, python-format +msgid "Signature type: %s" +msgstr "" + +#: views.py:221 +#, python-format +msgid "Key ID: %s" +msgstr "" + +#: views.py:222 +#, python-format +msgid "Timestamp: %s" +msgstr "" + +#: views.py:223 +#, python-format +msgid "Signee: %s" +msgstr "" + +#: views.py:228 +#, python-format +msgid "signature properties for: %s" +msgstr "" + +#: conf/settings.py:13 +msgid "List of keyservers to be queried for unknown keys." +msgstr "" + + diff --git a/apps/document_comments/locale/es/LC_MESSAGES/django.mo b/apps/document_comments/locale/es/LC_MESSAGES/django.mo index 47f03d30c89fd1c18a541c97123867c0350e5b12..d2d8efb3e2a839db1e0f6f49ff3103c4ff761f9a 100644 GIT binary patch delta 234 zcmZ3-*UmR#PyH4~28J|d1_osY1_o(X1_l!#Z4ac)fOH9vRtM6nfwUr!z6_+ff%I1( zEdit%*%%l^fV32l76a1yK-vmOy8&qxAl(S0`GNE@AT0!>w*hG`Abk=@gUq?Y#vl(? z@Bk2kmdl=-+(kbkp9oMIf~JdNrfxGKSbBLC^fMpGe1w)C9x#cO2Np$&`=kM mjEocv4XupKCO>07#p9EhmtLBf4wOzUp1htVdb2pII1>PDdnLgD delta 253 zcmZqYTgNwHPyKC128J|d1_osY1_mcq1_l!#od%@Mfb?u2tq!Cw0ck}b{R>EQ18EsH z1_lWrtpTJ(fV3l!76Z~@K-vmO=K^UJAiV-e^8@L#Kw1b$-v!cKK>7oa2AT7ljX@r) zfSsLz0cbXZIFRN5(y~Arh#1t^H%Bo#GO1^jloVL$>!)XyWRxc9CFker7v&cx7G>ra z>E|X^Cg$m978jSM7EhkdEXi$PscU4ZU}#}wVlsI@^QpT_Kzb^W76sDFfHXId-UFmT=A2+-kOwQc z3KU=g3OobS>_GZ8kmdl=pV>A?F*-7-a0U2>=sFjrCYEI8=jpm6mZVxK7#SEE>H?9G lk%FP2m66%xXUsc!d=m50OB2(9(gh`x=dna@W@D9Q0sznqCHMdU delta 252 zcmeyvyO?jnp88Xa3=C<^3=C=v3=D>>3=AefIs!Bq?Lj6Yaq=7r1{wx z7$ktS43HK9(xyOK7)W~oX^{RnAguwUCj)5#AiW1jivsDhK$;s!KL*kubKbKt$b%L9 z0Sd4H1$fyR7=VaD2uN@MX({&2QH+jE>KP>^1y=g{>6s-NrAd0p`MLT<`NfGvnfXQf zxrvpDdHR{f#igmmlV>waavNCc8d)kBT3DHwOy1ADb8;(76t_=eUV3R_da9K|!REUx G@=O5Ic`Z8t diff --git a/apps/document_comments/locale/ru/LC_MESSAGES/django.mo b/apps/document_comments/locale/ru/LC_MESSAGES/django.mo index 718e22ed33a3325624031b4793f47e5524d516b3..b31ea9bdc2e3cf28f5c766ab87bfff9de0dd7c48 100644 GIT binary patch delta 233 zcmdlY*ef_;PyH=M28J|d1_nn4AZBG?2m{j1Q2G{-)&%km*%%l!fOIsF76Z~PK-vIE z&j!+}K>7@jHU!e2fHX+I3OfUX3y`h`(jff@fV4W0eg&jK`h_?kv?d3G9D_7azyT-# zL<})Nf)7Zi0%-{#U9vfn(TPcgE5JWQ*SRP)u_QA;PuC@}B-Kj6$iUE07l@3E6bucm gjLarKV_wGNlbDxYnwSohE-Ibe%@V!&Gs{6{02W>*pa1{> delta 252 zcmeAb+#)z(Pdy_O149}!1A`+2149NY149^)UInEY*%%l!f&2&{tpTK)fV3EpUJaxT zfb=0CtqP<+18GAbEy)hi?+v6~fc!;38l?X@kX8rMf*cV2mQXr?gF%i#8Yqzt6aXTI zW+1@_q_?>q$;*?6R$ABXFS9| z+{c5Ww1)S%gc;I8UP;=;3X;+>+Vd9H@fzpx8x8tpb(9e7l%*}a!)^RTJHW!Yw2nt; z3saoN2b{qHy7+-p_=Sr&>N5;3V-2I>{sg(A7LMVC&$B87T@n*`gZ3r2IEy{BjbG9J zOoq1cFHT~fB3n1d;^4|q(|;NHanh_%Er#K$F+mu{CQ2Q5P}*ypCg+W$83)aF`sUXA Ikyp$A1F@4aHUIzs delta 458 zcmXZYOG^S#6u|LgV5#{CGOQLc1W^gosHo`Fs#;}G^dKr)Ofl#z)NsZ{i*VCpue)|F z+XWUx(FdqS5G^8t`UZi0g8oA;?){y6?>UEi@163kTsxk|r$^+}Cz2BpBOo$@7nsKz z%;FDj;Y7d4CLUuN-*5_jK?WwVfI4y{XXu_caTgzO3}YdY6lO!xkRpM_#w@<#9EQUG z7g)j!>l1Vf?=glSIE)?iV;~|jgi)NtIC5uM!)e^>uFsJYX<{E>-gUeE)YI`6N{(}=Yg*BAO7;DjgKAl;Rc)hY zRcl(=I5H}lW!rVr4y>+hs0%gIaI9)YT{Ik1S5k>&QcdL5R9;Eu^mL~6<$G@3`gelM RMx|Icilmr!I~<7j{sI1!MQ{KB diff --git a/apps/document_indexing/locale/pt/LC_MESSAGES/django.mo b/apps/document_indexing/locale/pt/LC_MESSAGES/django.mo index 5c371fea1752438828c2744ad45322bb6ae2aeb3..3039293cfc66ef1337f9ce01e5dac90d65c32ab1 100644 GIT binary patch delta 398 zcmXZXze~eF7{>9Z!D!O7R-__Pgg6utshSA>h=@A46&)26qy;Gs6^9B=b`l&5`4h4T zE<#;8w7aw5-QcmAX{8zI){)jF&1a=)tl>7^;1c%n z6u(;okqgfh=7UM)0n1D2DQDt5h%QSJnDi99%Y& Mo_pLWWjBZa06t$Zi2wiq delta 457 zcmXZXze_?<6u|LgW&X;nD54~I4H6Eor&6SbWYm_RL=i|3w-9}hKGEmUT z!W&%1Aui$wSMV2|hr%I-!w5P9Z6h;_!Zp17-`^rj?g$=y44sI~k@Pv3!67<7c|)h+ zC(h$HE@6lwH%8H^Hweys7ANo+o%>?%dF;!5P_gZL+AzwdU1^=KUszS6VKr65v>HZL zU8$O3Hk++d(_hFR>3fZmvQ4X|?<>2M))KLJT#u#nL`sV%(;J)JckiHk?<)p!s#b2P MG9~rik1sR%2d@Z5OaK4? diff --git a/apps/document_indexing/locale/ru/LC_MESSAGES/django.mo b/apps/document_indexing/locale/ru/LC_MESSAGES/django.mo index bd8617e870933602702f0ca426aeb9eecc0f5278..59a1601a1a751b53a0d61bda250f98b07c96d8d4 100644 GIT binary patch delta 398 zcmXZXu}i~H5XbRfg+i0mYGVEax&W|x_yv3;R9*g)IzZ<#kLW7;hp1)-HnaW$PTlBnw>js`5CXHr#v9edK SRjRu|VZRo(jB>{|XUu;_#x$P* delta 458 zcmXZXze_?<6bJCHhNR|?%C;JeAcS~bpIE422(?*R5C#2$Ylyx;pS&M~ML0w?hy4u& zPJ&ZgLyJ%?4Qm9+2+F?vj$!8gOaO`XOM>hyh4g}SE(?{VrZlvg#Qj%9>TNi;JgvRT#P{ z4NW&hRW@WzXr^hGO#gOaS6MSk($e&rvM#MsCYp$=suE8tiF8y=Ws*y+zW1SZ=Q{{& R$XeN!WtJFr_s5s@{{zBWNZ9}Y diff --git a/apps/documents/locale/pt/LC_MESSAGES/django.mo b/apps/documents/locale/pt/LC_MESSAGES/django.mo index cdde47b0a99fd330746127abd0729e18c73cf479..4e663ecd4d79105fbf5d9d05d11733a347e85048 100644 GIT binary patch delta 4259 zcmZA23shBA9>?)N6FyLRga{}a7e(X|1{W1X9j^&!2B?HqKp2@KQEEjdh8@e)#!*M- zYNeT3nnq@*lw}Xg%*kO<``j;A zhizLO=6nTOyz^4*!}&C%FV=|aNDET8 z9Y*?KC$I;eLOp*5^?U>;J+U9E-ZUIR`!7E@o8+vPcR2-;>`lM8VBNAsD{2n zReVu`Sv1C>?x&(gKEZPlDkHm*vD(LY37*k8?OSXjDaSNa1;a1}uf{BF#!P$yC*j+u zjwSUqE5K45g3BaRw;Pp__xmw8JcX(^A&D$tA@Yb7p+j9(OF<*J6ZPT>)b&lM9=Br@ zzKLV;1d>&YXThi=S*Q-wpw_~DsHuDplkpt-u>Sxzb7N5TO&&o0)nF|bl!04O6+5V& zu1BqfEvV;qB5m7?I2%u(o*P3xr{M$~iH~3fo}96e_ioQH$w*)LaL>^X;hT+pr5BK$eTWidW+Ms1BqskKx#aQ?MD8nPaF9opC6r zfk{k|QaugzLIV!Nxu_9rLruv(WIF91=*KTm8S2Y$NQMnWbtr(!XbGyLm8j=xQ5~&E z)$1&#pzW{Q)?wYH)yO7BcNN64kNks0N!* zQ@9k_0FFIMK`%a!>?k{evDk^UVc()&>`kxKv4Q;4h$f<*ukdU@b!Z7{q-#+F+KAe& z&tN#dhM{(Fy!L-51vTJHb6>m+HNpba3jyzXF{t5Rq&PPSyal;qblk-#2rx|R0El)%=uC82T;$Ipz4`| z>R2^u@iuzbTTu0`Ku4eXAO$U|t*DW`gzOyq0M*l)G0=siwh1in@OY zwQAo*EzWOHDGa;J9bqKqa~_WqupHH~2XHwa$2^>#qeW-75>xRlhXUKc&Y%`kPkt%1 zNRlxFb8!q-<5=8?f5Z;dlqHfLl4N=}hoGL%#|-WNQVO?oVF7B9e2sVESsaJ+$=8jz4^>gZDEEFQ>i!O7huSGr#nGeP zU6P8EInP7JXiHHUc@fp|e_|inx6df3;-695C6Y8KRq@z>xmb-4V>teZv+*3N;hD@| z4X#9u{7a0-Ur^7-vT&%{5-SVA`z})qxL@bz)zj=D2UbtvD5RUVy4- zJSJiV#$Y2VgDt2LE=Sd~1vQ{|P~VIHqIS>O0Qpyg%A-;ffvO-0m65@ylolZ04lBb9 zT!8u&+lm^%e(aAOsCs_DRP27a+i*7OxrwL@HKOi2mpkr=wsS!hwxN1{#Cz~V?8^Cf z-u<6Y4Meetb=`+*cnFqb5$gRQDpR{W_oGIB2(<|Rj;g1_p)i!fe{cZyEAmE+N@*D? zBekd(=b$pS5H*r}FdR3ap5KJ}E!c%>U@z*q!>Ej&L_PO4CZY2S13fB%UF3TtWPe__I#D~%)#6!e`gfhZ!k?kS|66=YE;MB;3jO!@{i8ly+)>;W2U0g!B zn@ahn;JU~pr_H4nf}=dN#-6719Pv1DfY5Q2xF=NN1BSN~DFok0D!5*>1dpSU_wd_G=OcQAj5i6BmyA zy~0w@r?83OqaWIsN#6B3PqCHQMrB{B$y&~Yy@ zg;+}z5%s~nG072oDe1VYy(4C2bo(4%are|7QzlntkH2kJT~ounx!FZ^O|uGp{+!(0 tZ2tUyUv6%pe?;)7^aH(0>*m(qQCH9H`Axw+8P)BHnNP;HA03_C<5x4(yQu&G delta 4320 zcmYk;3s6)_ zMw(^gVob+Q%)rxFgWqC>J|AV4jBjF&na{3J8N`Li2(tu?L$ymV1k2s_5*$st4jGHJ zqXx1K>D&H@jK!|w5WI=H{~rG6{zx2(NvM9aF`MUGJ{5J;hFRE$Yw=r5!_{NV3UM#S z;bqi=0>(NWkHRq8@u<%;Q8RzUbt@_(r;xeY4IGd6jHAu-EsjbkW}`YN##pS!N!Wo& zcpT^ARn)*HMVd{+CHMgDkSyIPR7O6#*DM8ZqWXOx%4{&sMQ*Wj^wF0!QPB)GqrSKk zb^Q=(#QiuNFJb{+N3v@Z*)SSN9%=xqQG4M9)KdNxV{llsnFlAMGFOV~Z)r67*8`il zpbTt4b^JVPr2A2O;Rx!!caUe>04~StsQXIE=OTOrbMSRsh@Yb#l*=d<;~doIFXIHf z6ytL?$!#vEqX>_)*5fgd_EJ=eYf+nOFKRO#blWFU-#ddrcplj{_9raCf1n1E&3dZ8 z4qSlGp)z#EM@1tW!~*F7OHnCbiTc8N%*3ZrGdY1;nzP8V+EvWJ|DZCJ!n8<=O+^i8 z5h}BlsDU=1?rT8})VGd`I^K>t7CopXcn$Bz_fVVVQw+x2sACiq@7zBEwRa*>9Ven5 znBiK8Yy&Gp4Qv(ac^$}7`fL{!PJ|speerk53A0P6HU9>ArUfz@eK7$wu&MmfjH*!g z*SoGq4QK~yK(C@EbOg2e`Y{kM;253%OH^iZ;TzNgvL-rTEJkId6!nEg?)3`PgKLm& zV~waK>qI@c2ep)MqQ2jU{yl;k(0SBAE@B|hx4*kLe2qc0Z=yQ5?K+sVrIbgYI`W`q zl!PHzfXdu#)b|&m?yE%gQ->PZ8r0@}%Dvu&K6Si@ieC2zQJd;GYG!{xPLTZ*m4UDX zr{f4zrpBX|E*V2`7HVLnsQZ_>*XwZ*Z7&W4m_3Q#r#&-?{LiPdC&~G7_!28=&z{7( z;y%p9fXU7-pN^Tdz39dLsP9K4yT=p@Y41g4?ZX?& z1b&8k(cHpOIF1*--XD{3Brd=(ticGK{}w7r@fK{sw@}AtBKc{@G%UiySc_kyI(j(I z`FuI*^J~b7wfO1IKuT~p?Zd8AdMPb1t#MQeG$bpY80_7!RX zv4zg@OF|u+DpbcS-1a(DKbvtJzKA+aM^Tx4A2q=NWdGSU)PzPCk$H z`cX6d8>+*9p+-KK!>Sp_VlZZ*KA(;n=tB2;E$YFmunf1LzJC$b?+w>msEG&pik;mU zis~pDQ!o{y@lo{87?sj4R7PGw?c&3zjP;=g_yGpupHcT;M*Sw-K=uD`)O{hdoQ(RS zsOZKNjKTucT2{K(SE3%!j(UYY@f7tcf|KPxN@=)(xKo~^lIb?sWB%i)W$Goi5f%t&X>+KMs66D~|-9yw@b#Dlb^1b0+J?@(1dIq%`%iZ=%c$&~5Sx9`3_>kx% zRuHccDvOEV5PBV)Bvf7|l<>90ULu|N2|+3IugL4v-X``D?-DBKh>iZ5*{`vch$V7} zd4!6V+e`eM*i6)PHw>TPJ4Wq&;yZ+120AX!5(kO9OE(Q2A+7x{i6;q_WTJxjEm2S0 zPfQ`S8Q&we5beaB(oCg^c$MI^=lfrMEi~p5eT2#kB7$gC{rTdrsH{ z-yUiztBL1{0-~1qDe+@sDxodfPpJHWC?PhfL7C;?KNpX?bzTVnF9)ctaa%|53F3#u zZsHv+;sh!Q#CqaR>2NF0xc&;;hlRhmsb4qt#!pZK9i4{ZVRX5eQRo8RNn%+x^heihX_tZBuRrhY39y{cJ D?MKmS diff --git a/apps/dynamic_search/locale/es/LC_MESSAGES/django.mo b/apps/dynamic_search/locale/es/LC_MESSAGES/django.mo index 9b4fdbe23008060e28f2c8476d1fe7987677e172..7512e7945d1f4feba10f0e3ce08f7812e08e3eac 100644 GIT binary patch delta 257 zcmZ20*d{pPO8pi_1_n761_m7l28PS53=DICGy@w01CYru7f3q+>BT_W9Y{X|(wsnA zkDY-*3`ko5X(=G>59Mb8X(1rL5lD*w=_x=Oh!|D^2@W8=5lDmNcL8aT{8@GeGX@nP z;}KAR3rMpARrvsEAs`L1C>}`j1L;g44Rj+zC6ERxWvJWS$#{xMg)6{6MAx|}HL)Z! zKTp>su_V<>!N|bSP#1`dj1&wFt&Gej&to~oYnAFK4~a1OUU{I7k2h diff --git a/apps/dynamic_search/locale/pt/LC_MESSAGES/django.mo b/apps/dynamic_search/locale/pt/LC_MESSAGES/django.mo index 31bf49eb597ca51c88abea66b9eab03ac7a9a5a7..85a80a99c71658b10fdbcf576875bca63bdd8daa 100644 GIT binary patch delta 257 zcmew>a7|#smHNeu3=DEC3=A3!3=D@@85kx5>DNFSh#2bG7#M7TbSsc{1IYtv4j|3R z&cGl7q=kUA7?4&6(n3Jm4oU|CX$c@d1xN!CLoJX1>1zej0zkSCNQ2bRXJ;^DPy#YG z0R^~#^i?431*9JUX^;kOAR!E-O@K7ejSL<@8YJ$!xs&k}lL}XWe~7MgQEFmIW`3To yOJYf?m4cChp`k7i85t=U8d@2dO`gZHlgB49FTFG|9VlH;GC7Pjdh85kx5X(2WS1|XAR8IZOC(rbXU8%Q2Va{y@r zb_NC!AZ-bx#elRQkQM^csZhECNJ{|u-9Q?M7?uDDkiIoQS^!9I1=1k($JiOn7?gmF zn?M0BApHkOdjV;7plXnYARr+Oq@#f}(2WfFQ1Oz@os6fLyfaEl3as??(=$slN|W@G z^KAj0_A7bqy?a ljVu)mEv!sTCzrGAoNUV)#qE=rmtLBf4pdUGc`EBcCIF%MH(~$) diff --git a/apps/dynamic_search/locale/ru/LC_MESSAGES/django.mo b/apps/dynamic_search/locale/ru/LC_MESSAGES/django.mo index 0591c4f58f7e59b1f9b5a253620098a845014d28..00f56a26ed56225c95075b9e7fc132c773a3ff9f 100644 GIT binary patch delta 257 zcmXZWy$bQjnGSaY#Uw^zu$ivBWVx&BZZen*7Ac#-V6a$}&7Yub z2AP!6Vp2BWEA^|-?|JRH$UAv?o!)(bA^}m~I*7HbrbPhwtrX<|B1Nm1$MX4b_l0Bp=OkN^Mx diff --git a/apps/folders/locale/es/LC_MESSAGES/django.mo b/apps/folders/locale/es/LC_MESSAGES/django.mo index 782395ee7ac6daa8facb0ea76ac20c645a93841e..8d12b0b0e1e651b9085d9f3a84174bddac99d1ef 100644 GIT binary patch delta 419 zcmXZXPbkA-9LMp`9DcT+jbX~avftXpVeKctQlgv3 zG?>Cc&g(t@h4yiP^C|Y?6q@}nK?fw=ab%~>twNmpdgv)!4qFrj<+6&en3hFnZ delta 438 zcmXZXze_?<6u|Lgg!cTAmIW$7&nToJUxh~cprNIt_MjSimgpPQ@Vz%UgiFz`Lt9g8 z5cnswRBJRu)Dlf?(a_M)_mb1++;i_eKVH+PGdHfU^WqcHG?94``3QBePg9h&5 zIp**kH!u+vS-}G|@d9V@5mWesag0Yqbj)B9H&NY}F^?w^=}4NOPa=y?s0t5{iV%v4 zCIgl*!TBuu-{=x2Ip5+KKBFH$aRR?^1OqXa!x*xgq;Lx7P`zJ|vF12Ii9`VRQ585s z)v%4KK^IlQD^wd?qZ;6D;2Wy{-vfX9_d}jPQ*+(pqG42Rx8{|zht0atYC2ZSZnliN y)wUXj?Kob=>8)wW&|+a>xu7qt7IXPtS$pUeg1g~ut5Nl=YNe=GoPIaB<^Ka5{yvBR diff --git a/apps/folders/locale/pt/LC_MESSAGES/django.mo b/apps/folders/locale/pt/LC_MESSAGES/django.mo index 539af2adda3e79808bb6754850b23871627bad78..d91efb95dde59c9a822f3889f3f52d13545c19c1 100644 GIT binary patch delta 419 zcmXZXKS)AR6vy$OftoMH%A^{kC*{_&-ZO%PpeSq!4Ejejy}_hKBc4-()Yj}6)gTQG zLf}$Md(dDIG`2Q51ku{}LU6gCbI$#D?oIaBeh5O%c1ViFq-jYVOS16|xA7bIurws) z@fu6`iUmx?r6t@zbN(DB@D4}t88g`XulI41JerUk%qG;-6hWE79M*9GZ;`k4j>|Oo ziD~joQp#ZgO@4s%qB=(K3{3+U7{x2(Z)&5tuY=?Gh-QB;xi1Y9d{7AE7mi{dQy5~A zP-!#=a%k$aIEE|8&@PjC@CXO7)_oahL}sk5@{Y51TJeM9dd*q)gG$l%=3Uoec%JRL dMQ^cn8GDLv`n77qud;j+v{tRd?!EOB{sZx{Hl6?g delta 437 zcmXZXyGz4R6vy#Xq1B|;`YPH%jELB!jgJa8sFR38w}L{qR7%>R5AqNMq5TJ(iX9!C zbQ3zciU=0R?oR#%4iNN z8<@jaJi%|=MQd85fK62AA8;8za1Ot41;cSamEcZt4zqZgkdCZUT+%SHk9mAUW{_Vz zWWd;rNQ!)%6xqNss@z3pkv_)o9@PMkIEgRFzsV5QeIs1LPgL)R$#aoeia#1rOcLfX zg$tNLw&VcSf#SGd!bP%!EV<^PK6ryucsqEX2x6Nx-)~!{>D2vNP|01i8m8OwY`5NW y&4zttH_f`|1y!$iq^09qh3rnj*xtADd%cSG+{@_Yc+qY;f$dZ+qdn;AVe}s#=s*ns diff --git a/apps/folders/locale/ru/LC_MESSAGES/django.mo b/apps/folders/locale/ru/LC_MESSAGES/django.mo index 6bdb087a35efe4bc5dc11a29c0e639f95b96e9e0..17af131f95395544501b41ad993927e63603aa58 100644 GIT binary patch delta 419 zcmXZXKS)AR6vy$O4Z?ewl~I;aNf}C{_8t)=1VMj>NNfl!Yk7l6Lk8j-qCsc~-co}Q zG(}5W5J*#FlPwKxO$|XqTSIf-hr;E4?m35h?>P_2i{xWH(D(>Qh9Tu7eVI}gr+TGR zEZ`Zo(a&#$q8ehvKSP8Il%;RB$D-n_}7#LI;7#JqAFfgzJ>777Y7)b92(tJSrGLYs3(vN^N8<2hj zr9T5{9w7f0lx7EN76kG|fHXIdR)f+;KpLdq4oHgtX%8SR3#4ON8T7y!s(}LHK!IK$ z4YFV}7#LI;7#Mc3FfgzJ=?6es7)U<@(tJSr7m(%z(wsm^HXtnw zrKNy050I}2rS*WcAdqhjq`85#FO-f1(jfh*Kw1Py=L2b3Al<^spa<5l2q+*96xaf! zK^B|=(n3J`GL-)eNV5a^Z#P#m&SLb+C@Cqh($`PVEXgQM(o4?I)i25~PAtmIFVfFV ztW3<)&nzx3O)cgM@DI^-E=o--$;{8wbxABqwNfxLFf`OPu+%lOR4}x#GMQ|~e0nk; XOB9b!VqSV_VmeSoYVqcDmUKn{s;@Nq diff --git a/apps/history/locale/pt/LC_MESSAGES/django.mo b/apps/history/locale/pt/LC_MESSAGES/django.mo index c1895c3b292ed1e6cceaa56f22a099bd84c26f49..75258a208b2ef07766852d06c38ea9cae01aad7a 100644 GIT binary patch delta 248 zcmX@iJ%@Y3nfk?y3=H>}7#O4&7#NyZ7#M(vVJ?sm0@90sG#`-O3ZyxK^bsJ<2Bgmc zX;vV86-aXf>Dxe>3rIf)(t<$x6I7f9s0bu30Hi_srGc~vkX8lKvOwC3l|c`zAp$4> zvM>us1NAc00%?$jCLj&6um?zU0O`q_D;Z}oDsu(+hv+&Nr6!hS=I80UB$lLFDHs_T o8tMX(k&%L-p_S3(Sms?kK8bnhrHSc4*@BYEVl2^{>sVYF0r2r9AOHXW delta 286 zcmbQkeVBW~nfg}7#O4&7#LQvFfafS!x11M1f)*_X+9u*2S{@Q>DNG-4M=|l z(yT!G50K^t(o8@O7m(%y(tVX0v z3nv0;pqUIyfHX+MN+1oga5IqR0MffQS2E6G^vWnHDX`MlPtPpLC{5B!&d=2^$}dhV z%FHj)&rPgM%+t>-E-pzw|fc$nK4N|`dN^gYH2cYzMAT0@$zsJg;2e#k`P(U0gz{keG z07MLGKtd5n>jG(zh5kSqWMRzaO2%1?%3J~dA-c{*sfi_-`FXl7i6yC43PuKohPpsx kWTaqdXk|1xmU%gkPhwtrX<|B1wy1P6GfVX5Y!+oE05RhwMgRZ+ delta 287 zcmaFP)5kmEOg$qL1H(NaHfCU8c)-HIzyYL1SQ!{pfwU};76sDIK$;IoM*?XcAe{lE zxqx&jkQM{d4N!VIkd^}S*8ype`jb%l29$mdrN04bNuWF{8-pI$0(mwD263Q(DUb#t z245hd2&6-RG|0j-APusxd2=P>EJm-4l9B=|ef{*zl8n+Mz2y8{{i6Kh#G=gnBK_RN z%EUbV%;MtG)MBmx{}5g0qSVBa%=|oEm&B4(D+MD1LqlByOI;&N1w#uflgVbx%O}5L Xj^go2%u6p#Ob4ncD&6eNqQV3KN4qm} diff --git a/apps/linking/locale/pt/LC_MESSAGES/django.mo b/apps/linking/locale/pt/LC_MESSAGES/django.mo index 35b5bd5ea866d41121433e06598e6a4f2e93c892..b9625e54787339d3ed304b53db5515c6a3a195a0 100644 GIT binary patch delta 853 zcmXxiPe@cz6vy%NoN@k-rItF@)Eg5l)a1Q|#DKLBaU--05sN`|8q7c7xRjwkMAV|_ zmP;dAwuxq=giug!S_v(pMbIvHZCeO^f5YqW?)}{Rd~?sa_xi5*rML03EB?}m674K) z&@&sukv2NwH(tRb9KeB?SvO8$JI-PPZ{Y|o;Rkg10I$c*7}_^X;deZPzc6psuww>E zUSwGLi{bk3FYT0XwLQ_fb1gQeBC@Jw;P8h1Sr~D|D3|H2q$o{{e*( zD|E#y*RYcebU$4*g%YXC9$JB>I8W0Fso4L~8x8s+OX$(pLurm|Yy jtW+1v{(9Zbrsv$J^s>8=*=i=Tk39D$S8u+}w|oBpUQtmM delta 1594 zcmZvbO=w(I6vyx6W0EGukN8nzO?y*~P0~yzDGKRGbg)T&-P|$@?p-aIo{Qc*>nrZR!&imba=G=47 z`QJC+Z~k_4<;rl+DZ`k?9>!iT81p3jt&109z1x_1_ygPuyLyb-4#(iVunK$Oi?9e^ zg39H=Id~GDhu^`s;c1E52p)%h@C4ih zKY}X!6F2}b!u#PmRQwgFyw~#I|AB+7izF#>z8PjB#|Tu1<4^@up$a<))nOAp2#>;@ z@O^j(JfHj5A&sr~-~c zLd;3XV?O3pf?q;Od>Q@>i=6TS`~{NM_*BPP<`wu5jG?aV1eCI$!4-9MmdQ5w6TAmr zhdQVW{Z#o7R6!$9{&{HOUU)aGL+wi;k9mieBtHdF+k64F?;EJAJOichn#&m}EeNHe}x4Juvse?&OooVQ!yM64$p@t;)Fea6B75Z>B1{ZiqdK7O< zrtTiW9>XM+y3yCY1Jjto@_%Mqu@TnlRAYPVkFGsit6?o|ctK*9qA+dRS5q%KX1yp1 zqlz7mTVvhHj2C$}2yNXv><3;w3ykgIFtW+AXKQH`X@5OWwEAIpqj2Dstmfu#`QF>t zc49o9{LdL%Kga^9V4_{tWZb&Z^KvCC(bW<^3}Tb5Zl=zce&RJcrg>Dny4d?vVc&9+ zG%MxulAkQ6hh}PFqa1~?i~KMuH{3B7l>InPy?Efj^9M_NBhTraQq?71#qOG&n=8%k zF74WF=jJPqKhe5SJlA@wZ*g$J1xu-0BBa^+wXZSt4AR-a*%~FLM-qo@G?O;bPfsRx z*w*&h{x@g$uYDfc<+V?Co^$R;-+r84;05mKm(#C^oQ;o4AH2 zIFGl(`YYPxA9#k}IEFh6kK;b-Jj6%5#44^9MW%7PC_R}aIAUW0uTT%%AZN)PPT@T= zD=(-od_&#$g(m(C^It5J=PeOJsi3aUq3&Nq{`+6k4=BsP%B#XcJ4w!6$MJ)togUN= z!;TY$v5JB)aysflb)6uNQ!ma=jf%D0T57avD;sVzyEfjlV{@;xrMiBqe9x_U@!;9q GE&Ks%GcyeU diff --git a/apps/main/locale/pt/LC_MESSAGES/django.mo b/apps/main/locale/pt/LC_MESSAGES/django.mo index 602863c9f411cd313b6207fdab9415a01cda052d..f0e4a1a726677c699069ce2665e39927ee0635eb 100644 GIT binary patch delta 330 zcmW;Hze@sP9LMpGn&)+#5T!0b2)2lvcn3Ays)IvNL0i)^n6z9X=j>vw4!5-y1Wkdo zM5}XKkl@n);289Ngoo$#{P=voJkPAMK6vwU)2EztT9itXR*tlfzi9Ux4Db(6u*Kmq z8g%giD>%myK4TfHT+&hv+`%TkVjBNz$q_=lT&@<^Eu1B3P g3>Z;V55qXh-isgpnMsBdGvs!ZW{GpX_;%Wbe@`7N4FCWD delta 348 zcmXZXy-EW?5Ww*{K$Z4O^T z3>bhZkU6-Fq*7(&DIj}_*M8qPjqKhW45*k>?eLTfF z-ryomu#C?b;TtaF5tpo#W1PnhK42F&QQW_|sw-pJB5zSx!48@Q8tIkrzwrv$Ew@;~ zdo+EI=->=X_=4s^?`Y;fkt1IO)Blb9_qVo=6`2<2bYZ)nrje(VAEf>4eCHw@s5ne? z9E7nN=#d_(AW5=bl3&^z#b&#)+pe_^y}kU-p5eE>16 diff --git a/apps/main/locale/ru/LC_MESSAGES/django.mo b/apps/main/locale/ru/LC_MESSAGES/django.mo index ea406179a23fa31da12baa3911e7d4a6e3109613..4dd6882e6c0537acdc8926b030d6648cdf5306e1 100644 GIT binary patch delta 330 zcmXZWu}i~16vy#jD~TF|DAW>hXrRz8ruH%@Mi4bVDg!DwKBa;Ga-XcZV(x z#p2+qQ)g)h{{+Fo!Og*MAaLC0-g|fV-e0f?rhabpnUfA3X;soTTZp^3g$W+vC7$9f z*6=N>|6rLoza~9l1&jEB9)4y1;yrz|F4f_IV)}0GX(Yqpo5UWLHlzZcV1NzW$1eJK zhnx6_X`|;XKgR>&C05bpaymc_T|CD0-!shS&<=4?GcRvf%3kBT8J+jrcF?`)MHhC^ kPQtiuOvH-g(3m71zc>^B%Jw=#+o9SYj$3YP_U^tq|M0#n1ONa4 delta 349 zcmXZWJxjwt7{KwTl|+p}6l#g+5O4^B?X@D>W)N|3($U4qtCTcI8_5gBsoy|^3=Vz( zK^%l`g1Eao_z~PXi-P}Phr8cBFL(ERI6uy_T^g0jBD;pjtcbMOTs*)f)VPN+?&BlY z@oQRl=0rA#H}MHiu!`Sk;qv^y>v&fZ@vuSujbd?cV#!FF94ZSUYj}he?4W}~+`?P5 z@f8>G9gB}Xr}+sg;$QTzMsqR1DVlhW#k&DAhcJh%u&Oe)%6i7@rRkNgR1l`UtW&>; z`YMhR9fwh@`ubWARG1`LH_0!JRjbjiZMWTK%iqawjEDTtJhPAWAjovk_1!od-%S{#bU)-8IL4?4oHnBVJZ4iG>h#M8rcFl#wivb_wYsOqb1q zAY{RV;Kj3IW_0lnh^1TDP3a=|1EPyJ5qf`dVVKwRSEr*Zu(RB06$_smhl!w(^4CDR2)L4^b-59 zm{zDR8aoWw#z$xi9XhwM6K#BqmnArdv$%+N@f17Jy&(4_(BgN{ z68MF+xQ8`(fKh85XSS10&`|muZ>jgZZr^u3FXxxO#(!QJnSNcEpDxVg+?h9}g~ZhM ILBenR4-7L{y#N3J delta 886 zcmXZa&ubGw6u|MPZC7ojNySEOqK$2}rgpncs$f<`O@)Hkv{FSa9$d?khS;R+kD{j) z4}$a}ws;aR9=t>?To`3ipCDdHUzIn217{6BvaP2L~jdECIW*u+5`O^Nj5Y|CY2 zN}l3rtfwTBQ99cU=*BS`U1;JEUPT>Wq0t1_@gBa#lbAdrGJ;ch0X@`p4ZMTjaTxPt zu?HQ@VFh*F%l?SPbiOc<#h$bX#gIwVOmATvLp+9$Q8%pP9Dc(Bo;WIU9VR7O(@Sk-cONxs-(1hwFG8-=QYBg+2He z^#a|LLqF*Vc447q5w)VxJRQAJfSTzG)Ka~|1inGt@DuXL7L6YG2Q_|xQ?)`Fatt$FoJGpGp_QRCc}_tD^e6o-0mCgT49<4bN= diff --git a/apps/metadata/locale/pt/LC_MESSAGES/django.mo b/apps/metadata/locale/pt/LC_MESSAGES/django.mo index c4d0149b8c094f43fea97c9ef7713eb01c0c1fbc..ad93c5f0759515c93d51d4b5a42106085100269d 100644 GIT binary patch delta 825 zcmXZaKWGzS7{~FS`BU2(X{;ogG-)rUwg%hkB}L^14Fqus8lhB5rwk=XA#|vMi-%hg zio;EHaj=WHh=D>2!9^4iqu>yyIw^E;DGoa6_h||*@8`YueeRy;ec#)!eyqOAg#G77 zq*6jkOX?=2)3}f4@z^OT#QJ4?8In8}&oKX!;3*s!k!J7`Ud1K!a1$-=JC0%~(=&&b zr;IKxX5{MvgLNhxY~wiY;#mx5C7KF!DZGJAtm8TSf(87G*D+5vd%laeF+khM4;;V~ zW6~^+puJxk^Q8)d2TWKYZDjNMg%q!d!%OI(6;{Lvyn{FKAzs5zxPZrVy@U<4xBw%# zgGccV()6Cr3fuD;OfyJuUh`PN5T`CSGUxOceF&o(V_91jwp1-i&7tAZSk>ez>mFpg`;t!V>8xP>v? z#(sQ<2Z_<)xr;Pkf9Ne?mJ1)oN#uW&hnBF0R$vpYpmnr@Hqqj@FoGYDrVd>Jf1|~X zau1gH;-Od2;>#FSm>;K=>bo@*JV~SmU3a=zbX?af2Cox4v1+~9xL0p1dCtmeu$)|M JeN8?a`VWb(Tc!X2 delta 884 zcmXZa-%C?r9LMob)2%W?`{Bx5dHN+Sn`fIsI-)2QLxx&JX;5*BF( zK)MX-B8a-E&?~R%w%zm(2nEtj6cJf>tM^xhhv)U2=lO2m@AEyUn*No38>#a@Zj!oN zqQ&>fd3q_;@7~SwZTAnN> z@P0(TTo#LLIJkmGFt}ejipP5<{Npl#&m)MMNkhiDz7{Fz0 z#Lu_|zvEhBba<{I!&f^m+7b@nP8>nLM|o%oXVD6Lf>zKXT0vE`_+_lepUBWEQxx0i zzr_urCBCxZ4Yc?y2305BUT6)CHVsdf%QGq0&F0FJm786+3e#?}Q1XhoLeZV}?s$1O zS1MI9rG~N5OR@f9#w+Iv`PhJ0&ZL}VJdudS`(nvHC()bgnSao-G=DLCbz9oYXDeQo Mh?#{~;pa{N0ZAiq$^ZZW diff --git a/apps/metadata/locale/ru/LC_MESSAGES/django.mo b/apps/metadata/locale/ru/LC_MESSAGES/django.mo index ec8d146e9ccbdddeb468cd645dc18e580a6f3720..d58dc848fd64c11ba39f8b83d3474a2a83aa8590 100644 GIT binary patch delta 825 zcmXZa%}Z2K7{~FS4Y)A#jm8?y9MdMPwC;>AHTE_!mEu4OqLvGFP!x-S31{WYg<6Zm z#f4ip5-kc{C@Sj4MbxfEkklU#Zh}HELBCH5!+f4|?!D(Z&vVXdpV#;Cg3tY)B}M$w zWl4GGq)M#ER_wt^p3md1PYMR4+svOSJAVEI+Zeyae*A%*c<-WgA3bcwuUL=y<7lG6AFZ9{)?3S(5$j$3#Z z3#+6@7{crLvWnH76U;H;;2&g7g=j}LwqPlyu?eTqI9c;mXOBM@~n8EjGt*@w&3h>2=6KEe!qqXugS}S!`3WI|-9z^z}S**h~ z@6GrFIgV2s4hJ148uk|b3%QAb)Np2C TcqkftmGOFtdzZF~_s<^zX>(>N delta 885 zcmXZaPe{{Y7{KwztvpEdk2Fow`KM`r=5L$Rwq+<~W$KuL#9Ea7jIj-ItKWV*bjTlc z6Aw{C7}Uvvs8gUsNC&UpqC-c6!n*7bbt{Ozj~fg=@AK~cz3=-x&s&<^n0?#kcIsRr z1C1glL}c)&NH^ZZtLWgvI*~1`xJ9nCh(wqV9k2eM!%5=zcn9~ii_BmOM{onr;U5g) zP=|;g=P`rNJH(MO275iS>jYjVevH@fGmc_kr$`vH=-?_wv8zjD3UA?ke1m$SHnLd6 z3{K+@Jc8jKkxLlEGg#_jw8Q5E5*oS#+#WJ47b<_$}a1Om*ksG*#Hh#ll zO!SIe#}%x{3hu|>cphDBK8~YKZLowzCO)B-e*C1!L3~>C1?q?EsHOacTFTy2B0)@` z#>>dv2!3)CxHB3^cPc>J2}l9{dHlto%gUF3qeS#31T{89av1 z(7<=7nSVzYcKfOada;4{0_wgo9Kzc;pxs#W83$lhLO(Nnc`gk z-TZ=K*+QY1Dl|;TXM7i}lxb)4Ip3IRr=p&qKM?TwBfen769`8` brRBy~rIDuOz9}=8E}CgpSjDw+)8FR*zqWhB diff --git a/apps/navigation/locale/es/LC_MESSAGES/django.mo b/apps/navigation/locale/es/LC_MESSAGES/django.mo index a94ea1cd0621ef3e745a20f0057378112272595b..60911107089abd56734915f298f9673a6febe42c 100644 GIT binary patch delta 78 zcmX@hx{P&#jqDai28Q_z3=9fD9KyuFpaG;yfiydio;NYFdg5 za(=FUQGRh^QD%OTer{rAVxE3xadByCF;{?ph^}){YGO%dex9yNVo9o%f{}rtp{{|Y ku92mJp@o&n#P6pkpJ$BX@kz`}FHKAbDo8D!tiejm%g|8Q$VkD^(8_4C d9OEt?pTxZM(!}&sD}{oR$=4a9C)+b+003UX5`+K% delta 137 zcmZo*UCuheM)njV1H*g<1_p5;&SGLE1OB2(93JNBRF=YV&(Ca8a diff --git a/apps/navigation/locale/ru/LC_MESSAGES/django.mo b/apps/navigation/locale/ru/LC_MESSAGES/django.mo index 61a3328f93422aee2cd31547bd71926e0b0f879a..8dec51de246ac087c61aa9554f23ac9a09a948d4 100644 GIT binary patch delta 78 zcmeBSd&)Y&M)npX1H*g<1_nhSu3}$(WPmHYg$|xx*u+rC0&n(F(P0~xw z&($x=FHS7V%rDZvQED!3_r diff --git a/apps/ocr/locale/es/LC_MESSAGES/django.mo b/apps/ocr/locale/es/LC_MESSAGES/django.mo index d70f38f4ed94961b625c27abe1ce3e2fee4b5145..9c4c0ed1f323144ece5addd8bdeb76a5afc85810 100644 GIT binary patch delta 863 zcmXZaO-K}R7{~EnsEe9esjFF5TW-?JPB^Q*Es~l-N|C*J@-)O|(V}2#(PhvGj~#@G zx<=4DiqW1#kPc!H>?!Ew;H8U~q8B_!^?i00hW-4X|IB}$m)Va!Gd(kjBLB?>X{}oF zCB2GDC()^ux-o+m&teCD$3ArH!gvBZi5Kw{7H^Z<@iZRBDLja8u^(5k6_3?R3Cz@! zb}NgAY;43A7{@H`!7l~>;$Gs|cIgmyVG|DHehkpX1*8`JzynyeL%NDBcmp5dIgB)f z4fHqAHqR<38-g{OFOtjRLfSpQ9yyhb8zP?R~kz z{wFLZUct@y4SCMj9~PEy9k*blF|4o-nUWG{m7YPn?_m{=pq;yi`Hs;h5TGS~jCSrN z+9z5>OZ*w_+-g3ih2Jdf#U-?9oz%{qi2T|VFF)UR+36Zdd!yHfhn#M2G@Xhin_bu8 kN+x5jn@S#?zFo6ian>6e9PCtk;3RAdBi zVG`XaX**cFXQLUv;9*?HLm24x?ZaN;3pj{(upgh`0D3r#zmZy`Yrn`*OyMj}<6ZoK zSI~(04cw2>Hh08hgO9B7W#A@`Vxq@CcoQ|jGt`8ia1v`sb8_*3U)T%=h#w=v6c@v| zh*S6uGZ;GPpPz78q}W)%6ZjHoUe@s**6ML7F2YlgN7#bT@hvW)j((PL>HZ=%;Tvp2 z4|O6RaX0=#E$A<5flecH)zNoj5c_c#n%IG9)JJj&`>=%DqeosF@lXr?jGFil?!w6ifQOoR z74_a%)JL>|ns^7@=)DlOYYQ8Ow5jxtY2%nSnNv+aEH62?GH28YbR3EtC!z>yH47PPVUg31MQ|74 zCUQ|OvIt$YsHNf}i=a)rBt(LQ$Z8Sw2M7fb_I>ffz~{W@+1EdGNL3{2aX0akBoyHuV z#D{1X{2cQoo>6RvL{sWSd$E8=@D3iu8MKan1lG4mgVeo9c)Ev&aSAK(0}`f|umOKz z0srA~%}OIIY|Y6G*eVkEE{`=-^wN#|2E`SUi}=4K|OpxP*kJ z?^un0kscHxW^2$-FtCL*+9w#m&6vlncma3g6||_w(V~BecJps&4XI#DZO{pp(>Lv}Ijt-DP)H5W=B< zK#09{@X{?j6zf4RQHUKn)G<#%1c7wRq9n+^4?CyN`@HYW`#$e)W_S8<`sHww_x^~; zpHm`BBC;J3vCuv(GJ!6R4_d`5xQX;2Us0d?i&Hq*Et19p z-o!1`3m)z9M0iFfdqikX?xViAgh_mkqxcE6 z^orcVicN5YjTnJb?#D zXwpWk$FU#jN#dvtdTD>bLHz`e@EE#y5?8PvpQ1*+fg1g1)SI`msF8PL0F&5^HtK#8 zetiQqnG6Q;Hqxjk_ZYM>F^3_1h#J{4>djYCJAL7=zd>#618SpRP!Il&y73Rc{)hTW zTP`$`=|xRs2=zT1JM;=u4D=_wiyBq7ZUugZ1{PemWE(~N}10!K> APyhe` diff --git a/apps/ocr/locale/ru/LC_MESSAGES/django.mo b/apps/ocr/locale/ru/LC_MESSAGES/django.mo index 97665fd5de89df50742127613f02140c10676aac..2532ba2c7164c9eaf4f97d0b18b5aa6f07839578 100644 GIT binary patch delta 863 zcmXZaPe_w-9LMpGM3%8EvZd8#E4Q+8>GNbpqm}&uqcSr_QJc`Br9n858A8S*h0u<} z-Ruwr)uA9VJ9a5LjV>OfP9B2(93;>r>C*e#1`oWxzrWw#_xF1iQl-?ZmKENYD(O$1 z^hVN*U%G`4Hc1)$jn^<)FI~rXIEqaT(f~ff+gP(%8o@El<1${w`&*yFxKys_F@93aSShD2fJE-9?79;tgVthV~+Os=5pdb z%CDt8j$D$87{%9UH#~>!xKQc$nmN90d=YKoJzS3kv_y~57JS5G__t!5MKB0WtbU(vG!xKS&a%$W^-|!ECp>$aQ delta 882 zcmXZa-%Ha`7{~FC6&7gI%H^-EzS$4^p>CQ{8x}={pd&FNXl~JLa~q_K{RpEU1L-CR zx-j|!DwHB_xnDw94U3JE!M1& z{%w(7N?LJCSMbGFDS@75X&h%Tj^FVn4z)-re1q4}-zrVxJl;j8O}d27FpPEWmAna@ zVEhe7@svkJ?IL*Skv8KZp1}`z9KBxY0$#)jmT?5#+oYqIz(IVBw(lqI!{Byl7~`11 z5=PN+L`CT~j^V1qsFxtTLpp@_@Ek6o6>9FNBqq@sd5xXu-C4QmY245FBifBt(AGoT z%7^)?Pw)Zb&uERNche-6iv&Xi%Q%3?_DBct2EM|1Jd06IwfPcKLtoKVL$H@3(7?5= z@EvsFbL6qKh~4-F?S6q2ERcH85{DN8wSP8trt81Y|59;i*sGA00M%rvLx| diff --git a/apps/permissions/locale/pt/LC_MESSAGES/django.mo b/apps/permissions/locale/pt/LC_MESSAGES/django.mo index ef53a7d2481186c2c12639b7b0ed47d6a3796225..715b0f389ba40b09480ce6eeb9d3af97083cc62b 100644 GIT binary patch delta 752 zcmXxiKP*F06vy#fs;#!Fic;#&Vi2il6C#}~8bl(JnmSb?HX*5qMADT(f*X^CG_e>B zA~CSo41&dAMPd<~|KH#9+~oE1?tAy0d(OGtOl(BU4>9kqVRUjOxxyY}dhruA_N$DU z!UGKBtLrxgnTM*4slhllU=kD9k0BgGALcQJ1=M#7SZ_?(thx&}(94T0RKQ*I;~q9* z#eIK*wam}31usznA5aNAqc-|LB@iK~;WK2FikGk%*RU0LPzfBN57^@Fzm)bSjhSPN%b(A)EJ~h9`>)^UKBgxor1h$%X?3TMG2qRPC(I L1xv@d(1_<3K-@!D literal 3213 zcmb7`OKclO7{`ZFpcqP^EwA#L1}q$vwVf!1)FGv59*~NtRxmm6P&L^d+Z%Rw%*<>Y zSDbo42!uEwgx~-wIUsrjA@2B4!~qGx2_z1jkPzIE=#BrkUdN6SJ7SdG-|Xzn_ukL> z?a09o7)BP?gSh5zVC)na9Ka3Z?2U{)2hM_J@NMt{_$&Adc=8})7s0o{+rd9l{sU(4 zeBdU=@W&40b{BXQydTVh=fG#cdGIoL8+Z-88T<)60{#M$oWH;k@Za?PA#8#QHUeUa z9RqIxkAp|S(e(Y3;H`L`2JZzIK$7<=NcPl0(sKzU{qKS#_oMXr6OjD63f>8R1CqY) zQvLvbkC^=ol0TmwV(dKlJ19U0%3lWG178CF1j&y@7(x11L6TbsafG$NW8h`*IQSvB z2!07toUVgp=Na6{-}B%-;0kyc^g;5!nZADqglX*k^!ekIpMeyQt04LNEl6>?22vgT z2tpP68@wC54&DcjAQ+gk97y&~f)u|Qko1;8lIwva?+uXjzX>8t$(bzp5Ux9LJ%Xz{ z$ZzuDM7l5vQfyFaiB7VMY<(CP>7YTrcL(XA+9G?q<55f~P84ez6jK^xE7?sxa$GbX z>ma#5&^M=i(I9^)_Y?>EJm3-vIp^AokqG6ODe-Jh%hzLRtkh$MdrF$MW~mK*A|eZ$AAT7|>p&n2#O0>I{FAgxRv7Chgk(`PAkEV=jP%CM!##MP?r z`ZBWpFY>HBIHTP}P=p3MK3qh{d2c`O`h^&_l0+;;zqxGHm(A4GPHHL7$pZ6|kT8j0 zut2QIfQ2$#qqL`_I->GHksC(!p4?1BxR{}!_uAI0aMqjhx(Yn0cdwYfCBsNpa!V!m z&pU5-xRz8lGZyvA%37^U*E&m{vkDLJxs~{`Xo-j~%rC!?S&07d-`3-`ysN^3Rz_%F=|V_n3;1yHWoOD4Do#>N5bMlCU5QT=$H$%GX=maz zAD=2sKIIfAi$!d7kO`mE_wS>v6TCQCIyG^!h>>|-80%C}c_t86=@OSAj8>6R_(TnV z+LIgkbm?p@6n;R)ox!oF7K@s6DpG_cUWQ9?4F*b{OI>L;n|Vue{i@u|N75D=oxqqv zqvMk!z0eda1QZ&!FqT7g%qjGe2==uaaMa382nmEAO>?&{w2^k^#mb^H z)!Rq7pWd zsSQ#O^Ry#*aHkn0n|e%5=;2``mxqX+gZa7_lTL%(u|i#T#{wz~VMQP4PPTnRP`iCK z39wJyx6^#3*=}{ibG2VEhKoT~C?;N#>A`OIgpkS9b`i%e@pHPvBXpv^08zd*kJN@p z7q=9Xn{E*MI_bfXquXEEI8aIdMJwu(!>Ug4ZJ}#r(Tmm2UQB_ZW{fkt_V((WSs&72 p0}{J?6}8;APTqC*(7dZYHK||AURL&>C9&Jbx4$8V|Cmtz>_3}`n0x>L diff --git a/apps/project_setup/locale/es/LC_MESSAGES/django.mo b/apps/project_setup/locale/es/LC_MESSAGES/django.mo index e76f92294e01d80388ddab0fae7c625a6b3279d5..37f3fb805306d9580ae222ba8e96d20ffc539e48 100644 GIT binary patch delta 70 zcmeyxa+zg& XVqSV_VtT5TLTd5kJ&e(lRhYs6MaB}J delta 131 zcmcc2@{47HiRf)c28Ln=1_nML_Ge;XkOtCm6FuX-GfGMdtn~HMGfOf`lk}4FbM=ez zixZ17^NaLz6Dt$*^fQZ#OH+%v0{laCor_WvOEUBGbX^ilQmqt>3=9o*4J>tyEENnb ftW3-&o;fvnF=G^uPhwtrX<|B1MQZWnKaAl3!PO|& diff --git a/apps/project_setup/locale/pt/LC_MESSAGES/django.mo b/apps/project_setup/locale/pt/LC_MESSAGES/django.mo index 912f33b52af2f53c4672c27cf6dc7214c8292641..7c882520090ae4c5eab03716106e87e8a90b1668 100644 GIT binary patch delta 70 zcmaFNvWI1YiRfZR28Ln=1_mA=7H0xtAgwadGk)TENp3?!T_Ym}LqjVgvx%Q~^7tg? WrI#kAr&=i#luTaD7(H2-DF6T~#1aVr delta 130 zcmdnP@|b0UiRdXt28Ln=1_mA=wqs&o5C_uU6FuX-GfGMdtn~HMGfOf`lk}4FbM=ez zixZ17^NaLz6Dt$*^fQZ#OH+%v0{laCor_WvOEUBGbX^ilQmqt>3=9o*4J>tyEENnb etW3-&p4mBh5@Qs%PhwtrX<|B1MZx4(j6ncpZYRtD diff --git a/apps/project_setup/locale/ru/LC_MESSAGES/django.mo b/apps/project_setup/locale/ru/LC_MESSAGES/django.mo index 8bc36db74190e19249a29578d270163c8b697c7d..6eafab04931b1807af2a58217a4e5e52c4f25b7f 100644 GIT binary patch delta 69 zcmbQq`iW(NiRdjx28Ln=1_mJ@_G4mT5CGDN6FuW6o|oh{G}JXRQZO{MGBTU^c^QvS XVqSV_VtT5TLQ(1D8H~}Bm}UY1G|m#T delta 131 zcmeywGLv14B3i0|Pe@|7M(M6{Nxy;2)yvT$Gwvl9`{U>ylWKYNcRgU}&fd lL`FsmhK5!~W)rXPLFUl`YEXvF; z($7t-Ow7~IEG{lhEuPpT$!%b%Yh%Ar$z_aD+&+nU>7|M3sa6UFlg}|) F0s!bJANT+O diff --git a/apps/project_tools/locale/ru/LC_MESSAGES/django.mo b/apps/project_tools/locale/ru/LC_MESSAGES/django.mo index 728bb26bddad734e2300017ef9d107ecc791cb66..99daa2c0af223caa95567c215f7ce69706b7da41 100644 GIT binary patch delta 19 acmaFI{Em4-KZlW#f}x?6k=e%S;fw%8q6RVm delta 19 acmaFI{Em4-KZk*_f}xp}vBk#e;fw%8%?3FD diff --git a/apps/smart_settings/locale/es/LC_MESSAGES/django.mo b/apps/smart_settings/locale/es/LC_MESSAGES/django.mo index 0fa0826b1c3f84652fdc687cb923f9f157146f9a..71eba7fd8b3614a732e13873e1ade605bb429df0 100644 GIT binary patch delta 88 zcmZ3<+Qd4+MRf}!1H)1V1_o{*j$&e9U<1-=K$;Io=K^U~AYC&tvv4vaqa?SXp{|jU kf}x?6k=bNb##1~#iFxUziRr0U3aQ1D-!Vo{4r2-i0GrhkmjD0& delta 147 zcmZo-UCBDZMfEl#1H)1V1_o{*Ze(I$U<1;j5~RJ67$ka6Vp?z6becv-(`%R?8@W^0G!zpRR910 delta 146 zcmaFEGMROPi|Q#x28N{!3=Hf*T)@P@zyhQzfHXglt_9MpK)QEgW}$aRNlAf~zJ7XU zNk(asUUGh}eo=mLVo_#(k$!GsWn!LwW^r+8YB5)Ue~7MgQEFmIW`3ToOJYf?m4cCh pp`osUrLK{sf}w?#iRHw9J0~AujN#28N{!3=HBx+`z=Zzy_pe0BIp0Js(I50O?H=GYcm(GD>nA8tNJu kDHs}B8JSI1Wn9MNlbDxYnwXwyrBGBl`4nUHWFw{>008_HA^-pY delta 147 zcmX@i`igaeiz*`%1H)1V1_p5;UJhil0qOlfS_nuV1JVLO`sT#ULhp={k^(Dz{q)R| zjM5~%T8 zia>-4Iz<#pR?r{NZ4U)|=vJ2w(V^`UguOpFFwE;a&ph+Y_xpXGPs7iKU!PkSKW~tJ zw@6Kr9_OSG7jYb`*pHogiBRJ>ikC6K72J+B?8ZO11v^@$&Df8AKZHl{ET-@gdcB2K z#o9sehJp21#cf!_tyo7d*xV+yV}Kj*Fz&+<Rn8nk$2dA(X@1Z-gjEC?C zx&ymuhqoZ^A#epl=t|FE8ZV-^WCqy<-M}uqk4|(MeP+I*x9A)C`#N$Aw3>|PDYud5 zF7*5&Zo~*lB-S8-56cL;Rj1GiT|rlV2c5tRq&fPC6t5b(k~(_fzvv$~>|Eg%4@^q8` zWrN6{Hj!o#c^?vq<3~J(RUE*v9U_Es87FWNO3mn3q*pDGL>Ap0N z-Izs9;36jRN{26UhG2z>?HKN?FNok2<0uAnA!?#G(Zr|77Wsty?l z@fB)E)^HeGX@hp)23QHPmmWiE{ML1E~8A zijR<2RKxvP$9))zNny-k z5|75@$tAeNf)(%r^EivP{te?8Z}Ag#;!5V1(F&VDOWeR-46T>WV+n__ig}EZ#11Ut zGQ5pe=>51SJtUZCfg*KqgMUB?Z!sSU=AV&A-x;=Ic%#IlR)#%z5)b1|w9eEpkH66l z&TW#mVHr>0B(~!RkKi1^U+l*I&1}Y}ID`#6h3SNU!y9;y`CHt9r?*Jg@hbA@D?@2Nmw3Cef~pP;p@jv4%g2QanMzu`#WBwC^ww1aEN zR=r1R)Q@1@A(`z_!ROd2DwN}uBZ&SMc*r2K?swAMbrD9)mF<_+3E^RD`= v>2aje+uX9;b=xwTgzI)?vXyJiGm)O*E2HDXqZhjpW8;;WGf=(bOgaAnaS~{; delta 950 zcmXZaPe{{Y7{Ku-1q)eWy3*2gWoEACZySrY^>3sQlr-J!534nP*4TD%ll_4v(heTh zx>VR9FGU1FSP*%rLzkjkgvYGIz=PK)+|KoV*x<+S^Sm#=_j#Z9d4C&I_e1Y)wwAWF zh-`I=SVW|56}g5jdqfs6f>ZbdM{s1X$YTuSIo!mPIMgjNjZr*??~x@^#S>UV588V~ zc3=#LG1(&}aS=RXLJRnU0bE0!@7O2Oi|0`j-NIe?1huf2sEO+sMEib{<2*+37Y5Kv z65X(f?f3$<&^Ns$kqW^&6BH>keT@y~vBdq8)2Ejsu4q-a)!6uTf9( z6?J_Low%Lq_&vmn+VUXk6-TfG6G;A&+$GQiWju`^Q17aSZtOcE;=>^72`f!kP!oMe z-MES@k-tc-(oGVbKZW|;Wz;K-HC@1A853D1&`Lk!4P3_%dX6>{mQgo;i@R|RwKE&2 z|IDA&<+g{GOUZnG&TklrR6bdl8NHpEHL{tUnN4M~#;jR1(?%+nE5vi{lfh}nST=6v yQ<=15+|0-QHn-E|ayWesx6kJC`aQm-iLO^m|GFZU2{WB2mF=7 zrTPsz_yBR$$2LmC$d+t5w+{fAYr_=a}_;(SJB|~IXM1D;X4>Jjoc`TyF z^)S9ioA?W(XjmdKOopa$hG)JI@p8K z`(F^pNx}u}#%c$7Cd3Bt_Yn;aSP%YB+nWQf=#h}Q-GF~duA|k;ukr)o+0B*SP zr5nFt6Zu-dNFhehi~X)cSVuhK&d*^h@gjzB8yoQ)^_)AuPU zSd1am1qOED5*Fbu>bgDDx)aokouSrUVlO^n7kVgGL#Xpp6?u_P1{)*-c!=7GD{RMm zq&f28=6_H(2vmxcU>vJ)6zg#owIi!og%;`qcaTqx`02U})Xv<@G0>Mhqqgn^^(CLE zg}+#Xx|<#lK&|U`9Yww1JZjxCHsI#%L&3FoV#RUR6NZsWJ1e=Rp5^SCVP$R8N@p!& z&D=6GM%uP>N!vFwJsTdglBSc+X2Rp9lS~9-k!Um=iHBqHV019i7e6>HeLQe{3;szn QlggPXN|N^NlkeF32R(L8T>t<8 diff --git a/apps/tags/locale/pt/LC_MESSAGES/django.mo b/apps/tags/locale/pt/LC_MESSAGES/django.mo index b73dcaac57face1fd1866f6e6c120534dc4e3681..db86c8cc36bd83b989e2ba849e1e37d9f03ea8f9 100644 GIT binary patch delta 527 zcmXZYKS%;`6u|LkW_jLe(H}`DyG+}J?g9sgT51R)2pWQ11k0%ivX+X2nwt(9BPggz z1gDyUT7s5naA_+dYOFNUFs{DxJ4eG%M&0 z<;WQhVHLH3d(;O%u?xRZCpSrSBO|B-WUv<fo>c{4~}COn$>t7dx;Y`ifQb@0}SGsPs-BCQ%gC?KCW44Su(^>~0oc#L7ZLM`AO{n*qjGK|6I^8X~of)=ua`ocpr z@B(R$JmLVppce3ndSPD+T|+;EzC4WDkpx^E8$abJJbGC4&}Jg+9) zqQ2w~+wlptb#K^>Kd1+`wf+}qpyo}WwtV*T-d*t|Hf_5QGtEra-gMR`*Yn$EF<-KZ z*?iI5wsx(YnJtx^bjiEClnTrh)0Umh=K^udPRERJFcb;|qk(YL2+hPI(W8@w>!Wq= TYGcyMWgII*O5yUx>v;YE?6pl* diff --git a/apps/tags/locale/ru/LC_MESSAGES/django.mo b/apps/tags/locale/ru/LC_MESSAGES/django.mo index 54f236cd26eaa231315d1742cee62e0a7d7e8fb7..9caaeae59d439a2ac82272fe066fd84b2467f25b 100644 GIT binary patch delta 527 zcmXZYzb`{k6u|M*2;S=t)rL@nrdmYu#Ct)c#8eE$EKH3^Q$q*I8|D20#PTdA9mLo) z?PL{;2#FYksf)y7qqfHPBu#Ga=l(eN+#@h2{mC0_2OR2 z(w~T@H1X%FO7_y$h?^TPVqnos^wS zRyI~7WjBl2+1iSYh5S#{?f$;UIp_P%_iB6@H=h-Sib|10y@)9y^9>?P=>GeF3(Wog zKi4tJ{2YB)!a97zHhjks{J|zP8byXNf)U(8-9N?z-Ze@=h8TRa(20FyX~8IV;4HS{ z7H;D{>II|=ZU4z;Lpy~vh?Z~~`s3OyXeTkOHozaN2uNIwgKW|2{}unRY^4;|D_ zPcVq*IEgPvlTzCv!cf{!^99X&XR z`VukJf@`Q3&7c-ILyfyfK6&J)apmH3)lK!%p6edOEi09F_q?6pZZ2avxxDS9bB>j< z5ACd#&gZ>k-nY84Zq7ML+fC=PX2N!p@qzJBIBbSu=6Gx%9F0d}rx*2Cr>nkg|FWG; Od3K5r$17g@jJjVe{7-%W diff --git a/apps/user_management/locale/es/LC_MESSAGES/django.mo b/apps/user_management/locale/es/LC_MESSAGES/django.mo index 00e662ae5668438d3cc7d16e11955224dab3f66c..ff5716ae9b4fa9195cfd4faee43e10fe68457e67 100644 GIT binary patch delta 479 zcmXZXu}i~16vy#bq>Uu48l}~?f)R`gVkHqO6hRym6v010mkw>sB1jP&3K0cImn<$q zCvg$QIO*Ub2yP;7y69S*M8wJOkwD1j{odtX?mp~0`|2U#-y}pVU1UZ?UJVfklj+Dg zoF#jh#Zw%>FwT!yAiv`<{^Ag3Gf}^UE_o%kg{$OC9KpYg_%cpW=n)yiMN|uQ9K|Cv z@f_7bZgB#?u!Ol@k$GIgCLZ7p{@@{Q^+j_#u_nW8`eiKQrs+pF+oLf^;}}zThCFh? zOWpV$yYU6p`WrG+KCvIaQGKTo__q`>iBrgB#X+@i0o9yk%;EYZMV_LL>Hzzw;R!NR w+Nd^MhhA4F6}0rc;W*Q-YdKEEEeCD=DZN(RZtPYYwTe}12A;7IJ{zX?4;_U#s{jB1 delta 539 zcmXZXze_?<6u|K#m4%k2VZSUi$ZANRT7yar{g^69q9B5TSC~FXE#EU}a%h$U|A2;u zXb6I6YG@IHAX*}*rW%@}KOh?VUJ5SvbI!T<9?rcEz6URAw(_e@B;XLyMC8XQ62+0` zKi6@T{0O~R!*+c6&%dyTyuL-G7ya0URuR`L8h<)7m?4(3aWQosQTov1NVjrx(O<% z1~|n=yg@a3i$B}M#Awp*Y?4cMHc;42VG(&F zOQ;`iU=-^Z!KSJ2oBFZIUn09mh`P@MGK)N!`YY-_ADG6kaW47`|4={7@V`A+ME%t+ wM=#EWTV3Cgpb^X3N=Mmpa;awid^)nIiv+ak13&sga`_5A$o_@^g0-YG&D6jG&Z_L z&!H)brWzV*a&CxxtOk}Oe+%ugZwGhde&N6#KEvoN=isMXtW|~T-2Q8;`Edq9B|=8 zYIi4g8kdzKXBQ3*%E86^>DlFZ{XXBm{eIu?Q;MEP?;bq)H;-icq*+OShHKg{Es_hk zgx}bW)Adp>Zes#ZF^*3-jx`O^7*66G9^(|g;yiW*q!rvl6DtAb)yd(9LJx)-r6dmH z1ny%S-eVeH(0R~ol16X=htS4nEMpnZn(4)j7HI%SgRWVOkPB$xV=(Vt_)EdrV6atU zSnZ&*=^@7Px>|q74)P~D7kfkQ05NnH$s%v+1o>&YL}!89YAzwOse&ec^mA}FucOOp zjPPm46!zh2H6Il>YrnkqfiG-WRwNcPEh`yI*cZMl|IFG}CbyPZPnx?qJ7p{vZ;Y`2 EAF(<{v;=TT0pd2vNHSO0prM$oGUgkBQ?>v2<=b2OE$+&uScX63~ zkE__~73oGBd+`v*@D4}u2V*!;Co+L)T*6D7LGg*iVHVf$7&ZL$NnSd6X|EUQ!Eub@ z22SA#w&DlQ;vcGp(+$+(K8EoYU-27X@S#!U9S@pB265A0wun0U9z*!)&zCRkXfAIM zM~0RoRGXfoYWP|?uWk_ukQ-55tfLxW1=S)&q^H~1OX?Oz28KUO3=GQ{7#KFPFfcd+=?GQ^27Vwt2S{@Q=`}z)3`lPS(i}jV zmyLnJ5lD*xX<;B80i=P5AqPnC0O?{NEf1s{fV3o#Ua;|{2a^g{fPaXtb5UwyNoIbY zu1jJ`s+EF~fuW%;5E&UM7#dm`nN6-?KE>mcn3rCfm=2UqEuQSk61{mXiwP3|k#Qj( delta 194 zcmdnR`;d1+OX_V#28KUO3=GQ{7#MD_Ffcd+>3UWM27VxY7)Wyh>B~Sm3`pMv(i}kA zgpGm05lGttX<;B;52S&JVKR{50n)R8v^^!3v-OEOB6 z^pf*)^^5Y06N@tQi}Z67D--kdGmDE$Q;R1XGD~tBSn3*CDi~T=nOIH^Wj-}ohb4-~ VCowO*G%-EZN+Gp)b1RD}69D@QD_sBp diff --git a/apps/web_theme/locale/pt/LC_MESSAGES/django.mo b/apps/web_theme/locale/pt/LC_MESSAGES/django.mo index e7f0bd59d120b5b4619b975a5c50311925088703..24ff710f54bb16fac7d9360141199e778dc8f3a8 100644 GIT binary patch delta 175 zcmdnbJA-#ZOX^}q28KUO3=FFn7#J3?Ffix>X>V2r1|VXn1QHxTx*kYJ0_hGY{}GUO z2J&A6X)z$J&&I$2L=0{~f(JV!Z delta 193 zcmbQiyPtPLOX?{`28KUO3=FFn7#NPTFfix>=^|DJ1|VWs03^=UwSa9XOxr_Sn2DhXO?7?Cg~;T z=js>b7bg~F<`?PbCRQfq>1P%fm!=j^He{CMHn7w+vQ#j%urjfn9Ll_NvNTH+w@+eT RdTC;Ms+B^)=3*9ACIG2wD3|~M diff --git a/apps/web_theme/locale/ru/LC_MESSAGES/django.mo b/apps/web_theme/locale/ru/LC_MESSAGES/django.mo index bf3748bb25ca5dff4572cae9262fbb365eecef08..53af98872f39e8c61599d3881f69bc0ecb3a9247 100644 GIT binary patch delta 175 zcmdnU-^D+nCG{2~1H&IC28IO;3=G#<7#JdebOkE|0~e6K2&9F8^c^7G2BhBuX%Qe@ z#>T)91*E%yv<8rV2Bi6bG(S57gD8-e1JYVR+5}3+Z+z*&q{0>8AEN7Al$uzQnV+ZY yl30>zrC?-WXs8QBMn(#ThE_&qlWUll@%SX>rI#kA1Eq^fCo8fYZ_;>4oN{389_#LC1x{mkOx($wO~hRl-O29~-;mI{UzRwkB{Lz$ON=3t59@kz`} SFHKBOwNfZ5-JHmx$qWFGu_%E6 From 882e9353117f583fed7764667ac203f396a7e98c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 13:55:46 -0400 Subject: [PATCH 78/92] Add the django_gpg to the list of transifex resources --- .tx/config | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.tx/config b/.tx/config index cbd85fb879..acbc95abba 100644 --- a/.tx/config +++ b/.tx/config @@ -148,3 +148,9 @@ trans.es = apps/web_theme/locale/es/LC_MESSAGES/django.po trans.pt = apps/web_theme/locale/pt/LC_MESSAGES/django.po trans.ru = apps/web_theme/locale/ru/LC_MESSAGES/django.po +[mayan-edms.apps-django_gpg] +source_file = apps/django_gpg/locale/en/LC_MESSAGES/django.po +source_lang = en +trans.es = apps/django_gpg/locale/es/LC_MESSAGES/django.po +trans.pt = apps/django_gpg/locale/pt/LC_MESSAGES/django.po +trans.ru = apps/django_gpg/locale/ru/LC_MESSAGES/django.po From 1a973723f24052a51b68161cda173da08c36f9b1 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 14:21:17 -0400 Subject: [PATCH 79/92] Fix typo --- apps/django_gpg/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/django_gpg/views.py b/apps/django_gpg/views.py index de07ade6fc..31bad2c530 100644 --- a/apps/django_gpg/views.py +++ b/apps/django_gpg/views.py @@ -205,7 +205,7 @@ def document_verify(request, document_pk): _(u'Signature status: %(widget)s %(text)s') % { 'widget': mark_safe(widget), 'text': signature_state['text'] - ), + }, ] if document.signature_state: From a41372f6e7a6f99c64ad26502cbced3fc7407d56 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 22:48:17 -0400 Subject: [PATCH 80/92] Remove CACHE dicctionary from repository, didn't fix problem with compressor + DjangoZoom --- static/CACHE/empty.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 static/CACHE/empty.txt diff --git a/static/CACHE/empty.txt b/static/CACHE/empty.txt deleted file mode 100644 index 6068d6a5eb..0000000000 --- a/static/CACHE/empty.txt +++ /dev/null @@ -1 +0,0 @@ -empty file From 2b6530d9d367b7e1a2bd0760171e83a7d7759d75 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 22:49:24 -0400 Subject: [PATCH 81/92] Use cStringIO whenever possible --- apps/documents/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index d90665d333..1fc86231e7 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -3,9 +3,13 @@ import tempfile import hashlib from ast import literal_eval import base64 -from StringIO import StringIO import datetime import logging + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO from django.db import models from django.utils.translation import ugettext_lazy as _ From 11edaaf4e7ce055dbd7afb9927049d1051617da9 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 01:49:45 -0400 Subject: [PATCH 82/92] Added signature file to the document model and the related method to add, verify and open the signature file --- ...dd_field_documentversion_signature_file.py | 157 ++++++++++++++++++ apps/documents/models.py | 43 ++++- 2 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 apps/documents/migrations/0012_auto__add_field_documentversion_signature_file.py diff --git a/apps/documents/migrations/0012_auto__add_field_documentversion_signature_file.py b/apps/documents/migrations/0012_auto__add_field_documentversion_signature_file.py new file mode 100644 index 0000000000..2938016090 --- /dev/null +++ b/apps/documents/migrations/0012_auto__add_field_documentversion_signature_file.py @@ -0,0 +1,157 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'DocumentVersion.signature_file' + db.add_column('documents_documentversion', 'signature_file', self.gf('django.db.models.fields.files.FileField')(max_length=100, null=True, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'DocumentVersion.signature_file' + db.delete_column('documents_documentversion', 'signature_file') + + + 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'}) + }, + 'comments.comment': { + 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, + 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_pk': ('django.db.models.fields.TextField', [], {}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', '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'}) + }, + 'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '48', 'blank': 'True'}) + }, + 'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentVersion']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'documents.documentversion': { + 'Meta': {'unique_together': "(('document', 'major', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'major': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'signature_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'signature_state': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['documents'] diff --git a/apps/documents/models.py b/apps/documents/models.py index 1fc86231e7..b691cdc36d 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -290,8 +290,20 @@ class Document(models.Model): return version.save() filename = property(_get_filename, _set_filename) - + + def add_detached_signature(self, *args, **kwargs): + return self.latest_version.add_detached_signature(*args, **kwargs) + def has_detached_signature(self): + return self.latest_version.has_detached_signature() + + def detached_signature(self): + return self.latest_version.detached_signature() + + def verify_signature(self): + return self.latest_version.verify_signature() + + class DocumentVersion(models.Model): ''' Model that describes a document version and its properties @@ -320,6 +332,7 @@ class DocumentVersion(models.Model): filename = models.CharField(max_length=255, default=u'', editable=False, db_index=True) checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False) signature_state = models.CharField(blank=True, null=True, max_length=16, verbose_name=_(u'signature state'), editable=False) + signature_file = models.FileField(blank=True, null=True, upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'signature file'), editable=False) class Meta: unique_together = ('document', 'major', 'minor', 'micro', 'release_level', 'serial') @@ -534,7 +547,35 @@ class DocumentVersion(models.Model): return self.file.storage.size(self.file.path) else: return None + + def add_detached_signature(self, detached_signature): + if not self.signature_state: + self.signature_file = detached_signature + self.save() + else: + raise Exception('document already has an embedded signature') + + def has_detached_signature(self): + if self.signature_file: + return self.signature_file.storage.exists(self.signature_file.path) + else: + return False + + def detached_signature(self): + return self.signature_file.storage.open(self.signature_file.path) + + def verify_signature(self): + try: + if self.has_detached_signature(): + logger.debug('has detached signature') + signature = gpg.verify_w_retry(self.open(), self.detached_signature()) + else: + signature = gpg.verify_w_retry(self.open(raw=True)) + except GPGVerificationError: + signature = None + return signature + class DocumentTypeFilename(models.Model): ''' From 755d140132500e272639abc88782d49da78a71dc Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 01:50:38 -0400 Subject: [PATCH 83/92] Add detached signature support to the django_gpg API --- apps/django_gpg/api.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/django_gpg/api.py b/apps/django_gpg/api.py index 50ca3d564c..8d9f2e2b4b 100644 --- a/apps/django_gpg/api.py +++ b/apps/django_gpg/api.py @@ -2,6 +2,8 @@ import types from StringIO import StringIO from pickle import dumps import logging +import tempfile +import os from django.core.files.base import File from django.utils.translation import ugettext_lazy as _ @@ -164,7 +166,7 @@ class GPG(object): self.gpg = gnupg.GPG(**kwargs) - def verify_w_retry(self, file_input): + def verify_w_retry(self, file_input, detached_signature=None): if isinstance(file_input, types.StringTypes): input_descriptor = open(file_input, 'rb') elif isinstance(file_input, types.FileType) or isinstance(file_input, File): @@ -175,12 +177,12 @@ class GPG(object): raise ValueError('Invalid file_input argument type') try: - verify = self.verify_file(input_descriptor) + verify = self.verify_file(input_descriptor, detached_signature) if verify.status == 'no public key': # Try to fetch the public key from the keyservers try: self.receive_key(verify.key_id) - return self.verify_w_retry(file_input) + return self.verify_w_retry(file_input, detached_signature) except KeyFetchingError: return verify else: @@ -188,7 +190,7 @@ class GPG(object): except IOError: return False - def verify_file(self, file_input): + def verify_file(self, file_input, detached_signature=None): """ Verify the signature of a file. """ @@ -199,7 +201,17 @@ class GPG(object): else: raise ValueError('Invalid file_input argument type') - verify = self.gpg.verify_file(descriptor) + if detached_signature: + # Save the original data and invert the argument order + # Signature first, file second + file_descriptor, filename = tempfile.mkstemp(prefix='django_gpg') + file_data = file_input.read() + file_input.close() + os.write(file_descriptor, file_data) + os.close(file_descriptor) + verify = self.gpg.verify_file(detached_signature, data_filename=filename) + else: + verify = self.gpg.verify_file(descriptor) descriptor.close() if verify: From 2e829b3be41f0b069f040c017c0afaa0a04d259e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 01:51:00 -0400 Subject: [PATCH 84/92] Add views to upload and download detached signatures --- apps/django_gpg/__init__.py | 14 ++++++++ apps/django_gpg/forms.py | 6 ++++ apps/django_gpg/urls.py | 3 +- apps/django_gpg/views.py | 67 +++++++++++++++++++++++++++++++++---- 4 files changed, 82 insertions(+), 8 deletions(-) diff --git a/apps/django_gpg/__init__.py b/apps/django_gpg/__init__.py index 7fe12a221c..b4d2c83950 100644 --- a/apps/django_gpg/__init__.py +++ b/apps/django_gpg/__init__.py @@ -16,6 +16,8 @@ PERMISSION_KEY_VIEW = {'namespace': 'django_gpg', 'name': 'key_view', 'label': _ PERMISSION_KEY_DELETE = {'namespace': 'django_gpg', 'name': 'key_delete', 'label': _(u'Delete keys')} PERMISSION_KEYSERVER_QUERY = {'namespace': 'django_gpg', 'name': 'keyserver_query', 'label': _(u'Query keyservers')} PERMISSION_KEY_RECEIVE = {'namespace': 'django_gpg', 'name': 'key_receive', 'label': _(u'Import key from keyservers')} +PERMISSION_SIGNATURE_UPLOAD = {'namespace': 'django_gpg', 'name': 'signature_upload', 'label': _(u'Upload detached signatures')} +PERMISSION_SIGNATURE_DOWNLOAD = {'namespace': 'django_gpg', 'name': 'key_receive', 'label': _(u'Download detached signatures')} # Permission setup set_namespace_title('django_gpg', _(u'Signatures')) @@ -24,6 +26,11 @@ register_permission(PERMISSION_KEY_VIEW) register_permission(PERMISSION_KEY_DELETE) register_permission(PERMISSION_KEYSERVER_QUERY) register_permission(PERMISSION_KEY_RECEIVE) +register_permission(PERMISSION_SIGNATURE_UPLOAD) +register_permission(PERMISSION_SIGNATURE_DOWNLOAD) + +def has_embedded_signature(context): + return context['object'].signature_state # Setup views private_keys = {'text': _(u'private keys'), 'view': 'key_private_list', 'args': 'object.pk', 'famfam': 'key', 'icon': 'key.png', 'permissions': [PERMISSION_KEY_VIEW]} @@ -31,12 +38,16 @@ public_keys = {'text': _(u'public keys'), 'view': 'key_public_list', 'args': 'ob key_delete = {'text': _(u'delete'), 'view': 'key_delete', 'args': ['object.fingerprint', 'object.type'], 'famfam': 'key_delete', 'permissions': [PERMISSION_KEY_DELETE]} key_query = {'text': _(u'Query keyservers'), 'view': 'key_query', 'famfam': 'zoom', 'permissions': [PERMISSION_KEYSERVER_QUERY]} key_receive = {'text': _(u'Import'), 'view': 'key_receive', 'args': 'object.keyid', 'famfam': 'key_add', 'keep_query': True, 'permissions': [PERMISSION_KEY_RECEIVE]} +document_signature_upload = {'text': _(u'Upload signature'), 'view': 'document_signature_upload', 'args': 'object.pk', 'famfam': 'pencil_add', 'permissions': [PERMISSION_SIGNATURE_UPLOAD], 'conditional_disable': has_embedded_signature} +document_signature_download = {'text': _(u'Download signature'), 'view': 'document_signature_download', 'args': 'object.pk', 'famfam': 'disk', 'permissions': [PERMISSION_SIGNATURE_DOWNLOAD], 'conditional_disable': has_embedded_signature} # Document views document_verify = {'text': _(u'signatures'), 'view': 'document_verify', 'args': 'object.pk', 'famfam': 'text_signature', 'permissions': [PERMISSION_DOCUMENT_VERIFY]} register_links(Document, [document_verify], menu_name='form_header') +register_links(['document_verify', 'document_signature_upload', 'document_signature_download'], [document_signature_upload, document_signature_download], menu_name='sidebar') + register_links(['key_delete', 'key_private_list', 'key_public_list', 'key_query'], [private_keys, public_keys, key_query], menu_name='sidebar') register_links(Key, [key_delete]) @@ -44,3 +55,6 @@ register_links(KeyServerKey, [key_receive]) register_setup(private_keys) register_setup(public_keys) + + + diff --git a/apps/django_gpg/forms.py b/apps/django_gpg/forms.py index 619035fd5d..b961daf037 100644 --- a/apps/django_gpg/forms.py +++ b/apps/django_gpg/forms.py @@ -11,3 +11,9 @@ class KeySearchForm(forms.Form): label=_(u'Term'), help_text=_(u'Name, e-mail, key ID or key fingerprint to look for.') ) + + +class DetachedSignatureForm(forms.Form): + file = forms.FileField( + label=_(u'Signature file'), + ) diff --git a/apps/django_gpg/urls.py b/apps/django_gpg/urls.py index 20c4c69360..4a22882d06 100644 --- a/apps/django_gpg/urls.py +++ b/apps/django_gpg/urls.py @@ -5,7 +5,8 @@ urlpatterns = patterns('django_gpg.views', url(r'^list/private/$', 'key_list', {'secret': True}, 'key_private_list'), url(r'^list/public/$', 'key_list', {'secret': False}, 'key_public_list'), url(r'^verify/(?P\d+)/$', 'document_verify', (), 'document_verify'), + url(r'^upload/signature/(?P\d+)/$', 'document_signature_upload', (), 'document_signature_upload'), + url(r'^download/signature/(?P\d+)/$', 'document_signature_download', (), 'document_signature_download'), url(r'^query/$', 'key_query', (), 'key_query'), url(r'^receive/(?P.+)/$', 'key_receive', (), 'key_receive'), - ) diff --git a/apps/django_gpg/views.py b/apps/django_gpg/views.py index 31bad2c530..d908d011e0 100644 --- a/apps/django_gpg/views.py +++ b/apps/django_gpg/views.py @@ -15,14 +15,16 @@ from documents.models import Document, RecentDocument from permissions.api import check_permissions from common.utils import pretty_size, parse_range, urlquote, \ return_diff, encapsulate - +from filetransfers.api import serve_file + from django_gpg.api import Key, SIGNATURE_STATES from django_gpg.runtime import gpg from django_gpg.exceptions import GPGVerificationError, KeyFetchingError from django_gpg import (PERMISSION_DOCUMENT_VERIFY, PERMISSION_KEY_VIEW, PERMISSION_KEY_DELETE, PERMISSION_KEYSERVER_QUERY, - PERMISSION_KEY_RECEIVE) -from django_gpg.forms import KeySearchForm + PERMISSION_KEY_RECEIVE, PERMISSION_SIGNATURE_UPLOAD, + PERMISSION_SIGNATURE_DOWNLOAD) +from django_gpg.forms import KeySearchForm, DetachedSignatureForm logger = logging.getLogger(__name__) @@ -193,10 +195,8 @@ def document_verify(request, document_pk): document = get_object_or_404(Document, pk=document_pk) RecentDocument.objects.add_document_for_user(request.user, document) - try: - signature = gpg.verify_w_retry(document.open(raw=True)) - except GPGVerificationError: - signature = None + + signature = document.verify_signature() signature_state = SIGNATURE_STATES.get(getattr(signature, 'status', None)) @@ -230,3 +230,56 @@ def document_verify(request, document_pk): 'document': document, 'paragraphs': paragraphs, }, context_instance=RequestContext(request)) + + +def document_signature_upload(request, document_pk): + check_permissions(request.user, [PERMISSION_SIGNATURE_UPLOAD]) + document = get_object_or_404(Document, pk=document_pk) + + RecentDocument.objects.add_document_for_user(request.user, document) + + post_action_redirect = None + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', '/'))) + + if request.method == 'POST': + form = DetachedSignatureForm(request.POST, request.FILES) + if form.is_valid(): + try: + document.add_detached_signature(request.FILES['file']) + messages.success(request, _(u'Detached signature uploaded successfully.')) + return HttpResponseRedirect(next) + except Exception, msg: + messages.error(request, msg) + return HttpResponseRedirect(previous) + else: + form = DetachedSignatureForm() + + return render_to_response('generic_form.html', { + 'title': _(u'Upload detached signature for: %s') % document, + 'form_icon': 'key_delete.png', + 'next': next, + 'form': form, + 'previous': previous, + 'object': document, + }, context_instance=RequestContext(request)) + + +def document_signature_download(request, document_pk): + check_permissions(request.user, [PERMISSION_SIGNATURE_DOWNLOAD]) + document = get_object_or_404(Document, pk=document_pk) + + try: + if document.has_detached_signature(): + signature = document.detached_signature() + return serve_file( + request, + signature, + save_as=u'"%s.sig"' % document.filename, + content_type=u'application/octet-stream' + ) + except Exception, e: + messages.error(request, e) + return HttpResponseRedirect(request.META['HTTP_REFERER']) + + return HttpResponseRedirect(request.META['HTTP_REFERER']) From 2d015d768e1bd163e71fe7ef8ce374361be9d442 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 01:56:39 -0400 Subject: [PATCH 85/92] Improve the logic of the signature download conditional disabling --- apps/django_gpg/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/django_gpg/__init__.py b/apps/django_gpg/__init__.py index b4d2c83950..e9e18416a8 100644 --- a/apps/django_gpg/__init__.py +++ b/apps/django_gpg/__init__.py @@ -31,6 +31,9 @@ register_permission(PERMISSION_SIGNATURE_DOWNLOAD) def has_embedded_signature(context): return context['object'].signature_state + +def doesnt_have_detached_signature(context): + return context['object'].has_detached_signature() == False # Setup views private_keys = {'text': _(u'private keys'), 'view': 'key_private_list', 'args': 'object.pk', 'famfam': 'key', 'icon': 'key.png', 'permissions': [PERMISSION_KEY_VIEW]} @@ -39,7 +42,7 @@ key_delete = {'text': _(u'delete'), 'view': 'key_delete', 'args': ['object.finge key_query = {'text': _(u'Query keyservers'), 'view': 'key_query', 'famfam': 'zoom', 'permissions': [PERMISSION_KEYSERVER_QUERY]} key_receive = {'text': _(u'Import'), 'view': 'key_receive', 'args': 'object.keyid', 'famfam': 'key_add', 'keep_query': True, 'permissions': [PERMISSION_KEY_RECEIVE]} document_signature_upload = {'text': _(u'Upload signature'), 'view': 'document_signature_upload', 'args': 'object.pk', 'famfam': 'pencil_add', 'permissions': [PERMISSION_SIGNATURE_UPLOAD], 'conditional_disable': has_embedded_signature} -document_signature_download = {'text': _(u'Download signature'), 'view': 'document_signature_download', 'args': 'object.pk', 'famfam': 'disk', 'permissions': [PERMISSION_SIGNATURE_DOWNLOAD], 'conditional_disable': has_embedded_signature} +document_signature_download = {'text': _(u'Download signature'), 'view': 'document_signature_download', 'args': 'object.pk', 'famfam': 'disk', 'permissions': [PERMISSION_SIGNATURE_DOWNLOAD], 'conditional_disable': doesnt_have_detached_signature} # Document views document_verify = {'text': _(u'signatures'), 'view': 'document_verify', 'args': 'object.pk', 'famfam': 'text_signature', 'permissions': [PERMISSION_DOCUMENT_VERIFY]} From 197d3b4642b532424b852aa756179125a1387591 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 02:03:33 -0400 Subject: [PATCH 86/92] Disable private key views until document signing is implemented --- apps/django_gpg/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/django_gpg/__init__.py b/apps/django_gpg/__init__.py index e9e18416a8..e45ae78165 100644 --- a/apps/django_gpg/__init__.py +++ b/apps/django_gpg/__init__.py @@ -39,10 +39,11 @@ def doesnt_have_detached_signature(context): private_keys = {'text': _(u'private keys'), 'view': 'key_private_list', 'args': 'object.pk', 'famfam': 'key', 'icon': 'key.png', 'permissions': [PERMISSION_KEY_VIEW]} public_keys = {'text': _(u'public keys'), 'view': 'key_public_list', 'args': 'object.pk', 'famfam': 'key', 'icon': 'key.png', 'permissions': [PERMISSION_KEY_VIEW]} key_delete = {'text': _(u'delete'), 'view': 'key_delete', 'args': ['object.fingerprint', 'object.type'], 'famfam': 'key_delete', 'permissions': [PERMISSION_KEY_DELETE]} -key_query = {'text': _(u'Query keyservers'), 'view': 'key_query', 'famfam': 'zoom', 'permissions': [PERMISSION_KEYSERVER_QUERY]} -key_receive = {'text': _(u'Import'), 'view': 'key_receive', 'args': 'object.keyid', 'famfam': 'key_add', 'keep_query': True, 'permissions': [PERMISSION_KEY_RECEIVE]} -document_signature_upload = {'text': _(u'Upload signature'), 'view': 'document_signature_upload', 'args': 'object.pk', 'famfam': 'pencil_add', 'permissions': [PERMISSION_SIGNATURE_UPLOAD], 'conditional_disable': has_embedded_signature} -document_signature_download = {'text': _(u'Download signature'), 'view': 'document_signature_download', 'args': 'object.pk', 'famfam': 'disk', 'permissions': [PERMISSION_SIGNATURE_DOWNLOAD], 'conditional_disable': doesnt_have_detached_signature} +key_query = {'text': _(u'query keyservers'), 'view': 'key_query', 'famfam': 'zoom', 'permissions': [PERMISSION_KEYSERVER_QUERY]} +key_receive = {'text': _(u'import'), 'view': 'key_receive', 'args': 'object.keyid', 'famfam': 'key_add', 'keep_query': True, 'permissions': [PERMISSION_KEY_RECEIVE]} +document_signature_upload = {'text': _(u'upload signature'), 'view': 'document_signature_upload', 'args': 'object.pk', 'famfam': 'pencil_add', 'permissions': [PERMISSION_SIGNATURE_UPLOAD], 'conditional_disable': has_embedded_signature} +document_signature_download = {'text': _(u'download signature'), 'view': 'document_signature_download', 'args': 'object.pk', 'famfam': 'disk', 'permissions': [PERMISSION_SIGNATURE_DOWNLOAD], 'conditional_disable': doesnt_have_detached_signature} +key_setup = {'text': _(u'key management'), 'view': 'key_public_list', 'args': 'object.pk', 'famfam': 'key', 'icon': 'key.png', 'permissions': [PERMISSION_KEY_VIEW]} # Document views document_verify = {'text': _(u'signatures'), 'view': 'document_verify', 'args': 'object.pk', 'famfam': 'text_signature', 'permissions': [PERMISSION_DOCUMENT_VERIFY]} @@ -51,13 +52,13 @@ register_links(Document, [document_verify], menu_name='form_header') register_links(['document_verify', 'document_signature_upload', 'document_signature_download'], [document_signature_upload, document_signature_download], menu_name='sidebar') -register_links(['key_delete', 'key_private_list', 'key_public_list', 'key_query'], [private_keys, public_keys, key_query], menu_name='sidebar') +#register_links(['key_delete', 'key_private_list', 'key_public_list', 'key_query'], [private_keys, public_keys, key_query], menu_name='sidebar') +register_links(['key_delete', 'key_public_list', 'key_query'], [public_keys, key_query], menu_name='sidebar') register_links(Key, [key_delete]) register_links(KeyServerKey, [key_receive]) -register_setup(private_keys) -register_setup(public_keys) +register_setup(key_setup) From 6b87c8fab13b35f1037ee2df5c4b30e7865c1070 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 02:05:05 -0400 Subject: [PATCH 87/92] Remove the filename re use checkbox from the document edit form --- apps/documents/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/documents/forms.py b/apps/documents/forms.py index 991b6488e8..63e4470a55 100644 --- a/apps/documents/forms.py +++ b/apps/documents/forms.py @@ -235,6 +235,7 @@ class DocumentForm_edit(DocumentForm): self.fields.pop('release_level') self.fields.pop('version_update') self.fields.pop('comment') + self.fields.pop('use_file_name') class DocumentPropertiesForm(DetailForm): From 265a4c6b1d931f52700d41ed2259c2c22c18650e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 02:26:59 -0400 Subject: [PATCH 88/92] Translation updates --- .../converter/locale/es/LC_MESSAGES/django.mo | Bin 5608 -> 5886 bytes .../converter/locale/es/LC_MESSAGES/django.po | 10 +- .../locale/en/LC_MESSAGES/django.po | 119 +++++++---- .../locale/es/LC_MESSAGES/django.mo | Bin 1619 -> 4842 bytes .../locale/es/LC_MESSAGES/django.po | 192 +++++++++++------- .../locale/pt/LC_MESSAGES/django.mo | Bin 524 -> 485 bytes .../locale/pt/LC_MESSAGES/django.po | 130 +++++++----- .../locale/ru/LC_MESSAGES/django.mo | Bin 595 -> 556 bytes .../locale/ru/LC_MESSAGES/django.po | 133 +++++++----- 9 files changed, 370 insertions(+), 214 deletions(-) diff --git a/apps/converter/locale/es/LC_MESSAGES/django.mo b/apps/converter/locale/es/LC_MESSAGES/django.mo index 30ce640c0dfda51f7987439c045f019f464f7752..e0c200c67c9f68f7d13db4d933c27e8886e2c3eb 100644 GIT binary patch delta 1662 zcmZY9OGs2v9LMp0jbmz>X^mNG-LleB(=i`q4-+j23?vAOKp1r}LFdLaO0;Nt&|?)- zVN8pxAcBfkBQ2r_)h2>!QBV{G5k%3VMHH=~?{DTBM29)^IrnuQ|MS21bItF%`1|6l zaierl%c)DJo9)8dEdD47zu8PYjU{*i12~DL_ymLa5sNU8ZB~X=*oK`r0}py0$6St2 z;c~OMC27=f;y%{mSIomwo;rh-n1i=)G2Tbb?E@C#CoI69xD>Ovc?qsSO|%;qVjouH zVbAMW#`yMvhHm_Zih!T#(X=HvA8Sw@Y{p6)KnG8vB5)1o;zLvfUZEoJ1!tq5)i_v! ztk_z;<6fN2_!g(3l_#(n?_x22M}lG5#Gl1k0qQt_8n7H43}OVg<343pF4O=~Y{Uev$497{YgUY9<;b&Ek6L*r zYUNu|5!{8Ncm$8(FI3165*9sw9JS@=QIU;bqM;6NdKVseK1TA-o}(uA1~uR>Bo)$8 z)4&>3d#tjCs!mmO?>egbR#`?>L{-*i&_d(3(K}E$R(ov+*^zWoDq~fYV6{~3pNigb zC7D*GqK}OxtZ1s}o3MyFkE+*ps;r`+h-lk_djGXw3bjf))Tr-3164WEK~=~$QAzA{ zg4UxFi~LKMR?jJvGtE;6o<3Akswg+~MpS48dIQ*|^gB&-jW4iSs@Xr_n==@8I^9St z>JB^Ip~&E9XfS;CTu!CW>Fhe2%IR(Daig(N-*DLJa{EU|!jYKM5$)eIG!}NYxY3bN z%>KV&OKN8BAzxl&T|h;a_P8-u13oRU04BSBtG=_Tq8S-bQI8@_X)OgdV`)5)2 z&7m?gk9o>tmN@xV#Y#~R&XG^m`YYU;#;*Swa?V zRF;GL)CA9=YMnz(Y!F*<6jk#gsw7`frTdK? z=%-%tI7V>VYYY;|%JHS+%bdp~~+$Ow&J% zhp>p6*iYnE_$ELZq(Lz`Uhw#iYnerJ4g$b9=iG+ r1&=peNP2rqGu~l`FBMOx!|7C4GF3X^54s9N!REqb@Mftm^wRMkHc@o2 diff --git a/apps/converter/locale/es/LC_MESSAGES/django.po b/apps/converter/locale/es/LC_MESSAGES/django.po index 68d184d9c0..710d29bd7a 100644 --- a/apps/converter/locale/es/LC_MESSAGES/django.po +++ b/apps/converter/locale/es/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgstr "" "Project-Id-Version: Mayan EDMS\n" "Report-Msgid-Bugs-To: http://github.com/rosarior/mayan/issues\n" "POT-Creation-Date: 2011-11-22 11:26-0400\n" -"PO-Revision-Date: 2011-11-22 21:01+0000\n" +"PO-Revision-Date: 2011-12-06 06:17+0000\n" "Last-Translator: rosarior \n" "Language-Team: Spanish (Castilian) (http://www.transifex.net/projects/p/mayan-edms/team/es/)\n" "MIME-Version: 1.0\n" @@ -557,11 +557,11 @@ msgstr "" #: literals.py:205 literals.py:206 msgid "Photo CD" -msgstr "" +msgstr "Photo CD" #: literals.py:207 msgid "Page Control Language" -msgstr "" +msgstr "Page Control Language" #: literals.py:208 literals.py:221 msgid "Apple Macintosh QuickDraw/PICT" @@ -577,11 +577,11 @@ msgstr "" #: literals.py:211 msgid "Portable Document Format" -msgstr "" +msgstr "Formato portatil de documento" #: literals.py:212 msgid "Portable Document Archive Format" -msgstr "" +msgstr "Format portatil de archivo de documento" #: literals.py:213 msgid "Pentax Electronic File" diff --git a/apps/django_gpg/locale/en/LC_MESSAGES/django.po b/apps/django_gpg/locale/en/LC_MESSAGES/django.po index 22a9e34631..bb2d01367b 100644 --- a/apps/django_gpg/locale/en/LC_MESSAGES/django.po +++ b/apps/django_gpg/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-12-05 13:40-0400\n" +"POT-Creation-Date: 2011-12-06 02:06-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -29,7 +29,7 @@ msgstr "" msgid "Delete keys" msgstr "" -#: __init__.py:17 __init__.py:32 +#: __init__.py:17 msgid "Query keyservers" msgstr "" @@ -37,75 +37,99 @@ msgstr "" msgid "Import key from keyservers" msgstr "" -#: __init__.py:21 +#: __init__.py:19 +msgid "Upload detached signatures" +msgstr "" + +#: __init__.py:20 +msgid "Download detached signatures" +msgstr "" + +#: __init__.py:23 msgid "Signatures" msgstr "" -#: __init__.py:29 views.py:65 +#: __init__.py:39 views.py:67 msgid "private keys" msgstr "" -#: __init__.py:30 views.py:68 +#: __init__.py:40 views.py:70 msgid "public keys" msgstr "" -#: __init__.py:31 +#: __init__.py:41 msgid "delete" msgstr "" -#: __init__.py:33 -msgid "Import" +#: __init__.py:42 +msgid "query keyservers" msgstr "" -#: __init__.py:36 +#: __init__.py:43 +msgid "import" +msgstr "" + +#: __init__.py:44 +msgid "upload signature" +msgstr "" + +#: __init__.py:45 +msgid "download signature" +msgstr "" + +#: __init__.py:46 +msgid "key management" +msgstr "" + +#: __init__.py:49 msgid "signatures" msgstr "" -#: api.py:20 +#: api.py:22 msgid "Public" msgstr "" -#: api.py:21 +#: api.py:23 msgid "Secret" msgstr "" -#: api.py:29 api.py:34 +#: api.py:31 api.py:36 msgid "RSA" msgstr "" -#: api.py:30 +#: api.py:32 msgid "DSA" msgstr "" -#: api.py:35 +#: api.py:37 msgid "Elgamal" msgstr "" -#: api.py:49 +#: api.py:51 msgid "Bad signature." msgstr "" -#: api.py:53 +#: api.py:55 msgid "Document not signed or invalid signature." msgstr "" -#: api.py:57 +#: api.py:59 msgid "Signature error." msgstr "" -#: api.py:61 +#: api.py:63 msgid "Document is signed but no public key is available for verification." msgstr "" -#: api.py:65 +#: api.py:67 msgid "Document is signed, and signature is good." msgstr "" -#: api.py:69 +#: api.py:71 msgid "Document is signed with a valid signature." msgstr "" -#: api.py:142 +#: api.py:144 msgid "unknown" msgstr "" @@ -117,43 +141,47 @@ msgstr "" msgid "Name, e-mail, key ID or key fingerprint to look for." msgstr "" -#: views.py:43 +#: forms.py:18 +msgid "Signature file" +msgstr "" + +#: views.py:45 #, python-format msgid "Key: %s, imported successfully." msgstr "" -#: views.py:46 +#: views.py:48 #, python-format msgid "Unable to import key id: %s" msgstr "" -#: views.py:50 +#: views.py:52 msgid "Import key" msgstr "" -#: views.py:51 +#: views.py:53 #, python-format msgid "Are you sure you wish to import key id: %s?" msgstr "" -#: views.py:76 +#: views.py:78 msgid "Key ID" msgstr "" -#: views.py:80 +#: views.py:82 msgid "Owner" msgstr "" -#: views.py:100 +#: views.py:102 #, python-format msgid "Key: %s, deleted successfully." msgstr "" -#: views.py:107 +#: views.py:109 msgid "Delete key" msgstr "" -#: views.py:109 +#: views.py:111 #, python-format msgid "" "Are you sure you wish to delete key: %s? If you try to delete a public key " @@ -161,47 +189,47 @@ msgid "" "well." msgstr "" -#: views.py:127 +#: views.py:129 msgid "Query key server" msgstr "" -#: views.py:140 +#: views.py:142 msgid "results" msgstr "" -#: views.py:145 +#: views.py:147 msgid "ID" msgstr "" -#: views.py:149 +#: views.py:151 msgid "type" msgstr "" -#: views.py:153 +#: views.py:155 msgid "creation date" msgstr "" -#: views.py:157 +#: views.py:159 msgid "disabled" msgstr "" -#: views.py:161 +#: views.py:163 msgid "expiration date" msgstr "" -#: views.py:165 +#: views.py:167 msgid "expired" msgstr "" -#: views.py:169 +#: views.py:171 msgid "length" msgstr "" -#: views.py:173 +#: views.py:175 msgid "revoked" msgstr "" -#: views.py:178 +#: views.py:180 msgid "Identifies" msgstr "" @@ -248,6 +276,15 @@ msgstr "" msgid "signature properties for: %s" msgstr "" +#: views.py:250 +msgid "Detached signature uploaded successfully." +msgstr "" + +#: views.py:259 +#, python-format +msgid "Upload detached signature for: %s" +msgstr "" + #: conf/settings.py:13 msgid "List of keyservers to be queried for unknown keys." msgstr "" diff --git a/apps/django_gpg/locale/es/LC_MESSAGES/django.mo b/apps/django_gpg/locale/es/LC_MESSAGES/django.mo index 578eb2056ebd7f32b40858bf2e750d814c96c4ac..e37a8ed1ed4f0dded084e479b950eef99e3cd029 100644 GIT binary patch literal 4842 zcma);Ux;K!6~;@AM#rcz(Wpt(T9fR~WT(4l#&unLcQ%>bnZY4DyPKWapde!Pz17{7 zxqYjxx_5eeUxa`VAA$<5hy+YDXaXS+)R!QLHVQth7{RB-MFWDMLLPilAN;<$_jdQ} z&We|={@p)Sr_MQb&Z)Y7*KJn>t`)TV(H?xO5T6I@L}-HlK0(Sd|wASUmwIzJcB<>Ev|r+_nneI0Pn@~ zD>K(5CHkam9&IQS*-Aowi!esBbG-amkhgFk}Q|4$%(;_vul{QVoe2Rwwy>7SFJ1Rn=K z0Xpy;cm-^Oe*x*IGcbn!-2!R%7rco(;)SH4ung^2;_Xf1u5^(AochwNc-OesmFgn zgo3yOoj(BH19HB@C69odcNygPvmnPkQNC}3)MEg0-fw{%|2^=d;EN#V|24=sxC(Oq z-}A=uFEUJ#5N{{3B9sv_r?t4W~XU+0R>viP%qOU6o1WWnZ~W z+P17dHu}!&DhTT<f#pN&VKUnO@&w1`alH&`z9EvGk^s zstk72#p>3ISRETMb?b4Tsi>=`y34$uSQYbC9!1*wcAg|d2(eM#(`hh?veBWHXEL?Z zHp5)Ct4yL=iI#2ai=&ilj7<+f>R<@n((c@k_Sm?!La6ZNr z;2Q9joF6abUT=;+7pr%Zx6c?Kf@Wtt2g3|;@#P#IMRz(kPj^x~NP{AEaaQ&8ywtTG z{68PUVPlnkfC$+(X-7N68P*SS5^HxTp)Sr1Qtia~5VhiQ$R39Ngum7Nz5Nn1K{ zIulz}e8KQw@#KYejBSe&~2H4RHqpZ^{XAF+}w~3r{~3qi@y2izgVxLm5{o!1xcjg-=Eg!U70* zC4{+*k=&vv7(fwdA~v4N#UjRvg%}(Q5xUpXF)~i*OMT;}goJ0jFU%2K$(~A8M>7#c zqSH>+6;sv2i%I?Qf}s+Yvsyi;eV$}qIK69kAa<+)Dv<4Kmm%QC?ihdcf-51)U=u}o zn3NZB-q|O0l+`xk8vNvu=uNq)hANe7tD9Rh7c|RLZPRy5TsxI_d~Mq{WjD+E%|@ex z?9W^E$o3k}dgY9DjULA~jQ6?rGw083*G`v?t*yebO}VtNxL8|Usx2Iq3ro#~qqT)2 z3k&F2yP$Usr{AaF(dOcj!wa}(&M2SNww+3SqB5?0HLpD3YzscH_3}-x{CtuNJ6ouTjtPx(f_ zFtVoOo^NE3)X=^$H?z61xi<0PVtrxev`sTCb!|K30#l^XPn1cI%c!fI*V*#L?e*HR zNk7il)~>deMm9#$H|4RG$;N6oC4HLYP9?Q<>v~ASv>zV*^3rix+%C_i^1!lOoSPB9 z{P`NvOM2bOon`Go0(q@u%Q~l=Oq5I#wHp!=d*YUnFc|$~UG>P*vWx7GUgOrNf)vCV zw?&FW72|#N-4@$pj3PB2$?Y+w7H;%>AsA1{`j#2Jgk9xUN)}mjqncvfxSpzZ2Zike zHSJVs4~^OLz->V)7j?}p4*@8o9NdN@eBLt9QeQF|B9{q?ofe(PMWPFh*&_@AkzkhF zWmgRguT>_)U&i-sYPe~_71#~ST{AMHAH%@^B^9!uPtdBGFl>8p*#A9{B@}^y{amMD zreZtURfEoz4W%{H-qSu(u2XoqZF=hFeKf{tqS{E=FF1K4?5owIr*9MMxyHM28Y)$I z!>$-wZpRT6bL02esKfnK42m;VG|rFXWbXn^btuCFp>#tRY`r0b_tOPO6}*-?ll?N( z32}^k$mxZ`G-~c6o7%|4%41))y_Uny92=*ju%c8uLd0o=PlD3vu*xQ~^Ekg1=Z?LG zaue_7I0Q0KP%#yP4Bn!tP^Xw0>y_)#t5%#Z(@daru-Zeb%8v0UFFvaxSkW52=8@CT zQexq&ZhKrm+3?lG-ys^pp1H=Io^r!09#Z#aieULb>{H(|OfhbsZGYSJC;1gbuQR57 zk*&xr9v8gW9=+sxCbeQ+qX5dG>{_S7m$McI;3$g8%7q;lknnvA>FUZv!AU$6ALiQB zN7kxf2aoFPAi}4UyJ!cU@R=upu+XBYlcJJ`z)(>Zs+nMr z6p0h$3LMRJ$02Pea*E?XA*r~>*Q%-soCNffg=>p6K}A#yfe;x8%%W`i@pBm&ULZJc u9U8?UfuSAF2?#}nBZlqc(8)X=FLtO@!Fz>YM1~~{R+XVISRqT7i~j;yY#v$w delta 754 zcmYk(O=}ZT6vpuz+a_t!ByCzP*1lu}L5SAZqE*-?$qe(N28FI% zD_nJ@bR&oh7vcx#A}hsR7cPW+0Nv91yBJZbo5MQy1->?s-wi%N{2lKcO(|8;w@H9^2WgNgZ4&xfC{t>GFG4``;)_GCk z3mn7^EZ{p-YM!%3oCP(*A9I}M7+Q*^>PH+mgH9{zEjA@oa3PbU%(Xe zw{F8Fwn7^fW8^5QfjWc+=oV_Yoz&l@UrSLWHBikJYR;L=h`tcIwSKw3u@JhIwx3j@ zB(%>4igqzQ>eMbRn3~&gW4Ch4ZU6TgnWbjnhp~N~nX%o>WZ&|gmVKVxS@dqU>Twdf zky&xWrhPeh@<0%Jx9hzQkqMHk4ey$|MTT8TBaTe;uPAN@wv)S;nX4Q+GJo`-UCcXr zKQ=#i_-!+PK4q^I#_fZ`Otv?^9@?M9{q|zXN!Jp$^TxM#hi6M?y|7v50G(H1(`)&5 YePqe5mYwn5y!v%NGBPx# diff --git a/apps/django_gpg/locale/es/LC_MESSAGES/django.po b/apps/django_gpg/locale/es/LC_MESSAGES/django.po index 9d384e4241..7517475856 100644 --- a/apps/django_gpg/locale/es/LC_MESSAGES/django.po +++ b/apps/django_gpg/locale/es/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Mayan EDMS\n" "Report-Msgid-Bugs-To: http://github.com/rosarior/mayan/issues\n" -"POT-Creation-Date: 2011-12-05 13:40-0400\n" -"PO-Revision-Date: 2011-12-05 17:52+0000\n" +"POT-Creation-Date: 2011-12-06 02:06-0400\n" +"PO-Revision-Date: 2011-12-06 06:14+0000\n" "Last-Translator: rosarior \n" "Language-Team: Spanish (Castilian) (http://www.transifex.net/projects/p/mayan-edms/team/es/)\n" "MIME-Version: 1.0\n" @@ -30,7 +30,7 @@ msgstr "Ver llaves" msgid "Delete keys" msgstr "Borrar llaves" -#: __init__.py:17 __init__.py:32 +#: __init__.py:17 msgid "Query keyservers" msgstr "Hacer búsquedas en servidores de llaves" @@ -38,77 +38,103 @@ msgstr "Hacer búsquedas en servidores de llaves" msgid "Import key from keyservers" msgstr "Importar llaves de los servidores de llaves" -#: __init__.py:21 +#: __init__.py:19 +msgid "Upload detached signatures" +msgstr "Subir una firma a parte" + +#: __init__.py:20 +msgid "Download detached signatures" +msgstr "Descargar la fima" + +#: __init__.py:23 msgid "Signatures" msgstr "Firmas" -#: __init__.py:29 views.py:65 +#: __init__.py:39 views.py:67 msgid "private keys" msgstr "llaves privadas" -#: __init__.py:30 views.py:68 +#: __init__.py:40 views.py:70 msgid "public keys" msgstr "llaves públicas" -#: __init__.py:31 +#: __init__.py:41 msgid "delete" msgstr "borrar" -#: __init__.py:33 -msgid "Import" -msgstr "Importar" +#: __init__.py:42 +msgid "query keyservers" +msgstr "consultar servidor de llaves" -#: __init__.py:36 +#: __init__.py:43 +msgid "import" +msgstr "importar" + +#: __init__.py:44 +msgid "upload signature" +msgstr "subir firma" + +#: __init__.py:45 +msgid "download signature" +msgstr "descargar firma" + +#: __init__.py:46 +msgid "key management" +msgstr "manejo de llaves" + +#: __init__.py:49 msgid "signatures" msgstr "firmas" -#: api.py:20 +#: api.py:22 msgid "Public" -msgstr "" +msgstr "Pública" -#: api.py:21 +#: api.py:23 msgid "Secret" -msgstr "" +msgstr "Secreta" -#: api.py:29 api.py:34 +#: api.py:31 api.py:36 msgid "RSA" msgstr "RSA" -#: api.py:30 +#: api.py:32 msgid "DSA" msgstr "DSA" -#: api.py:35 +#: api.py:37 msgid "Elgamal" -msgstr "" +msgstr "Elgamal" -#: api.py:49 +#: api.py:51 msgid "Bad signature." -msgstr "" +msgstr "Firma invalida." -#: api.py:53 +#: api.py:55 msgid "Document not signed or invalid signature." -msgstr "" +msgstr "Documento no firmado o firma invalida." -#: api.py:57 +#: api.py:59 msgid "Signature error." -msgstr "" +msgstr "Error de firma." -#: api.py:61 +#: api.py:63 msgid "Document is signed but no public key is available for verification." msgstr "" +"El document ha sido firmado pero no hay llave pública disponible para " +"verificación." -#: api.py:65 +#: api.py:67 msgid "Document is signed, and signature is good." -msgstr "" +msgstr "El document ha sido firmado y la firma esta es buen estado." -#: api.py:69 +#: api.py:71 msgid "Document is signed with a valid signature." -msgstr "" +msgstr "El document ha sido firmado y la firma ha sido validada." -#: api.py:142 +#: api.py:144 msgid "unknown" -msgstr "" +msgstr "desconocida" #: forms.py:11 msgid "Term" @@ -117,140 +143,160 @@ msgstr "Término" #: forms.py:12 msgid "Name, e-mail, key ID or key fingerprint to look for." msgstr "" +"Nombre, dirección de correo electrónico, identificador de llave or huella " +"digital de llave a buscar." -#: views.py:43 +#: forms.py:18 +msgid "Signature file" +msgstr "Archivo de firma" + +#: views.py:45 #, python-format msgid "Key: %s, imported successfully." -msgstr "" +msgstr "Llave: %s, importada exitosamente." -#: views.py:46 +#: views.py:48 #, python-format msgid "Unable to import key id: %s" -msgstr "" +msgstr "No se pudo importa la llave: %s" -#: views.py:50 +#: views.py:52 msgid "Import key" -msgstr "" +msgstr "Importar llave" -#: views.py:51 +#: views.py:53 #, python-format msgid "Are you sure you wish to import key id: %s?" msgstr "¿Esta seguro que desea importar la llave: %s?" -#: views.py:76 +#: views.py:78 msgid "Key ID" -msgstr "" +msgstr "Identificador de llave" -#: views.py:80 +#: views.py:82 msgid "Owner" msgstr "Dueño" -#: views.py:100 +#: views.py:102 #, python-format msgid "Key: %s, deleted successfully." msgstr "Llave: %s, borrada exitosamente." -#: views.py:107 +#: views.py:109 msgid "Delete key" msgstr "Borrar llave" -#: views.py:109 +#: views.py:111 #, python-format msgid "" "Are you sure you wish to delete key: %s? If you try to delete a public key " "that is part of a public/private pair the private key will be deleted as " "well." msgstr "" +"¿Esta seguro que desea borrar la llave: %s? Si trata de borrar una llave " +"pública que es parte de un par público/privado la llave privada será borrada" +" también." -#: views.py:127 +#: views.py:129 msgid "Query key server" -msgstr "" +msgstr "Consultar servidor de llaves" -#: views.py:140 +#: views.py:142 msgid "results" msgstr "resultados" -#: views.py:145 +#: views.py:147 msgid "ID" -msgstr "" +msgstr "ID" -#: views.py:149 +#: views.py:151 msgid "type" msgstr "tipo" -#: views.py:153 +#: views.py:155 msgid "creation date" msgstr "fecha de creación" -#: views.py:157 +#: views.py:159 msgid "disabled" -msgstr "" +msgstr "desactivada" -#: views.py:161 +#: views.py:163 msgid "expiration date" -msgstr "" +msgstr "fecha de expiración" -#: views.py:165 +#: views.py:167 msgid "expired" -msgstr "" +msgstr "expirada" -#: views.py:169 +#: views.py:171 msgid "length" -msgstr "" +msgstr "tamaño" -#: views.py:173 +#: views.py:175 msgid "revoked" -msgstr "" +msgstr "revocada" -#: views.py:178 +#: views.py:180 msgid "Identifies" -msgstr "" +msgstr "Identidades" #: views.py:205 #, python-format msgid "Signature status: %(widget)s %(text)s" -msgstr "" +msgstr "Estado de la firma: %(widget)s %(text)s" #: views.py:212 msgid "embedded" -msgstr "" +msgstr "integrada" #: views.py:214 msgid "detached" -msgstr "" +msgstr "a parte" #: views.py:219 #, python-format msgid "Signature ID: %s" -msgstr "" +msgstr "ID de la firma: %s" #: views.py:220 #, python-format msgid "Signature type: %s" -msgstr "" +msgstr "Tipo de firma: %s" #: views.py:221 #, python-format msgid "Key ID: %s" -msgstr "" +msgstr "ID de la llave: %s" #: views.py:222 #, python-format msgid "Timestamp: %s" -msgstr "" +msgstr "Fecha y hora: %s" #: views.py:223 #, python-format msgid "Signee: %s" -msgstr "" +msgstr "Firmantes: %s" #: views.py:228 #, python-format msgid "signature properties for: %s" -msgstr "" +msgstr "propiedades de firma para: %s" + +#: views.py:250 +msgid "Detached signature uploaded successfully." +msgstr "El archivo de firma fue subido exitosamente." + +#: views.py:259 +#, python-format +msgid "Upload detached signature for: %s" +msgstr "Subir firma a parte para: %s" #: conf/settings.py:13 msgid "List of keyservers to be queried for unknown keys." msgstr "" +"Lista de servidores de llaves a ser utilizados para buscar llaves " +"desconocidas." diff --git a/apps/django_gpg/locale/pt/LC_MESSAGES/django.mo b/apps/django_gpg/locale/pt/LC_MESSAGES/django.mo index 7fae94e56b671cf8420121b6a06c89cdcdbb91eb..9aea042e15ed1726fc95360f0144fddecdfc5543 100644 GIT binary patch delta 72 zcmeBSdCELNg>m&nRZkVJ0RIqO=c3falFa-(U6;g?R4WA|14BbyLnB=SGX(=9D+9BM bI}h^sB<7`;CZ+>r3rZ$eGDc5+$fykfEixAB delta 85 zcmaFL+`}?Kh4IovRZsPdl9B=|ef{*zl8n+Mz2y8{{i6Kh#G=gnBK_RN%EUbV%;MtG p)Z&SGvh1b`hQ?MV1`{V8oSeWI#qE=rmtLBfo@%8~FnI@~E&wN<9y|a5 diff --git a/apps/django_gpg/locale/pt/LC_MESSAGES/django.po b/apps/django_gpg/locale/pt/LC_MESSAGES/django.po index 00d912b06e..7b89397c22 100644 --- a/apps/django_gpg/locale/pt/LC_MESSAGES/django.po +++ b/apps/django_gpg/locale/pt/LC_MESSAGES/django.po @@ -1,20 +1,21 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: Mayan EDMS\n" -"Report-Msgid-Bugs-To: http://github.com/rosarior/mayan/issues\n" -"POT-Creation-Date: 2011-12-05 13:40-0400\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-12-06 02:06-0400\n" "PO-Revision-Date: 2011-12-05 17:43+0000\n" "Last-Translator: rosarior \n" -"Language-Team: Portuguese (http://www.transifex.net/projects/p/mayan-edms/team/pt/)\n" +"Language-Team: Portuguese (http://www.transifex.net/projects/p/mayan-edms/" +"team/pt/)\n" +"Language: pt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: pt\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #: __init__.py:14 @@ -29,7 +30,7 @@ msgstr "" msgid "Delete keys" msgstr "" -#: __init__.py:17 __init__.py:32 +#: __init__.py:17 msgid "Query keyservers" msgstr "" @@ -37,75 +38,99 @@ msgstr "" msgid "Import key from keyservers" msgstr "" -#: __init__.py:21 +#: __init__.py:19 +msgid "Upload detached signatures" +msgstr "" + +#: __init__.py:20 +msgid "Download detached signatures" +msgstr "" + +#: __init__.py:23 msgid "Signatures" msgstr "" -#: __init__.py:29 views.py:65 +#: __init__.py:39 views.py:67 msgid "private keys" msgstr "" -#: __init__.py:30 views.py:68 +#: __init__.py:40 views.py:70 msgid "public keys" msgstr "" -#: __init__.py:31 +#: __init__.py:41 msgid "delete" msgstr "" -#: __init__.py:33 -msgid "Import" +#: __init__.py:42 +msgid "query keyservers" msgstr "" -#: __init__.py:36 +#: __init__.py:43 +msgid "import" +msgstr "" + +#: __init__.py:44 +msgid "upload signature" +msgstr "" + +#: __init__.py:45 +msgid "download signature" +msgstr "" + +#: __init__.py:46 +msgid "key management" +msgstr "" + +#: __init__.py:49 msgid "signatures" msgstr "" -#: api.py:20 +#: api.py:22 msgid "Public" msgstr "" -#: api.py:21 +#: api.py:23 msgid "Secret" msgstr "" -#: api.py:29 api.py:34 +#: api.py:31 api.py:36 msgid "RSA" msgstr "" -#: api.py:30 +#: api.py:32 msgid "DSA" msgstr "" -#: api.py:35 +#: api.py:37 msgid "Elgamal" msgstr "" -#: api.py:49 +#: api.py:51 msgid "Bad signature." msgstr "" -#: api.py:53 +#: api.py:55 msgid "Document not signed or invalid signature." msgstr "" -#: api.py:57 +#: api.py:59 msgid "Signature error." msgstr "" -#: api.py:61 +#: api.py:63 msgid "Document is signed but no public key is available for verification." msgstr "" -#: api.py:65 +#: api.py:67 msgid "Document is signed, and signature is good." msgstr "" -#: api.py:69 +#: api.py:71 msgid "Document is signed with a valid signature." msgstr "" -#: api.py:142 +#: api.py:144 msgid "unknown" msgstr "" @@ -117,43 +142,47 @@ msgstr "" msgid "Name, e-mail, key ID or key fingerprint to look for." msgstr "" -#: views.py:43 +#: forms.py:18 +msgid "Signature file" +msgstr "" + +#: views.py:45 #, python-format msgid "Key: %s, imported successfully." msgstr "" -#: views.py:46 +#: views.py:48 #, python-format msgid "Unable to import key id: %s" msgstr "" -#: views.py:50 +#: views.py:52 msgid "Import key" msgstr "" -#: views.py:51 +#: views.py:53 #, python-format msgid "Are you sure you wish to import key id: %s?" msgstr "" -#: views.py:76 +#: views.py:78 msgid "Key ID" msgstr "" -#: views.py:80 +#: views.py:82 msgid "Owner" msgstr "" -#: views.py:100 +#: views.py:102 #, python-format msgid "Key: %s, deleted successfully." msgstr "" -#: views.py:107 +#: views.py:109 msgid "Delete key" msgstr "" -#: views.py:109 +#: views.py:111 #, python-format msgid "" "Are you sure you wish to delete key: %s? If you try to delete a public key " @@ -161,47 +190,47 @@ msgid "" "well." msgstr "" -#: views.py:127 +#: views.py:129 msgid "Query key server" msgstr "" -#: views.py:140 +#: views.py:142 msgid "results" msgstr "" -#: views.py:145 +#: views.py:147 msgid "ID" msgstr "" -#: views.py:149 +#: views.py:151 msgid "type" msgstr "" -#: views.py:153 +#: views.py:155 msgid "creation date" msgstr "" -#: views.py:157 +#: views.py:159 msgid "disabled" msgstr "" -#: views.py:161 +#: views.py:163 msgid "expiration date" msgstr "" -#: views.py:165 +#: views.py:167 msgid "expired" msgstr "" -#: views.py:169 +#: views.py:171 msgid "length" msgstr "" -#: views.py:173 +#: views.py:175 msgid "revoked" msgstr "" -#: views.py:178 +#: views.py:180 msgid "Identifies" msgstr "" @@ -248,8 +277,15 @@ msgstr "" msgid "signature properties for: %s" msgstr "" +#: views.py:250 +msgid "Detached signature uploaded successfully." +msgstr "" + +#: views.py:259 +#, python-format +msgid "Upload detached signature for: %s" +msgstr "" + #: conf/settings.py:13 msgid "List of keyservers to be queried for unknown keys." msgstr "" - - diff --git a/apps/django_gpg/locale/ru/LC_MESSAGES/django.mo b/apps/django_gpg/locale/ru/LC_MESSAGES/django.mo index 2541ca14bf8115e61ff6ea7c4a17ae73c7952fb8..93a6b8285844a1db89bc56a295db00cb4da1c6ae 100644 GIT binary patch delta 73 zcmcc2vW8`X3ezXXiE17yTmk+ey3R$Zi6xo&dAcr%C8<^lMh1q4x`sx&24)HdMpg!9 c6L;?8@kz`}FHKAb$`+MQE@O$$L^TifjFOT9D}DX+%#w`KB)#PPT>YZ_;>4oN{389_#LC1x{mkOx r($wOKd9v)L3Wmm3CI%BH?VB9S7{%k0n3rCfn4W5-P*gg38)F#&6*eAD diff --git a/apps/django_gpg/locale/ru/LC_MESSAGES/django.po b/apps/django_gpg/locale/ru/LC_MESSAGES/django.po index 54d387774b..fdc8b3cca2 100644 --- a/apps/django_gpg/locale/ru/LC_MESSAGES/django.po +++ b/apps/django_gpg/locale/ru/LC_MESSAGES/django.po @@ -1,21 +1,23 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: Mayan EDMS\n" -"Report-Msgid-Bugs-To: http://github.com/rosarior/mayan/issues\n" -"POT-Creation-Date: 2011-12-05 13:40-0400\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-12-06 02:06-0400\n" "PO-Revision-Date: 2011-12-05 17:43+0000\n" "Last-Translator: rosarior \n" -"Language-Team: Russian (http://www.transifex.net/projects/p/mayan-edms/team/ru/)\n" +"Language-Team: Russian (http://www.transifex.net/projects/p/mayan-edms/team/" +"ru/)\n" +"Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: ru\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" #: __init__.py:14 msgid "Verify document signatures" @@ -29,7 +31,7 @@ msgstr "" msgid "Delete keys" msgstr "" -#: __init__.py:17 __init__.py:32 +#: __init__.py:17 msgid "Query keyservers" msgstr "" @@ -37,75 +39,99 @@ msgstr "" msgid "Import key from keyservers" msgstr "" -#: __init__.py:21 +#: __init__.py:19 +msgid "Upload detached signatures" +msgstr "" + +#: __init__.py:20 +msgid "Download detached signatures" +msgstr "" + +#: __init__.py:23 msgid "Signatures" msgstr "" -#: __init__.py:29 views.py:65 +#: __init__.py:39 views.py:67 msgid "private keys" msgstr "" -#: __init__.py:30 views.py:68 +#: __init__.py:40 views.py:70 msgid "public keys" msgstr "" -#: __init__.py:31 +#: __init__.py:41 msgid "delete" msgstr "" -#: __init__.py:33 -msgid "Import" +#: __init__.py:42 +msgid "query keyservers" msgstr "" -#: __init__.py:36 +#: __init__.py:43 +msgid "import" +msgstr "" + +#: __init__.py:44 +msgid "upload signature" +msgstr "" + +#: __init__.py:45 +msgid "download signature" +msgstr "" + +#: __init__.py:46 +msgid "key management" +msgstr "" + +#: __init__.py:49 msgid "signatures" msgstr "" -#: api.py:20 +#: api.py:22 msgid "Public" msgstr "" -#: api.py:21 +#: api.py:23 msgid "Secret" msgstr "" -#: api.py:29 api.py:34 +#: api.py:31 api.py:36 msgid "RSA" msgstr "" -#: api.py:30 +#: api.py:32 msgid "DSA" msgstr "" -#: api.py:35 +#: api.py:37 msgid "Elgamal" msgstr "" -#: api.py:49 +#: api.py:51 msgid "Bad signature." msgstr "" -#: api.py:53 +#: api.py:55 msgid "Document not signed or invalid signature." msgstr "" -#: api.py:57 +#: api.py:59 msgid "Signature error." msgstr "" -#: api.py:61 +#: api.py:63 msgid "Document is signed but no public key is available for verification." msgstr "" -#: api.py:65 +#: api.py:67 msgid "Document is signed, and signature is good." msgstr "" -#: api.py:69 +#: api.py:71 msgid "Document is signed with a valid signature." msgstr "" -#: api.py:142 +#: api.py:144 msgid "unknown" msgstr "" @@ -117,43 +143,47 @@ msgstr "" msgid "Name, e-mail, key ID or key fingerprint to look for." msgstr "" -#: views.py:43 +#: forms.py:18 +msgid "Signature file" +msgstr "" + +#: views.py:45 #, python-format msgid "Key: %s, imported successfully." msgstr "" -#: views.py:46 +#: views.py:48 #, python-format msgid "Unable to import key id: %s" msgstr "" -#: views.py:50 +#: views.py:52 msgid "Import key" msgstr "" -#: views.py:51 +#: views.py:53 #, python-format msgid "Are you sure you wish to import key id: %s?" msgstr "" -#: views.py:76 +#: views.py:78 msgid "Key ID" msgstr "" -#: views.py:80 +#: views.py:82 msgid "Owner" msgstr "" -#: views.py:100 +#: views.py:102 #, python-format msgid "Key: %s, deleted successfully." msgstr "" -#: views.py:107 +#: views.py:109 msgid "Delete key" msgstr "" -#: views.py:109 +#: views.py:111 #, python-format msgid "" "Are you sure you wish to delete key: %s? If you try to delete a public key " @@ -161,47 +191,47 @@ msgid "" "well." msgstr "" -#: views.py:127 +#: views.py:129 msgid "Query key server" msgstr "" -#: views.py:140 +#: views.py:142 msgid "results" msgstr "" -#: views.py:145 +#: views.py:147 msgid "ID" msgstr "" -#: views.py:149 +#: views.py:151 msgid "type" msgstr "" -#: views.py:153 +#: views.py:155 msgid "creation date" msgstr "" -#: views.py:157 +#: views.py:159 msgid "disabled" msgstr "" -#: views.py:161 +#: views.py:163 msgid "expiration date" msgstr "" -#: views.py:165 +#: views.py:167 msgid "expired" msgstr "" -#: views.py:169 +#: views.py:171 msgid "length" msgstr "" -#: views.py:173 +#: views.py:175 msgid "revoked" msgstr "" -#: views.py:178 +#: views.py:180 msgid "Identifies" msgstr "" @@ -248,8 +278,15 @@ msgstr "" msgid "signature properties for: %s" msgstr "" +#: views.py:250 +msgid "Detached signature uploaded successfully." +msgstr "" + +#: views.py:259 +#, python-format +msgid "Upload detached signature for: %s" +msgstr "" + #: conf/settings.py:13 msgid "List of keyservers to be queried for unknown keys." msgstr "" - - From ac1c1ff6a4154b8b995ccdf83fc0157ff0a2a4b5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 03:22:14 -0400 Subject: [PATCH 89/92] Documentation update --- docs/changelog.rst | 45 ++++++++++++++++++++++--------------------- docs/conf.py | 4 ++-- docs/credits.rst | 10 ++++++++++ docs/features.rst | 43 +++++++++++++++++++++++++++++++++++++++-- docs/index.rst | 1 - docs/requirements.rst | 1 + docs/settings.rst | 35 ++++++++++++++++++++++++++++----- 7 files changed, 107 insertions(+), 32 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d04f4b2407..80d0787351 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,19 +1,22 @@ -2011-12-2 ---------- -* Added migrations and model updated to support document versions - -2011-12-1 ---------- -* OCR queue processing improvements -* Office documents handling improvements -* Text extraction support for office documents -* RTF text documents are now handled as office documents +Version 0.11 +------------ +* Support for signed documents verification added, embedded and detached + signatures are supported. When verifying a document Mayan EDMS will + try to fetch the public key from the list of keyservers provided in the + configuration option SIGNATURES_KEYSERVERS (which defaults to + 'pool.sks-keyservers.net'). A public key management view has been added + to the setup menu as well as a key query and fetching view to manually + import keys from a keyserver. +* Added support for document versions. Users can upload unlimited amount + of versions for a document using a very flexible document version numbering + system, users can also revert to a previous document version. +* OCR queue processing improvements. +* Office documents handling improvements. +* Text extraction support for office documents. +* RTF text documents are now handled as office documents. * Added a view to delete the document image cache, useful when switching - converter backends or doing diagnostics - -2011-11-30 ----------- -* Added South to the requirements + converter backends or doing diagnostics. +* Added South to the requirements. * Merged documents' filename and extension database fiels into a single filename field, filename are store as uploaded not manipulation is done Users with existing data must install South and run the appropiate @@ -22,16 +25,11 @@ $ ./manager syncdb $ ./manage.py migrate documents 0001 --fake $ ./manage.py migrate documents - * Added new office document mimetype * application/vnd.ms-office - * Fixed documents not saving the file encoding - - -2011-11-28 ----------- -* Removed extra slash in ajax-loader.gif URL fixes #15, thanks to IHLeanne for finding this one +* Removed extra slash in ajax-loader.gif URL fixes #15, thanks to + IHLeanne for finding this one Version 0.10.1 @@ -41,6 +39,9 @@ Version 0.10.1 $ pip install --upgrade -r requirements/production.txt to upgrade + +* django-compressor is now disabled by default, users must explicitly + enable it adding COMPRESS_ENABLED=True to their settings_local.py file Version 0.10 diff --git a/docs/conf.py b/docs/conf.py index 2b9ba3a5ff..cb26e3e359 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,10 +48,10 @@ copyright = u'2011, Roberto Rosario' # built documents. # # The short X.Y version. -version = '0.10.1' +version = '0.11' # The full version, including alpha/beta/rc tags. -release = '0.10' +release = '0.11' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/credits.rst b/docs/credits.rst index d56f2c0d93..e0b3e815d6 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -112,3 +112,13 @@ Credits * http://pypi.python.org/pypi/django-taggit * Image 392336_7079 (stock exchange) + +* djangorestframework + +* South + +* python-gnupg + +* python-hkp + + diff --git a/docs/features.rst b/docs/features.rst index 75360e15f6..e272d75e40 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -2,6 +2,23 @@ Features ======== + +* Document versioning. + + * Store many versions of the same document, download or revert to a previous version. + +* Electronic signature verification. + + * Check the authenticity of documents by verifying their embedded cryptographic signatures or upload detached signatures for document signed after they were stored. + +* Collaboration tools. + + * Discuss documents, comment on new version of a document. + +* Office document format support. + + * Word processing files? Spreadsheets? Sresentations? They are supported too. + * User defined metadata fields and meta data sets. * Metadata fields can be grouped into sets per technical, legal or structural requirements such as the `Dublin core`_ @@ -27,18 +44,28 @@ Features * Local file or server side file uploads. * Batch upload many documents with the same metadata. + + * Clone a document's metadata for speedier uploads and eliminate repetitive data entry. + * Previews for a great deal of image formats, including PDF. * **Mayan EDMS** provides different file conversion backends with different levels of functionality and requirements to adapt to different deployment environments. * Full text searching. + + * Document can be searched by their text content, their metadata or any other file attribute such as name, extension, etc. + * Configurable document grouping. * Automatic linking of documents based on metadata values or document properties. -* Permissions and roles support. +* Roles support. - * User can created many different roles and are not limited to the traditional limited admin, operator, guest paradigm. + * Users can created an unlimited amount of different roles and are not restricted to the traditional admin, operator, guest paradigm. + +* Fine grained permissions system. + + * There is a permission for every atomic operation performed by users. * Multi page document support. @@ -55,7 +82,19 @@ Features .. _Django: https://www.djangoproject.com/ * Multilingual OCR support. + + * *As supported by the OCR engine tesseract. + * Duplicated document search. + * Plugable storage backends (File based and GridFS included). + + * Very easy to convert other 3rd party such as the ones available for Amazon EC2. + * Color coded tagging. + + * Labeled and color coded tags that are intituitive. + * Staging folders to receive scanned documents directly from network attached scanners. + + * Preview scanned files even before uploading them. diff --git a/docs/index.rst b/docs/index.rst index 2f2f9d96fd..5c115df121 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,7 +43,6 @@ Contents settings updates development - technical contributors credits faq diff --git a/docs/requirements.rst b/docs/requirements.rst index 1ba97459bc..665b9ba641 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -23,6 +23,7 @@ Executables: * ``tesseract-ocr`` - An OCR Engine that was developed at HP Labs between 1985 and 1995... and now at Google. * ``unpaper`` - post-processing scanned and photocopied book pages +* ``gpg`` - The GNU Privacy Guard Optional requirements ===================== diff --git a/docs/settings.rst b/docs/settings.rst index 0f95b103d4..323bf84405 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -88,6 +88,8 @@ Documents Default: ``image_cache`` (relative to the installation path) + The path where the visual representations of the documents are stored for fast display. + Converter --------- @@ -128,19 +130,32 @@ Converter Graphics conversion backend to use. Options are: ``converter.backends.imagemagick``, ``converter.backends.graphicsmagick`` and ``converter.backends.python``. + Suggested options: ``-limit files 1 -limit memory 1GB -limit map 2GB -density 200`` + .. data:: CONVERTER_UNOCONV_PATH Default: ``/usr/bin/unoconv`` + Path to the unoconv program. -Grouping ---------- - -.. data:: GROUPING_SHOW_EMPTY_GROUPS + +.. data:: CONVERTER_UNOCONV_USE_PIPE Default: ``True`` + Use alternate method of connection to LibreOffice using a pipe, it is slower but less prone to segmentation faults. + + +Linking +------- + +.. data:: LINKING_SHOW_EMPTY_SMART_LINKS + + Default: ``True`` + + Show smart links even when they don't return any documents. + Storage ------- @@ -224,7 +239,7 @@ OCR .. data:: OCR_REPLICATION_DELAY - Default: ``10`` + Default: ``0`` Amount of seconds to delay OCR of documents to allow for the node's storage replication overhead. @@ -384,3 +399,13 @@ User management Default: ``[]`` A list of existing roles that are automatically assigned to newly created users + + +Signatures +---------- + +.. data:: SIGNATURES_KEYSERVERS + + Default: ``['pool.sks-keyservers.net']`` + + List of keyservers to be queried for unknown keys. From 4cc0ff3386fe755bafccd652649778129699c618 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 03:30:11 -0400 Subject: [PATCH 90/92] Added home template text --- apps/main/templates/home.html | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/main/templates/home.html b/apps/main/templates/home.html index c4f0d284b2..095ed73001 100644 --- a/apps/main/templates/home.html +++ b/apps/main/templates/home.html @@ -5,6 +5,7 @@

{% project_name %}

+

{% trans 'Django based open source document management system' %}


{% endblock %} {% block footer %} From d6a62feefa5912bdb5664463c813fefe440a76a5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 03:30:27 -0400 Subject: [PATCH 91/92] Home screen translation update --- apps/main/locale/en/LC_MESSAGES/django.po | 20 ++++++---- apps/main/locale/es/LC_MESSAGES/django.mo | Bin 2577 -> 2727 bytes apps/main/locale/es/LC_MESSAGES/django.po | 45 ++++++++++++---------- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/apps/main/locale/en/LC_MESSAGES/django.po b/apps/main/locale/en/LC_MESSAGES/django.po index ad9034e619..3f41a881ab 100644 --- a/apps/main/locale/en/LC_MESSAGES/django.po +++ b/apps/main/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-11-22 11:26-0400\n" +"POT-Creation-Date: 2011-12-06 03:26-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,31 +17,31 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: __init__.py:15 +#: __init__.py:31 msgid "maintenance" msgstr "" -#: __init__.py:16 +#: __init__.py:32 msgid "statistics" msgstr "" -#: __init__.py:17 +#: __init__.py:33 msgid "diagnostics" msgstr "" -#: __init__.py:18 +#: __init__.py:34 msgid "sentry" msgstr "" -#: __init__.py:19 +#: __init__.py:35 msgid "admin site" msgstr "" -#: __init__.py:30 +#: __init__.py:38 msgid "home" msgstr "" -#: __init__.py:32 +#: __init__.py:40 msgid "search" msgstr "" @@ -130,6 +130,10 @@ msgstr "" msgid "Other available actions" msgstr "" +#: templates/home.html:8 +msgid "Django based open source document management system" +msgstr "" + #: templates/project_description.html:6 msgid "" "Open source, Django based electronic document manager with custom metadata, " diff --git a/apps/main/locale/es/LC_MESSAGES/django.mo b/apps/main/locale/es/LC_MESSAGES/django.mo index 128108745e395055f0be6bfbce0982ae8f4b3b11..3c57a910a9e48b2d9537d81617cf048f6d855ec1 100644 GIT binary patch delta 911 zcmYMyKWGzC9Ki8+NpnqWthG)5Xd8VM3MnNaB-mgOR0z71>Y!3Y!83P_k>*bCE`u(a z1cxqqIJh|IAUG+(K|uu9iima+=`J{`gOlIiap;40pZB|a_ulXQxu1VF)!&^MdZ;Le zsq@slq0|!QQamUxa2Q|W1int<54cRcgH=>%rH()iNAM!<$E$b+ecXeeu!i3-qf}pw z4=H8xVj8!wfHU|U_u+e7z|S~_KXDY({7qAhVGbuz64fZ*izpM;Q2L!n;wFw0pF@-R zl@&LXx{mUrhuiGJ9h3o2^N@trFpC>lmVS5?ALAnKB;WsFo;XeCO`OHOco!QOp)Bm9 zyk~y3OXC&(z=QaN{hPwKC@cShlJPH;flRVw4wc7AoJW3D6_kmOq4Zlr+1blU+`?(% zTgXuA9u}m-gQUYFWa05g={OUOq~`jwv?O1)BOdk;%0WnxTT`G)8gtYGR1P*SlCl)J zRdN#~Wl2*GLy9a>irgwLQM~tZkg^4`jJOn=gQuw_qt?7(xouBhvjWG~-c85Vf!Fg} zj<&s4uj{y>?pm(ZcH(AmI|!ZbU_1TB7_OEoE4osPwlWL#q}{02M^~*NES>i)H`ug7 z&u{2ccDLhpg3z~Y&)536CGpAl8U4<*q7Cy>Zj~H*Ow!O!5WO>(i|hY}aO~2BpyRe1 s+Kw|V`Cj0Jp`|P3m2!3P-8^gL&o}{7Y5RZDUJ$jji$-}+&we%j0`gaV-2eap delta 793 zcmXxiKWGzS7{~GF(ln{L#3mf}(>si2raaij$jzQb#-Z{e}2&?|tsQ_wL^3dGCENbTiZXKA5~^ z#4g%Sn(LTN;RY)1Cd|@!4+rs4r~d-8^xt3!f8%c4(`B{^PvBOZ#|2!%K75Vc_#RVc zE&Ia7b#AQTcD#@@+k&e&fj4k7KE_^rg&g*dlZ&5F34TM3|3FRj3pwm>r>|KD=)1_o zHr}>nwwDXNn8hblJ&XF_Po7djf3OGpxS7Wc&frnphgVSJcQAtwv4$_PAEy~qK?hO& zllT+)s_bp2{{e^Te?<;k z=cMrs)bm|b-LK!QK_xI8jU>ies#y!pGPDot}nL88u^+wHa24UF?gN7e22cdT;43;WkGst)D5xx e>($GCwNmyfjp$nHT(q8=j*q8LJMqV!HRm59CtS?{ diff --git a/apps/main/locale/es/LC_MESSAGES/django.po b/apps/main/locale/es/LC_MESSAGES/django.po index c530cc4628..da34696a3f 100644 --- a/apps/main/locale/es/LC_MESSAGES/django.po +++ b/apps/main/locale/es/LC_MESSAGES/django.po @@ -1,49 +1,49 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: # Roberto Rosario , 2011. msgid "" msgstr "" "Project-Id-Version: Mayan EDMS\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-11-22 11:26-0400\n" -"PO-Revision-Date: 2011-11-04 00:56+0000\n" -"Last-Translator: rosarior \n" -"Language-Team: Spanish (Castilian) (http://www.transifex.net/projects/p/" -"mayan-edms/team/es/)\n" -"Language: es\n" +"POT-Creation-Date: 2011-12-06 03:26-0400\n" +"PO-Revision-Date: 2011-12-06 03:27\n" +"Last-Translator: Administrador \n" +"Language-Team: Spanish (Castilian) (http://www.transifex.net/projects/p/mayan-edms/team/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" +"X-Translated-Using: django-rosetta 0.6.2\n" -#: __init__.py:15 +#: __init__.py:31 msgid "maintenance" msgstr "mantenimiento" -#: __init__.py:16 +#: __init__.py:32 msgid "statistics" msgstr "estadísticas" -#: __init__.py:17 +#: __init__.py:33 msgid "diagnostics" msgstr "diagnósticos" -#: __init__.py:18 +#: __init__.py:34 msgid "sentry" msgstr "sentry" -#: __init__.py:19 +#: __init__.py:35 msgid "admin site" msgstr "sitio administrativo" -#: __init__.py:30 +#: __init__.py:38 msgid "home" msgstr "inicio" -#: __init__.py:32 +#: __init__.py:40 msgid "search" msgstr "búsqueda" @@ -61,11 +61,11 @@ msgstr "Diagnósticos" #: conf/settings.py:12 msgid "" -"Controls whether the search functionality is provided by a sidebar widget or " -"by a menu entry." +"Controls whether the search functionality is provided by a sidebar " +"widget or by a menu entry." msgstr "" -"Controla si la funcionalidad de búsqueda es proporcionada por una barra " -"lateral o por una entrada de menú." +"Controla si la funcionalidad de búsqueda es proporcionada por una " +"barra lateral o por una entrada de menú." #: templates/about.html:5 msgid "About this program" @@ -134,10 +134,15 @@ msgstr "Acciones" msgid "Other available actions" msgstr "Otras acciones disponibles" +#: templates/home.html:8 +msgid "Django based open source document management system" +msgstr "Gestor de documentos de código abierto, basado en Django." + #: templates/project_description.html:6 msgid "" -"Open source, Django based electronic document manager with custom metadata, " -"indexing, tagging, file serving integration and OCR capabilities" +"Open source, Django based electronic document manager with custom " +"metadata, indexing, tagging, file serving integration and OCR " +"capabilities" msgstr "" "Gestor documental de código abierto, basado en Django con metadatos " "personaliables, indexación, etiquedado de documentos, integración de " From f7776b3300f9d8102e1c9155e74d5ca0d4e6d76d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 6 Dec 2011 03:32:32 -0400 Subject: [PATCH 92/92] Bumped version to 0.11 --- apps/main/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/main/__init__.py b/apps/main/__init__.py index 79fb05d017..e75d65f796 100644 --- a/apps/main/__init__.py +++ b/apps/main/__init__.py @@ -18,10 +18,10 @@ __status__ = 'Production' __version_info__ = { 'major': 0, - 'minor': 10, + 'minor': 11, 'micro': 0, - 'releaselevel': 'hotfix', - 'serial': 4 + 'releaselevel': 'final', + 'serial': 0 } def is_superuser(context):