From 8ff727c50b83ff3579005d8a7f2e83a870122e52 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 11 Jun 2012 01:25:02 -0400 Subject: [PATCH 01/39] Initial commit adding document checkout support --- apps/checkouts/__init__.py | 39 ++++++ apps/checkouts/exceptions.py | 11 ++ apps/checkouts/forms.py | 21 +++ apps/checkouts/links.py | 12 ++ apps/checkouts/literals.py | 1 + apps/checkouts/managers.py | 26 ++++ apps/checkouts/migrations/0001_initial.py | 122 ++++++++++++++++++ ...o__add_unique_documentcheckout_document.py | 115 +++++++++++++++++ apps/checkouts/migrations/__init__.py | 0 apps/checkouts/models.py | 48 +++++++ apps/checkouts/permissions.py | 12 ++ apps/checkouts/urls.py | 8 ++ apps/checkouts/views.py | 103 +++++++++++++++ apps/permissions/exceptions.py | 5 + settings.py | 1 + urls.py | 1 + 16 files changed, 525 insertions(+) create mode 100644 apps/checkouts/__init__.py create mode 100644 apps/checkouts/exceptions.py create mode 100644 apps/checkouts/forms.py create mode 100644 apps/checkouts/links.py create mode 100644 apps/checkouts/literals.py create mode 100644 apps/checkouts/managers.py create mode 100644 apps/checkouts/migrations/0001_initial.py create mode 100644 apps/checkouts/migrations/0002_auto__add_unique_documentcheckout_document.py create mode 100644 apps/checkouts/migrations/__init__.py create mode 100644 apps/checkouts/models.py create mode 100644 apps/checkouts/permissions.py create mode 100644 apps/checkouts/urls.py create mode 100644 apps/checkouts/views.py create mode 100644 apps/permissions/exceptions.py diff --git a/apps/checkouts/__init__.py b/apps/checkouts/__init__.py new file mode 100644 index 0000000000..99fdc80c60 --- /dev/null +++ b/apps/checkouts/__init__.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import + +from django.utils.translation import ugettext_lazy as _ + +from navigation.api import (register_links, register_top_menu, + register_multi_item_links, register_sidebar_template) + +from documents.models import Document +from documents.permissions import PERMISSION_DOCUMENT_VIEW +from acls.api import class_permissions + +from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN) +from .links import checkout_list, checkout_document, checkout_info, checkin_document +from .models import DocumentCheckout + + +def initialize_document_checkout_extra_methods(): + Document.add_to_class('is_checked_out', lambda document: DocumentCheckout.objects.is_document_checked_out(document)) + Document.add_to_class('check_in', lambda document: DocumentCheckout.objects.check_in_document(document)) + +#register_multi_item_links(['folder_view'], [folder_document_multiple_remove]) +#register_links(Folder, [folder_view, folder_edit, folder_delete, folder_acl_list]) +#register_links([Folder, 'folder_list', 'folder_create'], [folder_list, folder_create], menu_name='secondary_menu') +register_top_menu(name='checkouts', link=checkout_list)#, children_views=['folder_list', 'folder_create', 'folder_edit', 'folder_delete', 'folder_view', 'folder_document_multiple_remove']) +register_links(Document, [checkout_info], menu_name='form_header') +#register_sidebar_template(['folder_list'], 'folders_help.html') +register_links(['checkout_info', 'checkout_document', 'checkin_document'], [checkout_document, checkin_document], menu_name="sidebar") + +class_permissions(Document, [ + PERMISSION_DOCUMENT_CHECKOUT, + PERMISSION_DOCUMENT_CHECKIN, +]) + +initialize_document_checkout_extra_methods() + + +#TODO: default checkout time +#TODO: forcefull check in +#TODO: specify checkout option check (document.allows_new_versions()) diff --git a/apps/checkouts/exceptions.py b/apps/checkouts/exceptions.py new file mode 100644 index 0000000000..e27b9645de --- /dev/null +++ b/apps/checkouts/exceptions.py @@ -0,0 +1,11 @@ +class DocumentNotCheckedOut(Exception): + """ + Raised when trying to checkin a document that is not checkedout + """ + pass + +class DocumentAlreadyCheckedOut(Exception): + """ + Raised when trying to checkout an already checkedout document + """ + pass diff --git a/apps/checkouts/forms.py b/apps/checkouts/forms.py new file mode 100644 index 0000000000..205f386d86 --- /dev/null +++ b/apps/checkouts/forms.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .models import DocumentCheckout + + +class DocumentCheckoutForm(forms.ModelForm): + days = forms.IntegerField(min_value=0, label=_(u'Days'), help_text=_(u'Amount of time to hold the document checked out in days.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 10em;'})) + hours = forms.IntegerField(min_value=0, label=_(u'Hours'), help_text=_(u'Amount of time to hold the document checked out in hours.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 10em;'})) + minutes = forms.IntegerField(min_value=0, label=_(u'Minutes'), help_text=_(u'Amount of time to hold the document checked out in minutes.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 10em;'})) + + class Meta: + model = DocumentCheckout + exclude = ('expiration_datetime', ) + #fields = ('username', 'first_name', 'last_name', 'email', 'is_staff', 'is_superuser', 'last_login', 'date_joined', 'groups') + + #def clean(self): + + diff --git a/apps/checkouts/links.py b/apps/checkouts/links.py new file mode 100644 index 0000000000..a72c0adea9 --- /dev/null +++ b/apps/checkouts/links.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from django.utils.translation import ugettext_lazy as _ + +from documents.permissions import PERMISSION_DOCUMENT_VIEW + +from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN) + +checkout_list = {'text': _(u'check in/out'), 'view': 'checkout_list', 'famfam': 'basket'} +checkout_document = {'text': _('check out document'), 'view': 'checkout_document', 'args': 'object.pk', 'famfam': 'basket_put'}#, 'permissions': [PERMISSION_DOCUMENT_CHECKOUT]} +checkin_document = {'text': _('check out document'), 'view': 'checkout_document', 'args': 'object.pk', 'famfam': 'basket_remove'}#, 'permissions': [PERMISSION_DOCUMENT_CHECKIN]} +checkout_info = {'text': _('check in/out'), 'view': 'checkout_info', 'args': 'object.pk', 'famfam': 'basket', 'children_views': ['checkout_document', 'checkin_document']}#, 'permissions': [PERMISSION_DOCUMENT_CHECKIN]} diff --git a/apps/checkouts/literals.py b/apps/checkouts/literals.py new file mode 100644 index 0000000000..c3961685ab --- /dev/null +++ b/apps/checkouts/literals.py @@ -0,0 +1 @@ +from __future__ import absolute_import diff --git a/apps/checkouts/managers.py b/apps/checkouts/managers.py new file mode 100644 index 0000000000..8fc1f69276 --- /dev/null +++ b/apps/checkouts/managers.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import + +from django.db import models + +from documents.models import Document + +from .exceptions import DocumentNotCheckedOut + + +class DocumentCheckoutManager(models.Manager): + def checked_out(self): + return Document.objects.filter(pk__in=self.model.objects.all().values_list('pk', flat=True)) + + def is_document_checked_out(self, document): + if self.model.objects.filter(document=document): + return True + else: + return False + + def check_in_document(self, document): + try: + document_checkout = self.model.objects.get(document=document) + except self.model.DoesNotExist: + raise DocumentNotCheckedOut + else: + document_checkout.delete() diff --git a/apps/checkouts/migrations/0001_initial.py b/apps/checkouts/migrations/0001_initial.py new file mode 100644 index 0000000000..a4ff9d11a2 --- /dev/null +++ b/apps/checkouts/migrations/0001_initial.py @@ -0,0 +1,122 @@ +# -*- coding: 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 'DocumentCheckout' + db.create_table('checkouts_documentcheckout', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('document', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.Document'])), + ('checkout_datetime', self.gf('django.db.models.fields.DateTimeField')()), + ('expiration_datetime', self.gf('django.db.models.fields.DateTimeField')()), + ('block_new_version', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('checkouts', ['DocumentCheckout']) + + + def backwards(self, orm): + # Deleting model 'DocumentCheckout' + db.delete_table('checkouts_documentcheckout') + + + 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'}) + }, + 'checkouts.documentcheckout': { + 'Meta': {'object_name': 'DocumentCheckout'}, + 'block_new_version': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'checkout_datetime': ('django.db.models.fields.DateTimeField', [], {}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'expiration_datetime': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + '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', [], {'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.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'}) + }, + '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'}) + }, + '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 = ['checkouts'] \ No newline at end of file diff --git a/apps/checkouts/migrations/0002_auto__add_unique_documentcheckout_document.py b/apps/checkouts/migrations/0002_auto__add_unique_documentcheckout_document.py new file mode 100644 index 0000000000..bbdb79b350 --- /dev/null +++ b/apps/checkouts/migrations/0002_auto__add_unique_documentcheckout_document.py @@ -0,0 +1,115 @@ +# -*- coding: 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 unique constraint on 'DocumentCheckout', fields ['document'] + db.create_unique('checkouts_documentcheckout', ['document_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'DocumentCheckout', fields ['document'] + db.delete_unique('checkouts_documentcheckout', ['document_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'}) + }, + 'checkouts.documentcheckout': { + 'Meta': {'object_name': 'DocumentCheckout'}, + 'block_new_version': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'checkout_datetime': ('django.db.models.fields.DateTimeField', [], {}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']", 'unique': 'True'}), + 'expiration_datetime': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + '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', [], {'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.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'}) + }, + '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'}) + }, + '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 = ['checkouts'] \ No newline at end of file diff --git a/apps/checkouts/migrations/__init__.py b/apps/checkouts/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/checkouts/models.py b/apps/checkouts/models.py new file mode 100644 index 0000000000..6de45fca9b --- /dev/null +++ b/apps/checkouts/models.py @@ -0,0 +1,48 @@ +from __future__ import absolute_import + +import logging +import datetime + +from django.db import models, IntegrityError +from django.utils.translation import ugettext_lazy as _ + +from documents.models import Document + +from .managers import DocumentCheckoutManager +from .exceptions import DocumentAlreadyCheckedOut + +logger = logging.getLogger(__name__) + + +class DocumentCheckout(models.Model): + """ + Model to store the state and information of a document checkout + """ + document = models.ForeignKey(Document, verbose_name=_(u'document'), unique=True, editable=False) + checkout_datetime = models.DateTimeField(verbose_name=_(u'checkout date and time'), editable=False) + expiration_datetime = models.DateTimeField(verbose_name=_(u'checkout expiration date and time')) + block_new_version = models.BooleanField(verbose_name=_(u'block new version upload'), help_text=_(u'Do not allow new version of this document to be uploaded.')) + #block_metadata + #block_editing + #block tag add/remove + + objects = DocumentCheckoutManager() + + def __unicode__(self): + return unicode(self.document) + + def save(self, *args, **kwargs): + if not self.pk: + self.checkout_date = datetime.datetime.now() + try: + return super(DocumentCheckout, self).save(*args, **kwargs) + except IntegrityError: + raise DocumentAlreadyCheckedOut + + @models.permalink + def get_absolute_url(self): + return ('checkout_info', [self.document.pk]) + + class Meta: + verbose_name = _(u'document checkout') + verbose_name_plural = _(u'document checkouts') diff --git a/apps/checkouts/permissions.py b/apps/checkouts/permissions.py new file mode 100644 index 0000000000..2a24c9d47d --- /dev/null +++ b/apps/checkouts/permissions.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from django.utils.translation import ugettext_lazy as _ + +from permissions.models import PermissionNamespace, Permission + +namespace = PermissionNamespace('checkouts', _(u'Document checkout')) + +PERMISSION_DOCUMENT_CHECKOUT = Permission.objects.register(namespace, 'checkout_document', _(u'Check out documents')) +PERMISSION_DOCUMENT_CHECKIN = Permission.objects.register(namespace, 'checkin_document', _(u'Check in documents')) +PERMISSION_DOCUMENT_CHECKIN_OVERRIDE = Permission.objects.register(namespace, 'checkin_document_override', _(u'Forcefully check in documents')) + diff --git a/apps/checkouts/urls.py b/apps/checkouts/urls.py new file mode 100644 index 0000000000..6dd1d8fdd8 --- /dev/null +++ b/apps/checkouts/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns('checkouts.views', + url(r'^list/$', 'checkout_list', (), 'checkout_list'), + url(r'^(?P\d+)/check/out/$', 'checkout_document', (), 'checkout_document'), + url(r'^(?P\d+)/check/in/$', 'checkin_document', (), 'checkin_document'), + url(r'^(?P\d+)/check/info/$', 'checkout_info', (), 'checkout_info'), +) diff --git a/apps/checkouts/views.py b/apps/checkouts/views.py new file mode 100644 index 0000000000..fb43d2f430 --- /dev/null +++ b/apps/checkouts/views.py @@ -0,0 +1,103 @@ +from __future__ import absolute_import + +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.html import mark_safe +from django.conf import settings + +from documents.views import document_list +from documents.models import Document +from permissions.exceptions import PermissionDenied +from permissions.models import Permission +from acls.models import AccessEntry + +from .models import DocumentCheckout +from .permissions import PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN +from .forms import DocumentCheckoutForm +from .exceptions import DocumentAlreadyCheckedOut + + +def checkout_list(request): + return document_list(request, object_list=DocumentCheckout.objects.checked_out(), title=_(u'checked out documents')) + + +def checkout_info(request, document_pk): + document = get_object_or_404(Document, pk=document_pk) + try: + Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN]) + except PermissionDenied: + AccessEntry.objects.check_access([PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN], request.user, document) + + if document.is_checked_out(): + content = 'checkedout' + else: + content = _(u'Document has not been checked out.') + #

{{ content|safe }}

+ #{% endif %} + + #{% for paragraph in paragraphs %} + #

{{ paragraph|safe }}

# + + return render_to_response('generic_template.html', { + 'content': content, + 'object': document, + 'title': _(u'Check out details for document: %s') % document + }, context_instance=RequestContext(request)) + + +def checkout_document(request, document_pk): + document = get_object_or_404(Document, pk=document_pk) + try: + Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_CHECKOUT]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_DOCUMENT_CHECKOUT, request.user, document) + + if request.method == 'POST': + form = DocumentCheckoutForm(request.POST) + if form.is_valid(): + try: + document_checkout = form.save() + except DocumentAlreadyCheckedOut: + messages.error(request, _(u'Document already checked out.')) + except Exception, exc: + messages.error(request, _(u'Error trying to check out document; %s') % exc) + else: + messages.success(request, _(u'Document "%s" checked out successfully.') % document) + return HttpResponseRedirect(document_checkout.get_absolute_url()) + else: + form = DocumentCheckoutForm()#document=document, initial={ + #'new_filename': document.filename}) + + return render_to_response('generic_form.html', { + 'form': form, + 'object': document, + 'title': _(u'Check out document: %s') % document + }, context_instance=RequestContext(request)) + + +def checkin_document(request, document_pk): + document = get_object_or_404(Document, pk=document_pk) + try: + Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_CHECKIN]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_DOCUMENT_CHECKIN, request.user, document) + + if request.method == 'POST': + try: + document.check_in() + except DocumentAlreadyCheckedOut: + messages.error(request, _(u'Document already checked out.')) + except Exception, exc: + messages.error(request, _(u'Error trying to check in document; %s') % exc) + else: + messages.success(request, _(u'Document "%s" checked out successfully.') % document) + return HttpResponseRedirect(reverse('checkout_info', args=[document.pk])) + + return render_to_response('generic_form.html', { + 'object': document, + 'title': _(u'Check in document: %s') % document + }, context_instance=RequestContext(request)) diff --git a/apps/permissions/exceptions.py b/apps/permissions/exceptions.py new file mode 100644 index 0000000000..14c1eb54f6 --- /dev/null +++ b/apps/permissions/exceptions.py @@ -0,0 +1,5 @@ +from django.core.exceptions import PermissionDenied as DjangoPermissionDenied + + +class PermissionDenied(DjangoPermissionDenied): + pass diff --git a/settings.py b/settings.py index a0da8da709..6132d57479 100644 --- a/settings.py +++ b/settings.py @@ -174,6 +174,7 @@ INSTALLED_APPS = ( 'main', 'rest_api', 'document_signatures', + 'checkouts', # Has to be last so the other apps can register it's signals 'signaler', diff --git a/urls.py b/urls.py index a6ae677f51..4f3430b786 100644 --- a/urls.py +++ b/urls.py @@ -32,6 +32,7 @@ urlpatterns = patterns('', (r'^gpg/', include('django_gpg.urls')), (r'^documents/signatures/', include('document_signatures.urls')), (r'^feedback/', include('feedback.urls')), + (r'^checkouts/', include('checkouts.urls')), ) From e265715c7d14bd186718225f76b2dd76251449cd Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Jun 2012 02:42:51 -0400 Subject: [PATCH 02/39] Reduce fields size, compose expiration datetime from the form inputs --- apps/checkouts/forms.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/checkouts/forms.py b/apps/checkouts/forms.py index 205f386d86..0a68c216ec 100644 --- a/apps/checkouts/forms.py +++ b/apps/checkouts/forms.py @@ -1,5 +1,7 @@ from __future__ import absolute_import +import datetime + from django import forms from django.utils.translation import ugettext_lazy as _ @@ -7,15 +9,17 @@ from .models import DocumentCheckout class DocumentCheckoutForm(forms.ModelForm): - days = forms.IntegerField(min_value=0, label=_(u'Days'), help_text=_(u'Amount of time to hold the document checked out in days.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 10em;'})) - hours = forms.IntegerField(min_value=0, label=_(u'Hours'), help_text=_(u'Amount of time to hold the document checked out in hours.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 10em;'})) - minutes = forms.IntegerField(min_value=0, label=_(u'Minutes'), help_text=_(u'Amount of time to hold the document checked out in minutes.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 10em;'})) + days = forms.IntegerField(min_value=0, label=_(u'Days'), help_text=_(u'Amount of time to hold the document checked out in days.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 5em;'})) + hours = forms.IntegerField(min_value=0, label=_(u'Hours'), help_text=_(u'Amount of time to hold the document checked out in hours.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 4, 'style':'width: 5em;'})) + minutes = forms.IntegerField(min_value=0, label=_(u'Minutes'), help_text=_(u'Amount of time to hold the document checked out in minutes.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 5, 'style':'width: 5em;'})) class Meta: model = DocumentCheckout - exclude = ('expiration_datetime', ) - #fields = ('username', 'first_name', 'last_name', 'email', 'is_staff', 'is_superuser', 'last_login', 'date_joined', 'groups') + exclude = ('expiration_datetime', 'document') - #def clean(self): + def clean_expiration_datetime(self): + data = self.cleaned_data['expiration_datetime'] + timedelta = datetime.timedelta(days=self.cleaned_data['days'], hours=self.cleaned_data['hours'], minutes=self.cleaned_data['minutes']) + return datetime.datetime.now() + timedelta From b412a6bf439fa6cde75ee8caa7b2523d774d95aa Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Jun 2012 02:43:28 -0400 Subject: [PATCH 03/39] Rename link name --- apps/checkouts/links.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/checkouts/links.py b/apps/checkouts/links.py index a72c0adea9..5e387d7a34 100644 --- a/apps/checkouts/links.py +++ b/apps/checkouts/links.py @@ -6,7 +6,7 @@ from documents.permissions import PERMISSION_DOCUMENT_VIEW from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN) -checkout_list = {'text': _(u'check in/out'), 'view': 'checkout_list', 'famfam': 'basket'} +checkout_list = {'text': _(u'check ins/outs'), 'view': 'checkout_list', 'famfam': 'basket'} checkout_document = {'text': _('check out document'), 'view': 'checkout_document', 'args': 'object.pk', 'famfam': 'basket_put'}#, 'permissions': [PERMISSION_DOCUMENT_CHECKOUT]} checkin_document = {'text': _('check out document'), 'view': 'checkout_document', 'args': 'object.pk', 'famfam': 'basket_remove'}#, 'permissions': [PERMISSION_DOCUMENT_CHECKIN]} checkout_info = {'text': _('check in/out'), 'view': 'checkout_info', 'args': 'object.pk', 'famfam': 'basket', 'children_views': ['checkout_document', 'checkin_document']}#, 'permissions': [PERMISSION_DOCUMENT_CHECKIN]} From 6568e64c1fdde225f36176c877587511948a1a11 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Jun 2012 02:48:21 -0400 Subject: [PATCH 04/39] Update model to try to detect consecutive checkout of the same document --- apps/checkouts/models.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/checkouts/models.py b/apps/checkouts/models.py index 6de45fca9b..bfba0d8853 100644 --- a/apps/checkouts/models.py +++ b/apps/checkouts/models.py @@ -18,9 +18,9 @@ class DocumentCheckout(models.Model): """ Model to store the state and information of a document checkout """ - document = models.ForeignKey(Document, verbose_name=_(u'document'), unique=True, editable=False) - checkout_datetime = models.DateTimeField(verbose_name=_(u'checkout date and time'), editable=False) - expiration_datetime = models.DateTimeField(verbose_name=_(u'checkout expiration date and time')) + document = models.ForeignKey(Document, verbose_name=_(u'document'), unique=True) + checkout_datetime = models.DateTimeField(verbose_name=_(u'checkout date and time'), editable=False, default=datetime.datetime.now()) + expiration_datetime = models.DateTimeField(verbose_name=_(u'checkout expiration date and time'), default=datetime.datetime.now()) block_new_version = models.BooleanField(verbose_name=_(u'block new version upload'), help_text=_(u'Do not allow new version of this document to be uploaded.')) #block_metadata #block_editing @@ -36,8 +36,11 @@ class DocumentCheckout(models.Model): self.checkout_date = datetime.datetime.now() try: return super(DocumentCheckout, self).save(*args, **kwargs) - except IntegrityError: - raise DocumentAlreadyCheckedOut + except IntegrityError, exc: + #if exc[1] == 'Column \'checkout_datetime\' cannot be null': + # raise DocumentAlreadyCheckedOut + #else: + raise @models.permalink def get_absolute_url(self): From e01c38b9a31029c173fd1b65dc3275cf7cf1b480 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Jun 2012 02:49:36 -0400 Subject: [PATCH 05/39] Improve checkout view --- apps/checkouts/views.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/checkouts/views.py b/apps/checkouts/views.py index fb43d2f430..0f2191affe 100644 --- a/apps/checkouts/views.py +++ b/apps/checkouts/views.py @@ -36,11 +36,6 @@ def checkout_info(request, document_pk): content = 'checkedout' else: content = _(u'Document has not been checked out.') - #

{{ content|safe }}

- #{% endif %} - - #{% for paragraph in paragraphs %} - #

{{ paragraph|safe }}

# return render_to_response('generic_template.html', { 'content': content, @@ -57,10 +52,12 @@ def checkout_document(request, document_pk): AccessEntry.objects.check_access(PERMISSION_DOCUMENT_CHECKOUT, request.user, document) if request.method == 'POST': - form = DocumentCheckoutForm(request.POST) + form = DocumentCheckoutForm(data=request.POST) if form.is_valid(): try: - document_checkout = form.save() + document_checkout = form.save(commit=False) + document_checkout.document = document + document_checkout.save() except DocumentAlreadyCheckedOut: messages.error(request, _(u'Document already checked out.')) except Exception, exc: @@ -69,8 +66,7 @@ def checkout_document(request, document_pk): messages.success(request, _(u'Document "%s" checked out successfully.') % document) return HttpResponseRedirect(document_checkout.get_absolute_url()) else: - form = DocumentCheckoutForm()#document=document, initial={ - #'new_filename': document.filename}) + form = DocumentCheckoutForm() return render_to_response('generic_form.html', { 'form': form, From 144e01dc97c33d84bd14198f5d2a4ad73b647221 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Jun 2012 22:41:01 -0400 Subject: [PATCH 06/39] Add sample custom field and widget to fashion the split checkout expiration field from this one --- apps/checkouts/forms.py | 77 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/apps/checkouts/forms.py b/apps/checkouts/forms.py index 0a68c216ec..428ec7618b 100644 --- a/apps/checkouts/forms.py +++ b/apps/checkouts/forms.py @@ -8,18 +8,81 @@ from django.utils.translation import ugettext_lazy as _ from .models import DocumentCheckout +class SplitDateTimeWidget(forms.widgets.MultiWidget): + """ + A Widget that splits datetime input into two boxes. + """ + date_format = forms.widgets.DateInput.format + time_format = forms.widgets.TimeInput.format + + def __init__(self, attrs=None, date_format=None, time_format=None): + widgets = (forms.widgets.DateInput(attrs=attrs, format=date_format), + forms.widgets.TimeInput(attrs=attrs, format=time_format)) + super(SplitDateTimeWidget, self).__init__(widgets, attrs) + + def decompress(self, value): + if value: + return [value.date(), value.time().replace(microsecond=0)] + return [None, None] + +class SplitHiddenDateTimeWidget(forms.widgets.SplitDateTimeWidget): + """ + A Widget that splits datetime input into two inputs. + """ + is_hidden = True + + def __init__(self, attrs=None, date_format=None, time_format=None): + super(SplitHiddenDateTimeWidget, self).__init__(attrs, date_format, time_format) + for widget in self.widgets: + widget.input_type = 'hidden' + widget.is_hidden = True + + +class SplitTimeDeltaField(forms.MultiValueField): + widget = SplitDateTimeWidget + hidden_widget = SplitHiddenDateTimeWidget + default_error_messages = { + 'invalid_date': _(u'Enter a valid date.'), + 'invalid_time': _(u'Enter a valid time.'), + } + + def __init__(self, input_date_formats=None, input_time_formats=None, *args, **kwargs): + errors = self.default_error_messages.copy() + if 'error_messages' in kwargs: + errors.update(kwargs['error_messages']) + localize = kwargs.get('localize', False) + fields = ( + forms.DateField(input_formats=input_date_formats, + error_messages={'invalid': errors['invalid_date']}, + localize=localize), + forms.TimeField(input_formats=input_time_formats, + error_messages={'invalid': errors['invalid_time']}, + localize=localize), + ) + super(SplitTimeDeltaField, self).__init__(fields, *args, **kwargs) + + def compress(self, data_list): + if data_list: + # Raise a validation error if time or date is empty + # (possible if SplitDateTimeField has required=False). + if data_list[0] in validators.EMPTY_VALUES: + raise ValidationError(self.error_messages['invalid_date']) + if data_list[1] in validators.EMPTY_VALUES: + raise ValidationError(self.error_messages['invalid_time']) + return datetime.datetime.combine(*data_list) + return None + + class DocumentCheckoutForm(forms.ModelForm): days = forms.IntegerField(min_value=0, label=_(u'Days'), help_text=_(u'Amount of time to hold the document checked out in days.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 5em;'})) hours = forms.IntegerField(min_value=0, label=_(u'Hours'), help_text=_(u'Amount of time to hold the document checked out in hours.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 4, 'style':'width: 5em;'})) minutes = forms.IntegerField(min_value=0, label=_(u'Minutes'), help_text=_(u'Amount of time to hold the document checked out in minutes.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 5, 'style':'width: 5em;'})) + expiration_datetime = SplitTimeDeltaField() class Meta: model = DocumentCheckout - exclude = ('expiration_datetime', 'document') + widgets = { + 'document': forms.widgets.HiddenInput(), + } - def clean_expiration_datetime(self): - data = self.cleaned_data['expiration_datetime'] - timedelta = datetime.timedelta(days=self.cleaned_data['days'], hours=self.cleaned_data['hours'], minutes=self.cleaned_data['minutes']) - return datetime.datetime.now() + timedelta - - + From 03cd450839a779be021b53b8a8d98a0b9eae99be Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Jun 2012 22:41:48 -0400 Subject: [PATCH 07/39] Add conditional display to check in and out links --- apps/checkouts/links.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/checkouts/links.py b/apps/checkouts/links.py index 5e387d7a34..8e7c2049ae 100644 --- a/apps/checkouts/links.py +++ b/apps/checkouts/links.py @@ -6,7 +6,16 @@ from documents.permissions import PERMISSION_DOCUMENT_VIEW from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN) + +def is_checked_out(context): + return context['object'].is_checked_out() + + +def is_not_checked_out(context): + return not context['object'].is_checked_out() + + checkout_list = {'text': _(u'check ins/outs'), 'view': 'checkout_list', 'famfam': 'basket'} -checkout_document = {'text': _('check out document'), 'view': 'checkout_document', 'args': 'object.pk', 'famfam': 'basket_put'}#, 'permissions': [PERMISSION_DOCUMENT_CHECKOUT]} -checkin_document = {'text': _('check out document'), 'view': 'checkout_document', 'args': 'object.pk', 'famfam': 'basket_remove'}#, 'permissions': [PERMISSION_DOCUMENT_CHECKIN]} +checkout_document = {'text': _('check out document'), 'view': 'checkout_document', 'args': 'object.pk', 'famfam': 'basket_put', 'condition': is_not_checked_out}#, 'permissions': [PERMISSION_DOCUMENT_CHECKOUT]} +checkin_document = {'text': _('check in document'), 'view': 'checkin_document', 'args': 'object.pk', 'famfam': 'basket_remove', 'condition': is_checked_out}#, 'permissions': [PERMISSION_DOCUMENT_CHECKIN]} checkout_info = {'text': _('check in/out'), 'view': 'checkout_info', 'args': 'object.pk', 'famfam': 'basket', 'children_views': ['checkout_document', 'checkin_document']}#, 'permissions': [PERMISSION_DOCUMENT_CHECKIN]} From 6f108c32f5bc86d3aaa9cfe3ab691275520ef8d5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Jun 2012 22:43:28 -0400 Subject: [PATCH 08/39] Tweak model options --- apps/checkouts/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/checkouts/models.py b/apps/checkouts/models.py index bfba0d8853..31a49566ff 100644 --- a/apps/checkouts/models.py +++ b/apps/checkouts/models.py @@ -19,8 +19,8 @@ class DocumentCheckout(models.Model): Model to store the state and information of a document checkout """ document = models.ForeignKey(Document, verbose_name=_(u'document'), unique=True) - checkout_datetime = models.DateTimeField(verbose_name=_(u'checkout date and time'), editable=False, default=datetime.datetime.now()) - expiration_datetime = models.DateTimeField(verbose_name=_(u'checkout expiration date and time'), default=datetime.datetime.now()) + checkout_datetime = models.DateTimeField(verbose_name=_(u'checkout date and time'), default=datetime.datetime.now()) + expiration_datetime = models.DateTimeField(verbose_name=_(u'checkout expiration date and time')) block_new_version = models.BooleanField(verbose_name=_(u'block new version upload'), help_text=_(u'Do not allow new version of this document to be uploaded.')) #block_metadata #block_editing From fae74e8791ce2ee26b3ca6f9cca322f00b1515a1 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Jun 2012 22:44:25 -0400 Subject: [PATCH 09/39] Update view to new form definition --- apps/checkouts/views.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/checkouts/views.py b/apps/checkouts/views.py index 0f2191affe..ae09c31ff5 100644 --- a/apps/checkouts/views.py +++ b/apps/checkouts/views.py @@ -6,7 +6,6 @@ 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.html import mark_safe from django.conf import settings from documents.views import document_list @@ -50,23 +49,21 @@ def checkout_document(request, document_pk): Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_CHECKOUT]) except PermissionDenied: AccessEntry.objects.check_access(PERMISSION_DOCUMENT_CHECKOUT, request.user, document) - + if request.method == 'POST': - form = DocumentCheckoutForm(data=request.POST) + form = DocumentCheckoutForm(data=request.POST, initial={'document': document}) if form.is_valid(): try: - document_checkout = form.save(commit=False) - document_checkout.document = document - document_checkout.save() + document_checkout = form.save() except DocumentAlreadyCheckedOut: messages.error(request, _(u'Document already checked out.')) except Exception, exc: messages.error(request, _(u'Error trying to check out document; %s') % exc) else: messages.success(request, _(u'Document "%s" checked out successfully.') % document) - return HttpResponseRedirect(document_checkout.get_absolute_url()) + return HttpResponseRedirect(reverse('checkout_info', args=[document.pk])) else: - form = DocumentCheckoutForm() + form = DocumentCheckoutForm(initial={'document': document}) return render_to_response('generic_form.html', { 'form': form, From 897fc031ec0d64ef4b77765864afe6a8037b2243 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 13 Jun 2012 03:36:13 -0400 Subject: [PATCH 10/39] Add improved migrations --- apps/checkouts/migrations/0001_initial.py | 8 ++--- ..._user_content_type__add_field_document.py} | 30 +++++++++++++++---- 2 files changed, 28 insertions(+), 10 deletions(-) rename apps/checkouts/migrations/{0002_auto__add_unique_documentcheckout_document.py => 0002_auto__add_field_documentcheckout_user_content_type__add_field_document.py} (83%) diff --git a/apps/checkouts/migrations/0001_initial.py b/apps/checkouts/migrations/0001_initial.py index a4ff9d11a2..85ee9306e2 100644 --- a/apps/checkouts/migrations/0001_initial.py +++ b/apps/checkouts/migrations/0001_initial.py @@ -11,8 +11,8 @@ class Migration(SchemaMigration): # Adding model 'DocumentCheckout' db.create_table('checkouts_documentcheckout', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('document', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.Document'])), - ('checkout_datetime', self.gf('django.db.models.fields.DateTimeField')()), + ('document', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.Document'], unique=True)), + ('checkout_datetime', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2012, 6, 13, 0, 0))), ('expiration_datetime', self.gf('django.db.models.fields.DateTimeField')()), ('block_new_version', self.gf('django.db.models.fields.BooleanField')(default=False)), )) @@ -57,8 +57,8 @@ class Migration(SchemaMigration): 'checkouts.documentcheckout': { 'Meta': {'object_name': 'DocumentCheckout'}, 'block_new_version': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'checkout_datetime': ('django.db.models.fields.DateTimeField', [], {}), - 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}), + 'checkout_datetime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 6, 13, 0, 0)'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']", 'unique': 'True'}), 'expiration_datetime': ('django.db.models.fields.DateTimeField', [], {}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) }, diff --git a/apps/checkouts/migrations/0002_auto__add_unique_documentcheckout_document.py b/apps/checkouts/migrations/0002_auto__add_field_documentcheckout_user_content_type__add_field_document.py similarity index 83% rename from apps/checkouts/migrations/0002_auto__add_unique_documentcheckout_document.py rename to apps/checkouts/migrations/0002_auto__add_field_documentcheckout_user_content_type__add_field_document.py index bbdb79b350..be5474450e 100644 --- a/apps/checkouts/migrations/0002_auto__add_unique_documentcheckout_document.py +++ b/apps/checkouts/migrations/0002_auto__add_field_documentcheckout_user_content_type__add_field_document.py @@ -8,14 +8,30 @@ from django.db import models class Migration(SchemaMigration): def forwards(self, orm): - # Adding unique constraint on 'DocumentCheckout', fields ['document'] - db.create_unique('checkouts_documentcheckout', ['document_id']) + # Adding field 'DocumentCheckout.user_content_type' + db.add_column('checkouts_documentcheckout', 'user_content_type', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'], null=True, blank=True), + keep_default=False) + # Adding field 'DocumentCheckout.user_object_id' + db.add_column('checkouts_documentcheckout', 'user_object_id', + self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True), + keep_default=False) + + + # Changing field 'DocumentCheckout.checkout_datetime' + db.alter_column('checkouts_documentcheckout', 'checkout_datetime', self.gf('django.db.models.fields.DateTimeField')(null=True)) def backwards(self, orm): - # Removing unique constraint on 'DocumentCheckout', fields ['document'] - db.delete_unique('checkouts_documentcheckout', ['document_id']) + # Deleting field 'DocumentCheckout.user_content_type' + db.delete_column('checkouts_documentcheckout', 'user_content_type_id') + # Deleting field 'DocumentCheckout.user_object_id' + db.delete_column('checkouts_documentcheckout', 'user_object_id') + + + # Changing field 'DocumentCheckout.checkout_datetime' + db.alter_column('checkouts_documentcheckout', 'checkout_datetime', self.gf('django.db.models.fields.DateTimeField')()) models = { 'auth.group': { @@ -50,10 +66,12 @@ class Migration(SchemaMigration): 'checkouts.documentcheckout': { 'Meta': {'object_name': 'DocumentCheckout'}, 'block_new_version': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'checkout_datetime': ('django.db.models.fields.DateTimeField', [], {}), + 'checkout_datetime': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']", 'unique': 'True'}), 'expiration_datetime': ('django.db.models.fields.DateTimeField', [], {}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'user_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) }, 'comments.comment': { 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"}, From 41b654695da6be9ab8d19bf168e8a508e06d1510 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 13 Jun 2012 03:36:30 -0400 Subject: [PATCH 11/39] Finish checkout form, custom fields and custom widgets --- apps/checkouts/forms.py | 87 ++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/apps/checkouts/forms.py b/apps/checkouts/forms.py index 428ec7618b..8f8aad89cb 100644 --- a/apps/checkouts/forms.py +++ b/apps/checkouts/forms.py @@ -4,85 +4,100 @@ import datetime from django import forms from django.utils.translation import ugettext_lazy as _ +from django.core import validators from .models import DocumentCheckout -class SplitDateTimeWidget(forms.widgets.MultiWidget): +class SplitDeltaWidget(forms.widgets.MultiWidget): """ - A Widget that splits datetime input into two boxes. + A Widget that splits a timedelta input into three boxes. """ - date_format = forms.widgets.DateInput.format - time_format = forms.widgets.TimeInput.format - - def __init__(self, attrs=None, date_format=None, time_format=None): - widgets = (forms.widgets.DateInput(attrs=attrs, format=date_format), - forms.widgets.TimeInput(attrs=attrs, format=time_format)) - super(SplitDateTimeWidget, self).__init__(widgets, attrs) + def __init__(self, attrs=None): + widgets = ( + forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 5em;', 'placeholder': _(u'Days')}), + forms.widgets.TextInput(attrs={'maxlength': 4, 'style':'width: 5em;', 'placeholder': _(u'Hours')}), + forms.widgets.TextInput(attrs={'maxlength': 5, 'style':'width: 5em;', 'placeholder': _(u'Minutes')}), + ) + super(SplitDeltaWidget, self).__init__(widgets, attrs) def decompress(self, value): if value: - return [value.date(), value.time().replace(microsecond=0)] - return [None, None] + return [value.days, value.seconds / 3600, (value.seconds / 60) % 60] + return [None, None, None] -class SplitHiddenDateTimeWidget(forms.widgets.SplitDateTimeWidget): + def value_from_datadict(self, data, files, name): + return [data.get('expiration_datetime_0', 0) or 0, data.get('expiration_datetime_1', 0) or 0, data.get('expiration_datetime_2', 0) or 0] + + +class SplitHiddenDeltaWidget(forms.widgets.SplitDateTimeWidget): """ - A Widget that splits datetime input into two inputs. + A Widget that splits a timedelta input into three inputs. """ is_hidden = True - def __init__(self, attrs=None, date_format=None, time_format=None): - super(SplitHiddenDateTimeWidget, self).__init__(attrs, date_format, time_format) + def __init__(self, attrs=None): + super(SplitHiddenDeltaWidget, self).__init__(attrs, date_format, time_format) for widget in self.widgets: widget.input_type = 'hidden' widget.is_hidden = True class SplitTimeDeltaField(forms.MultiValueField): - widget = SplitDateTimeWidget - hidden_widget = SplitHiddenDateTimeWidget + widget = SplitDeltaWidget + hidden_widget = SplitHiddenDeltaWidget default_error_messages = { - 'invalid_date': _(u'Enter a valid date.'), - 'invalid_time': _(u'Enter a valid time.'), + 'invalid_days': _(u'Enter a valid number of days.'), + 'invalid_hours': _(u'Enter a valid number of hours.'), + 'invalid_minutes': _(u'Enter a valid number of minutes.'), } - def __init__(self, input_date_formats=None, input_time_formats=None, *args, **kwargs): + def __init__(self, *args, **kwargs): errors = self.default_error_messages.copy() if 'error_messages' in kwargs: errors.update(kwargs['error_messages']) localize = kwargs.get('localize', False) fields = ( - forms.DateField(input_formats=input_date_formats, - error_messages={'invalid': errors['invalid_date']}, - localize=localize), - forms.TimeField(input_formats=input_time_formats, - error_messages={'invalid': errors['invalid_time']}, - localize=localize), + forms.IntegerField(min_value=0, + error_messages={'invalid': errors['invalid_days']}, + localize=localize + ), + forms.IntegerField(min_value=0, + error_messages={'invalid': errors['invalid_hours']}, + localize=localize + ), + forms.IntegerField(min_value=0, + error_messages={'invalid': errors['invalid_minutes']}, + localize=localize + ), ) super(SplitTimeDeltaField, self).__init__(fields, *args, **kwargs) + self.help_text = _(u'Amount of time to hold the document in the checked out state in days, hours and/or minutes.') + self.label = _('Check out expiration date and time') def compress(self, data_list): if data_list: # Raise a validation error if time or date is empty # (possible if SplitDateTimeField has required=False). if data_list[0] in validators.EMPTY_VALUES: - raise ValidationError(self.error_messages['invalid_date']) + raise ValidationError(self.error_messages['invalid_days']) if data_list[1] in validators.EMPTY_VALUES: - raise ValidationError(self.error_messages['invalid_time']) - return datetime.datetime.combine(*data_list) + raise ValidationError(self.error_messages['invalid_hours']) + if data_list[2] in validators.EMPTY_VALUES: + raise ValidationError(self.error_messages['invalid_minutes']) + + timedelta = datetime.timedelta(days=data_list[0], hours=data_list[1], minutes=data_list[2]) + return datetime.datetime.now() + timedelta return None class DocumentCheckoutForm(forms.ModelForm): - days = forms.IntegerField(min_value=0, label=_(u'Days'), help_text=_(u'Amount of time to hold the document checked out in days.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 5em;'})) - hours = forms.IntegerField(min_value=0, label=_(u'Hours'), help_text=_(u'Amount of time to hold the document checked out in hours.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 4, 'style':'width: 5em;'})) - minutes = forms.IntegerField(min_value=0, label=_(u'Minutes'), help_text=_(u'Amount of time to hold the document checked out in minutes.'), required=False, widget=forms.widgets.TextInput(attrs={'maxlength': 5, 'style':'width: 5em;'})) - expiration_datetime = SplitTimeDeltaField() + class Meta: model = DocumentCheckout + exclude = ('checkout_datetime', 'user_content_type', 'user_object_id', 'block_new_version') + widgets = { 'document': forms.widgets.HiddenInput(), - } - - + } From 35322b194c1dbea5c4105eb522e559751783fbfe Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 13 Jun 2012 03:37:27 -0400 Subject: [PATCH 12/39] Add permission_checkin_override to document class permissions --- apps/checkouts/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/checkouts/__init__.py b/apps/checkouts/__init__.py index 99fdc80c60..701f1f1914 100644 --- a/apps/checkouts/__init__.py +++ b/apps/checkouts/__init__.py @@ -9,7 +9,7 @@ from documents.models import Document from documents.permissions import PERMISSION_DOCUMENT_VIEW from acls.api import class_permissions -from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN) +from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE) from .links import checkout_list, checkout_document, checkout_info, checkin_document from .models import DocumentCheckout @@ -17,18 +17,17 @@ from .models import DocumentCheckout def initialize_document_checkout_extra_methods(): Document.add_to_class('is_checked_out', lambda document: DocumentCheckout.objects.is_document_checked_out(document)) Document.add_to_class('check_in', lambda document: DocumentCheckout.objects.check_in_document(document)) + Document.add_to_class('checkout_info', lambda document: DocumentCheckout.objects.document_checkout_info(document)) + Document.add_to_class('checkout_state', lambda document: DocumentCheckout.objects.document_checkout_state(document)) -#register_multi_item_links(['folder_view'], [folder_document_multiple_remove]) -#register_links(Folder, [folder_view, folder_edit, folder_delete, folder_acl_list]) -#register_links([Folder, 'folder_list', 'folder_create'], [folder_list, folder_create], menu_name='secondary_menu') -register_top_menu(name='checkouts', link=checkout_list)#, children_views=['folder_list', 'folder_create', 'folder_edit', 'folder_delete', 'folder_view', 'folder_document_multiple_remove']) +register_top_menu(name='checkouts', link=checkout_list) register_links(Document, [checkout_info], menu_name='form_header') -#register_sidebar_template(['folder_list'], 'folders_help.html') register_links(['checkout_info', 'checkout_document', 'checkin_document'], [checkout_document, checkin_document], menu_name="sidebar") class_permissions(Document, [ PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN, + PERMISSION_DOCUMENT_CHECKIN_OVERRIDE ]) initialize_document_checkout_extra_methods() @@ -37,3 +36,4 @@ initialize_document_checkout_extra_methods() #TODO: default checkout time #TODO: forcefull check in #TODO: specify checkout option check (document.allows_new_versions()) +#TODO: out check in after expiration datetime From 4234285923c0345351ed089b171031bb20188d80 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 13 Jun 2012 03:38:02 -0400 Subject: [PATCH 13/39] Apply proper permissions to link definitions --- apps/checkouts/links.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/checkouts/links.py b/apps/checkouts/links.py index 8e7c2049ae..94b3e69a4b 100644 --- a/apps/checkouts/links.py +++ b/apps/checkouts/links.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from documents.permissions import PERMISSION_DOCUMENT_VIEW -from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN) +from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE) def is_checked_out(context): @@ -16,6 +16,6 @@ def is_not_checked_out(context): checkout_list = {'text': _(u'check ins/outs'), 'view': 'checkout_list', 'famfam': 'basket'} -checkout_document = {'text': _('check out document'), 'view': 'checkout_document', 'args': 'object.pk', 'famfam': 'basket_put', 'condition': is_not_checked_out}#, 'permissions': [PERMISSION_DOCUMENT_CHECKOUT]} -checkin_document = {'text': _('check in document'), 'view': 'checkin_document', 'args': 'object.pk', 'famfam': 'basket_remove', 'condition': is_checked_out}#, 'permissions': [PERMISSION_DOCUMENT_CHECKIN]} -checkout_info = {'text': _('check in/out'), 'view': 'checkout_info', 'args': 'object.pk', 'famfam': 'basket', 'children_views': ['checkout_document', 'checkin_document']}#, 'permissions': [PERMISSION_DOCUMENT_CHECKIN]} +checkout_document = {'text': _('check out document'), 'view': 'checkout_document', 'args': 'object.pk', 'famfam': 'basket_put', 'condition': is_not_checked_out, 'permissions': [PERMISSION_DOCUMENT_CHECKOUT]} +checkin_document = {'text': _('check in document'), 'view': 'checkin_document', 'args': 'object.pk', 'famfam': 'basket_remove', 'condition': is_checked_out, 'permissions': [PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE]} +checkout_info = {'text': _('check in/out'), 'view': 'checkout_info', 'args': 'object.pk', 'famfam': 'basket', 'children_views': ['checkout_document', 'checkin_document'], 'permissions': [PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE, PERMISSION_DOCUMENT_CHECKOUT]} From 79c4a472513bdd706bfdfaacce159d112cc94b55 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 13 Jun 2012 03:38:20 -0400 Subject: [PATCH 14/39] Add literals for checkout state and state icons --- apps/checkouts/literals.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/checkouts/literals.py b/apps/checkouts/literals.py index c3961685ab..23e9920984 100644 --- a/apps/checkouts/literals.py +++ b/apps/checkouts/literals.py @@ -1 +1,16 @@ from __future__ import absolute_import + +from django.utils.translation import ugettext_lazy as _ + +STATE_CHECKED_OUT = 'checkedout' +STATE_CHECKED_IN = 'checkedin' + +STATE_ICONS = { + STATE_CHECKED_OUT: 'basket_put.png', + STATE_CHECKED_IN: 'traffic_lights_green.png', +} + +STATE_LABELS = { + STATE_CHECKED_OUT: _(u'checked out'), + STATE_CHECKED_IN: _(u'checked in/available'), +} From df8be16e9d2e79c9e62d4a5e17a335d5e5164cc5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 13 Jun 2012 03:38:45 -0400 Subject: [PATCH 15/39] Add document_checkout_info and document_checkout_state manager methods --- apps/checkouts/managers.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/checkouts/managers.py b/apps/checkouts/managers.py index 8fc1f69276..530460e32d 100644 --- a/apps/checkouts/managers.py +++ b/apps/checkouts/managers.py @@ -5,10 +5,13 @@ from django.db import models from documents.models import Document from .exceptions import DocumentNotCheckedOut +from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN class DocumentCheckoutManager(models.Manager): - def checked_out(self): + #TODO: 'check_expiration' method + + def checked_out_documents(self): return Document.objects.filter(pk__in=self.model.objects.all().values_list('pk', flat=True)) def is_document_checked_out(self, document): @@ -24,3 +27,14 @@ class DocumentCheckoutManager(models.Manager): raise DocumentNotCheckedOut else: document_checkout.delete() + + def document_checkout_info(self, document): + return self.model.objects.get(document=document) + + def document_checkout_state(self, document): + if self.is_document_checked_out(document): + return STATE_CHECKED_OUT + else: + return STATE_CHECKED_IN + + From fb2572820213a28f225c7c577ba4b8af5c3009b2 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 13 Jun 2012 03:39:12 -0400 Subject: [PATCH 16/39] Add user object field to checkout model --- apps/checkouts/models.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/checkouts/models.py b/apps/checkouts/models.py index 31a49566ff..198cf78277 100644 --- a/apps/checkouts/models.py +++ b/apps/checkouts/models.py @@ -5,6 +5,8 @@ import datetime from django.db import models, IntegrityError from django.utils.translation import ugettext_lazy as _ +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic from documents.models import Document @@ -19,9 +21,13 @@ class DocumentCheckout(models.Model): Model to store the state and information of a document checkout """ document = models.ForeignKey(Document, verbose_name=_(u'document'), unique=True) - checkout_datetime = models.DateTimeField(verbose_name=_(u'checkout date and time'), default=datetime.datetime.now()) - expiration_datetime = models.DateTimeField(verbose_name=_(u'checkout expiration date and time')) + checkout_datetime = models.DateTimeField(verbose_name=_(u'check out date and time'), blank=True, null=True) + expiration_datetime = models.DateTimeField(verbose_name=_(u'check out expiration date and time'), help_text=_(u'Amount of time to hold the document checked out in minutes.')) block_new_version = models.BooleanField(verbose_name=_(u'block new version upload'), help_text=_(u'Do not allow new version of this document to be uploaded.')) + user_content_type = models.ForeignKey(ContentType, null=True, blank=True) # blank and null added for ease of db migration + user_object_id = models.PositiveIntegerField(null=True, blank=True) + user_object = generic.GenericForeignKey(ct_field='user_content_type', fk_field='user_object_id') + #block_metadata #block_editing #block tag add/remove @@ -33,7 +39,7 @@ class DocumentCheckout(models.Model): def save(self, *args, **kwargs): if not self.pk: - self.checkout_date = datetime.datetime.now() + self.checkout_datetime = datetime.datetime.now() try: return super(DocumentCheckout, self).save(*args, **kwargs) except IntegrityError, exc: @@ -42,6 +48,8 @@ class DocumentCheckout(models.Model): #else: raise + #TODO: clean method that raises DocumentAlreadyCheckedOut + @models.permalink def get_absolute_url(self): return ('checkout_info', [self.document.pk]) From a4af59527a5daf05283b58978928014ace2c4681 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 13 Jun 2012 03:39:39 -0400 Subject: [PATCH 17/39] Finish all views (check info, check out and check in) --- apps/checkouts/views.py | 113 +++++++++++++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 18 deletions(-) diff --git a/apps/checkouts/views.py b/apps/checkouts/views.py index ae09c31ff5..d47ed3b695 100644 --- a/apps/checkouts/views.py +++ b/apps/checkouts/views.py @@ -6,22 +6,24 @@ 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.conf import settings from documents.views import document_list from documents.models import Document -from permissions.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied + from permissions.models import Permission from acls.models import AccessEntry +from common.utils import get_object_name from .models import DocumentCheckout from .permissions import PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN from .forms import DocumentCheckoutForm -from .exceptions import DocumentAlreadyCheckedOut - +from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut +from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN, STATE_ICONS, STATE_LABELS +from .widgets import checkout_widget def checkout_list(request): - return document_list(request, object_list=DocumentCheckout.objects.checked_out(), title=_(u'checked out documents')) + return document_list(request, object_list=DocumentCheckout.objects.checked_out_documents(), title=_(u'checked out documents')) def checkout_info(request, document_pk): @@ -29,15 +31,18 @@ def checkout_info(request, document_pk): try: Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN]) except PermissionDenied: - AccessEntry.objects.check_access([PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN], request.user, document) + AccessEntry.objects.check_accesses([PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN], request.user, document) + + paragraphs = [checkout_widget(document)] if document.is_checked_out(): - content = 'checkedout' - else: - content = _(u'Document has not been checked out.') + checkout_info = document.checkout_info() + paragraphs.append(_(u'User: %s') % get_object_name(checkout_info.user_object, display_object_type=False)) + paragraphs.append(_(u'Checkout time: %s') % checkout_info.checkout_datetime) + paragraphs.append(_(u'Checkout expiration: %s') % checkout_info.expiration_datetime) return render_to_response('generic_template.html', { - 'content': content, + 'paragraphs': paragraphs, 'object': document, 'title': _(u'Check out details for document: %s') % document }, context_instance=RequestContext(request)) @@ -54,7 +59,10 @@ def checkout_document(request, document_pk): form = DocumentCheckoutForm(data=request.POST, initial={'document': document}) if form.is_valid(): try: - document_checkout = form.save() + document_checkout = form.save(commit=False) + document_checkout.user_object = request.user + #document_checkout.clean() + document_checkout.save() except DocumentAlreadyCheckedOut: messages.error(request, _(u'Document already checked out.')) except Exception, exc: @@ -74,23 +82,92 @@ def checkout_document(request, document_pk): def checkin_document(request, document_pk): document = get_object_or_404(Document, pk=document_pk) + post_action_redirect = reverse('checkout_info', args=[document.pk]) + # TODO: add forcefull checkin + # TODO: check user try: Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_CHECKIN]) except PermissionDenied: AccessEntry.objects.check_access(PERMISSION_DOCUMENT_CHECKIN, request.user, document) + 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: document.check_in() - except DocumentAlreadyCheckedOut: - messages.error(request, _(u'Document already checked out.')) + except DocumentNotCheckedOut: + messages.error(request, _(u'Document has not been checked out.')) except Exception, exc: messages.error(request, _(u'Error trying to check in document; %s') % exc) else: - messages.success(request, _(u'Document "%s" checked out successfully.') % document) - return HttpResponseRedirect(reverse('checkout_info', args=[document.pk])) + messages.success(request, _(u'Document "%s" checked in successfully.') % document) + return HttpResponseRedirect(next) - return render_to_response('generic_form.html', { + context = { + 'object_name': _(u'document'), + 'delete_view': False, + 'previous': previous, + 'next': next, + 'form_icon': u'basket_remove.png', 'object': document, - 'title': _(u'Check in document: %s') % document - }, context_instance=RequestContext(request)) + 'title': _(u'Are you sure you wish to check in document: %s') % document + } + + return render_to_response('generic_confirm.html', context, + context_instance=RequestContext(request)) + + +def document_delete(request, document_id=None, document_id_list=None): + post_action_redirect = None + + if document_id: + documents = [get_object_or_404(Document, pk=document_id)] + post_action_redirect = reverse('document_list_recent') + elif document_id_list: + documents = [get_object_or_404(Document, pk=document_id) for document_id in document_id_list.split(',')] + else: + messages.error(request, _(u'Must provide at least one document.')) + return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/')) + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_DELETE]) + except PermissionDenied: + documents = AccessEntry.objects.filter_objects_by_access(PERMISSION_DOCUMENT_DELETE, request.user, documents, exception_on_empty=True) + + 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': + for document in documents: + try: + warnings = delete_indexes(document) + if request.user.is_staff or request.user.is_superuser: + for warning in warnings: + messages.warning(request, warning) + + document.delete() + #create_history(HISTORY_DOCUMENT_DELETED, data={'user': request.user, 'document': document}) + messages.success(request, _(u'Document deleted successfully.')) + except Exception, e: + messages.error(request, _(u'Document: %(document)s delete error: %(error)s') % { + 'document': document, 'error': e + }) + + return HttpResponseRedirect(next) + + context = { + 'object_name': _(u'document'), + 'delete_view': True, + 'previous': previous, + 'next': next, + 'form_icon': u'page_delete.png', + } + if len(documents) == 1: + context['object'] = documents[0] + context['title'] = _(u'Are you sure you wish to delete the document: %s?') % ', '.join([unicode(d) for d in documents]) + elif len(documents) > 1: + context['title'] = _(u'Are you sure you wish to delete the documents: %s?') % ', '.join([unicode(d) for d in documents]) + + return render_to_response('generic_confirm.html', context, + context_instance=RequestContext(request)) From 4d8c930852b67a6a775e72ab8b39964aee0d8eb5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 13 Jun 2012 03:40:01 -0400 Subject: [PATCH 18/39] Add checkouts app static media --- .../checkouts/static/images/icons/basket_put.png | Bin 0 -> 1903 bytes .../static/images/icons/basket_remove.png | Bin 0 -> 1888 bytes .../static/images/icons/traffic_lights_green.png | Bin 0 -> 1673 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/checkouts/static/images/icons/basket_put.png create mode 100644 apps/checkouts/static/images/icons/basket_remove.png create mode 100644 apps/checkouts/static/images/icons/traffic_lights_green.png diff --git a/apps/checkouts/static/images/icons/basket_put.png b/apps/checkouts/static/images/icons/basket_put.png new file mode 100644 index 0000000000000000000000000000000000000000..66e7fcec1a961af45f6f03fd091161f931ce730e GIT binary patch literal 1903 zcmV-#2ax!QP)w zF#!Sc6N5g_hzPLiy(_fZ1TGEee=Hed-wZBFbo6#=bvzn{8H&EY|wx* zo%J?ON)d@9VDpxxm`*!d+5*o}feZ$JWA+lL8l#K$whmbmFu&^6GUg!)|5pmQlR6=I zC!%im!w_u$aTH50e+T@IdCo-vHbH)9(_y2k-Ao4gtRW+=h}HqYMTc$TXWp|za<;I_ zjK!Gz%nEorLUBm+w64vFpG^st9?}k)b{Z~=_?h?8NGQ3AFc^z58Jk1k?2ITBLtx3e z-Kgm>;^m{*Ca434?tBe#o!qM>fjI5<=?5UlLrvv9EdkuN5?zvmJVPLx7GsEDY;UN!c!VEM z1omRX(ZO)&pFe{|pKLBt^Ng%%$;8DJkk^>7@3Vl>swIWYbwDt-SO=zEZ+PvHvUG|iR3rIVVf>k&%DR) zn1^}!44(~c(8ZjF)C#jL(+b zTRm#*xQYZEFdQ`yM^-285Rd{b2Y`e1g}BNw4@n_P1(Bfg z)F32Pa$^!5ZHKCFedYa%7YO`tNX604!(XYZN@uYGexeCju<`YrF%xbY<9A~DtVy^* zIRu5;gu&7U%T$rp5`0))TY+TmMohlQQlRNKz;Wa*EG(`jcSXqWn$;$$vIWx8MeDEK z<4_rFL3=oX&b9+zVD+5~h^bYun^88=xUo-f+J4+WHfMaMj2CB3hE%r}2~j2`#FI); z_Q|3TpR_-Ly?x)A3KV8ti#x`=3{@v^Bas5GL()}f)8=4Buo(x*^7A5Xy)Dm`-9k)< zh)xVtlB@FhXVzanDZgxuaWyu#D#*`qqjL6S^sIf8(pVy^L^II@+Y=7zKn_@?kR*2} z^|)!d0S4ye(&?>+;d36t^7YNwe?r0S<6OAc_yK$N?ccoY*%xNg(fB|smPOfJR&;X- ziVYQWK22hKhl*v@`>_1($55PTN1WhvL9`P`u>V{78!+tuRx1?Aa*lhga}1W(wBSIW ziknAESToHFRVYDY>$_!kg$*j;GHATT5;{Jw{~q%)1@!a`$4|Z4c(SSq&r>)YUsh9c zr8hbplziHa6Z#jruBmGr*W)@LZMn$ zhP!z9@L|?`6^8bD11T4fG_E7-^~hH`gzk zP?6r_cxu_QD`>etH3wou$B7}9pO=@RX__4sJIn2R<3C+Xl7wA5d%76Q#vHs;F=9+n zVtbP`Q(Z0@P8yWWP5VreWv5Ahcg?a`Cd-aS`x^)=I&g1Htt%}Yjx?xBY--N1VTktk zL!~}0QtivKbw|;3%QNS))~P2rX#9D+UPSs%fG~E`tHkNXiQ6}|F+&_C@Ly4f9vbhTwhm2mlzF6x-9xgf#y&Mot>ROrYaz&D4+&|!5t@h zd*StXkU`nXmN?0kIwd8O)9ExR?X?*S*_6dHS#}^0*pYf|9})s0B^HY*2~~CbeLmC# zgAg4eN=r&jhOJw-n!hC_C26g zQ_>`%shZh0m=g5N#P>a>Gpz^s`7Fy|SuB%`Yp~sKoawtj>+kPx?C$QGnm6$xl+T=D zmJ%lt51yG7Gpw#}xB(7O$*19o?Je^^yjRe$Ptqi~v8!fiR%yeq=IaR~oH7v>SzV_#ImsX1^u;ST%z p&TT*c8-JW6irs#bsW1KzU;wA)g7CcsM@9eu002ovPDHLkV1m-qgWmuE literal 0 HcmV?d00001 diff --git a/apps/checkouts/static/images/icons/basket_remove.png b/apps/checkouts/static/images/icons/basket_remove.png new file mode 100644 index 0000000000000000000000000000000000000000..0d8c5a36a34a664bc96cf77d591731057785eb2e GIT binary patch literal 1888 zcmV-m2cP(fP)551Tg@9seRE$U~ z21;t}i#{lpZnwL$vomu&XLh%2waEIO=5+79bME=Rd(OG%-jRe5_&*=%FV3^wx)DA@ z!X4bP2<+JN7)q(B{THOIH&vke>5m{fy5BK{+dRha{(bOrR6n&1qP6Ee@#b!r!fhVo zcYl_Ig+yMxvqMz2o5>)b2fS*|G!(Bad=Isa%P9~rh1)#F?^x$NBxJ^7Ol~s)cV{31 z8@0BziO4xfSm#%uYVHgaub8(PyN@o1WGD7=Ot{Ts{El^4_Z%eTO2S|)#$;?JVDAbl zkZC24`@TnY=kG|^iZ)>9O|wwE{Kgk?pn26WXu<;tGW?EpSeNI-HHN|cWV>wZVK`{C zn=l!h37~6GHCr>sr4xw;e0*{Qi8M(@(ZQp@>bKV7-8x`aZW#m_NfsXCcdWy@Jcs8d zz;}?%vTa9j#aNH&?URIw1jDW%g@7DpZ*Qo+EcdFCwZ0$lakq%vIaYFTp42>ytLQhI z#8J2bwIR_la&7eRn2pBvtu=vp0jCo=#i=QX(j)02pu8xE1OAAVwfR|(n%0785Ytob(Cwc8CFP(<#l~yR^XTi%O1c}Hv zt?9V^Ee9T0`Cj3AIEJMjAE2zIWHK+yvbU;+XX5pj?czi{TF!hZtogHd#~{qz}L zy}gXRCB^d~lO%WYlRNjG_D;^aI9nx zWNAjmY%G|t9;y+h0iARV*bEigyro$0Z^F;C@-e}-{u9qGyqTu7lAKAL1&)g6p5A`- zw7i9Hi)*o~ML}Mc6BSFQqi^dQP>G{KBrNo6wK&KZGOe0OiL)n)ATXUYbt6K#pwanY zP{=goEWID)+ndmQM!{{l4lEO20>|9tdzX*jw#nRn6_I9dR zS9KiacioS|Xgj5~H5Lv?=HF)26ZmOB#r%mjygkniRVqSb%jSjg zZ9YuEAq;_V)WE*_uW(n-Rpu%Tp#S$al>X~aVRHq8;F6@f;ms>q-? zODPeMC_^92j9^VZ2gF86nF*JOl|^&)(%BFu2pEAtV80{HS(uZPZEhd7B#nqK0FnAN zCwZN%4u=f8-G-)P$4!xC zyD5LQ&$iem+YW_>8u;u=+#i$ol{PpSY*3Z(?5yl;gocKoQXRKY!I9c6Zv6$%Op z%-{O@6#H`upy?WP-GGM(Gz9|a>gxI?nSdpUfa>@A51r}nhuh^s8f7c5#7?d>C@ER& zcDpGl_svkqrY*L~wtYU|q2%lPh!Bt{;c!@qs;blL@u1r8hsA2af}#bcLTzoW`CC*} zlv2N@wgv{RzNEMqWD~()uqS?5(MIzCr>dqXN@HMP0N!*jC5;YE)y%rV5~Vf^?Iu&s z)lca2T(-fs*d{HmA?|)Lng>iT3=Iu6_V)J7o_gseC|Oi&ZY546K7bZ2E{;_#Jys|F zsLuxz&*v@Z_P9YdDFFvZB;aTSqHS$$dwY6%R@ex5`m}uBHV6(W2!%r_f5-9iTxl*! zJSSW(H^QM1$R+bze`MIB^RpmlAz@3{{Gk@6;)XMC}%_3fSA*`%z_O zW&LlzAEi;w5-!W*&ATF9l>f#D?1+Vt^pSLO`AyD&(Twx0!x@)<>SL3WM2VMQJ^972 a0t^5F@qJm!;0?I|0000}2tKGmAxgm_g<@Kz#6(jQGrig? zvB}^~IeX^&!=jSTb?mIGnuCBtaox9P}+=`3@lF4My-u@^)`0yi?m6c_EQBHpK zrB6&!&bscLz&3Cvd-gsnMg}z5>5pg6U}YtZpSru@mH@`b$KdyRQBzls%F1eGluD)0 zd+8Fo&!0zga|@Em6x!O_R3Ojn-3L=LqiHVudiAnegC^M&=catqri$LNv5^jy5tV8e z3<<{T_2G780i_#D(cIdqoI1Pk`FtpAe+(nxFd~s#D1T(LvY{;lrY;A4=dPI>K;pAc zKRLSp`GY4mmXvhBuD6jf)b1k-5j^$uQ*e7cs@|&y^|o%^itB@esHmt?HvBd@F~Ndk zITq_Z`@`v@Tw{%0P5^O{DN*d2pP%oI$K%S0X=YTzRc)!jMyaVscI@-}*9gCHu4+rA z(zR?{pbz?@Px`jkC>Q{{^_Ix^g$vzXu~_sMHcrMcRSg@0jVj%KiY&TD7?cQgsB771 zpbz?@Puq9;UIB1qvO%2hKCS|gX3}I=b+t9{2Lg95?ug7$hq}r}Z0Tdw;+eZ}j{L(3 zB0wjsw_D)19E}OQLb!4BmW^8n)YhM>%1r{-A*E}m&Q}U^g%-b808S8!6ATgE?G_X) zAsik?Fc_4&2Ne7`yaLN)BCh5lqC;K7AtdopXz@Y;cwatzB&l(GvI8~|JDMcq7RQT= zOE`D#9HytIvPfXF)z;Rbp&=;2c#uw~l?}(?Bd^X91^oB*sgu6d98f5L!1nDsdZeK` z>G;Z(%L>Q3+oIl~p})Z3f{h^sP1U(l@5sV}%>9YScRZmMbAsEsbC>#^OeTAPlYwFa zFy4Fjonr@IeCgwoKuL#1L?%44`|0Uvm4*8HhC7^ZpCM(XrI?)j7gLjyl8qWQN87={ zYly{Ty{EtX_85KVE|w1kiVMV`Yi4Ff!H~{$-HjOuAQTEIxcOE=(fL2;sH5wiEdSF7 zedXhTLIDt`3xv+ao(snGe93U3>Wg9itEDALV*RT zppXm~!m1N=&9HS8bj|Ep@nA4iB@k9~3A@!xr^f|)JaRRxy*se+cvM%Em*d9#g0iuB z;J#sqe3u8Nb47O5+|-20scFSR&i%#OsEPWzT1-eII%O+ntQ!EKXTXACbUBK-xj99* z>9k2`WshUGTT^q?p)PHl8`J#;poqxYp~dO4afJfzEE-MVkL#B(Havz*%F3+n)e&k5 zp{XSZkH;$|GL#LIKq9iT@jr=ZV4z=dJxPat2Kok2))K{m_vLC+VVOtgOV00000NkvXXu0mjfrRXrd literal 0 HcmV?d00001 From 2a7588deac8c000078fc422ed6199ee48572ccad Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 13 Jun 2012 03:40:22 -0400 Subject: [PATCH 19/39] Move checkout status widget code to a separate file --- apps/checkouts/widgets.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 apps/checkouts/widgets.py diff --git a/apps/checkouts/widgets.py b/apps/checkouts/widgets.py new file mode 100644 index 0000000000..e1a0d38302 --- /dev/null +++ b/apps/checkouts/widgets.py @@ -0,0 +1,17 @@ +from __future__ import absolute_import + +from django.utils.translation import ugettext_lazy as _ +from django.utils.safestring import mark_safe +from django.conf import settings + +from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN, STATE_ICONS, STATE_LABELS + + +def checkout_widget(document): + checkout_state = document.checkout_state() + + widget = (u'' % (settings.STATIC_URL, STATE_ICONS[checkout_state])) + return _(u'Document status: %(widget)s %(text)s') % { + 'widget': mark_safe(widget), + 'text': STATE_LABELS[checkout_state] + } From 6ddbc5156c93e970f59b4e38eaef373d7882fee8 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 15 Jun 2012 17:45:55 -0400 Subject: [PATCH 20/39] Return the primary key of document not of checkout entry --- apps/checkouts/managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/checkouts/managers.py b/apps/checkouts/managers.py index 530460e32d..75e323150c 100644 --- a/apps/checkouts/managers.py +++ b/apps/checkouts/managers.py @@ -12,7 +12,7 @@ class DocumentCheckoutManager(models.Manager): #TODO: 'check_expiration' method def checked_out_documents(self): - return Document.objects.filter(pk__in=self.model.objects.all().values_list('pk', flat=True)) + return Document.objects.filter(pk__in=self.model.objects.all().values_list('document__pk', flat=True)) def is_document_checked_out(self, document): if self.model.objects.filter(document=document): From 30842d5d9b2dd3bb560ecf962cc319394b78247a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 15 Jun 2012 19:27:41 -0400 Subject: [PATCH 21/39] Rename main menu checkout link text --- apps/checkouts/links.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/checkouts/links.py b/apps/checkouts/links.py index 94b3e69a4b..5583dfe5b3 100644 --- a/apps/checkouts/links.py +++ b/apps/checkouts/links.py @@ -15,7 +15,7 @@ def is_not_checked_out(context): return not context['object'].is_checked_out() -checkout_list = {'text': _(u'check ins/outs'), 'view': 'checkout_list', 'famfam': 'basket'} +checkout_list = {'text': _(u'checkouts'), 'view': 'checkout_list', 'famfam': 'basket'} checkout_document = {'text': _('check out document'), 'view': 'checkout_document', 'args': 'object.pk', 'famfam': 'basket_put', 'condition': is_not_checked_out, 'permissions': [PERMISSION_DOCUMENT_CHECKOUT]} checkin_document = {'text': _('check in document'), 'view': 'checkin_document', 'args': 'object.pk', 'famfam': 'basket_remove', 'condition': is_checked_out, 'permissions': [PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE]} checkout_info = {'text': _('check in/out'), 'view': 'checkout_info', 'args': 'object.pk', 'famfam': 'basket', 'children_views': ['checkout_document', 'checkin_document'], 'permissions': [PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE, PERMISSION_DOCUMENT_CHECKOUT]} From 8fdd3610716f3444f53894ea6608d47c766554f3 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 15 Jun 2012 19:28:22 -0400 Subject: [PATCH 22/39] Add NewDocumentVersionNotAllowed exception to documents app --- apps/documents/exceptions.py | 6 ++++++ apps/documents/models.py | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 apps/documents/exceptions.py diff --git a/apps/documents/exceptions.py b/apps/documents/exceptions.py new file mode 100644 index 0000000000..e3ab3eec34 --- /dev/null +++ b/apps/documents/exceptions.py @@ -0,0 +1,6 @@ +class NewDocumentVersionNotAllowed(Exception): + """ + Uploading new versions for this document is not allowed + Current reasons: Document is in checked out state + """ + pass diff --git a/apps/documents/models.py b/apps/documents/models.py index c820780c3b..8185bb8297 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -37,6 +37,7 @@ from .managers import DocumentPageTransformationManager from .utils import document_save_to_temp_dir from .literals import (RELEASE_LEVEL_FINAL, RELEASE_LEVEL_CHOICES, VERSION_UPDATE_MAJOR, VERSION_UPDATE_MINOR, VERSION_UPDATE_MICRO) +from .exceptions import NewDocumentVersionNotAllowed # document image cache name hash function HASH_FUNCTION = lambda x: hashlib.sha256(x).hexdigest() @@ -171,6 +172,9 @@ class Document(models.Model): def new_version(self, file, comment=None, version_update=None, release_level=None, serial=None): logger.debug('creating new document version') + if not self.is_new_versions_allowed(): + raise NewDocumentVersionNotAllowed + if version_update: new_version_dict = self.latest_version.get_new_version_dict(version_update) logger.debug('new_version_dict: %s' % new_version_dict) From cd89bbd4887c13fd54ffc857980e47d9aef50e73 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 15 Jun 2012 19:29:28 -0400 Subject: [PATCH 23/39] Show document checkout information on checked out document list view --- apps/checkouts/views.py | 79 +++++++++++------------------------------ 1 file changed, 20 insertions(+), 59 deletions(-) diff --git a/apps/checkouts/views.py b/apps/checkouts/views.py index d47ed3b695..9df54dc145 100644 --- a/apps/checkouts/views.py +++ b/apps/checkouts/views.py @@ -6,14 +6,15 @@ 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.core.exceptions import PermissionDenied from documents.views import document_list from documents.models import Document -from django.core.exceptions import PermissionDenied from permissions.models import Permission from acls.models import AccessEntry from common.utils import get_object_name +from common.utils import encapsulate from .models import DocumentCheckout from .permissions import PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN @@ -22,8 +23,21 @@ from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN, STATE_ICONS, STATE_LABELS from .widgets import checkout_widget + def checkout_list(request): - return document_list(request, object_list=DocumentCheckout.objects.checked_out_documents(), title=_(u'checked out documents')) + + return document_list( + request, + object_list=DocumentCheckout.objects.checked_out_documents(), + title=_(u'checked out documents'), + extra_context={ + 'extra_columns': [ + {'name': _(u'checkout user'), 'attribute': encapsulate(lambda document: get_object_name(document.checkout_info().user_object, display_object_type=False))}, + {'name': _(u'checkout time and date'), 'attribute': encapsulate(lambda document: document.checkout_info().checkout_datetime)}, + {'name': _(u'checkout expiration'), 'attribute': encapsulate(lambda document: document.checkout_info().expiration_datetime)}, + ], + } + ) def checkout_info(request, document_pk): @@ -38,8 +52,10 @@ def checkout_info(request, document_pk): if document.is_checked_out(): checkout_info = document.checkout_info() paragraphs.append(_(u'User: %s') % get_object_name(checkout_info.user_object, display_object_type=False)) - paragraphs.append(_(u'Checkout time: %s') % checkout_info.checkout_datetime) - paragraphs.append(_(u'Checkout expiration: %s') % checkout_info.expiration_datetime) + paragraphs.append(_(u'Check out time: %s') % checkout_info.checkout_datetime) + paragraphs.append(_(u'Check out expiration: %s') % checkout_info.expiration_datetime) + paragraphs.append(_(u'Check out expiration: %s') % checkout_info.expiration_datetime) + paragraphs.append(_(u'New versions allowed: %s') % (_(u'yes') if not checkout_info.block_new_version else _(u'no'))) return render_to_response('generic_template.html', { 'paragraphs': paragraphs, @@ -116,58 +132,3 @@ def checkin_document(request, document_pk): return render_to_response('generic_confirm.html', context, context_instance=RequestContext(request)) - - -def document_delete(request, document_id=None, document_id_list=None): - post_action_redirect = None - - if document_id: - documents = [get_object_or_404(Document, pk=document_id)] - post_action_redirect = reverse('document_list_recent') - elif document_id_list: - documents = [get_object_or_404(Document, pk=document_id) for document_id in document_id_list.split(',')] - else: - messages.error(request, _(u'Must provide at least one document.')) - return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/')) - - try: - Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_DELETE]) - except PermissionDenied: - documents = AccessEntry.objects.filter_objects_by_access(PERMISSION_DOCUMENT_DELETE, request.user, documents, exception_on_empty=True) - - 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': - for document in documents: - try: - warnings = delete_indexes(document) - if request.user.is_staff or request.user.is_superuser: - for warning in warnings: - messages.warning(request, warning) - - document.delete() - #create_history(HISTORY_DOCUMENT_DELETED, data={'user': request.user, 'document': document}) - messages.success(request, _(u'Document deleted successfully.')) - except Exception, e: - messages.error(request, _(u'Document: %(document)s delete error: %(error)s') % { - 'document': document, 'error': e - }) - - return HttpResponseRedirect(next) - - context = { - 'object_name': _(u'document'), - 'delete_view': True, - 'previous': previous, - 'next': next, - 'form_icon': u'page_delete.png', - } - if len(documents) == 1: - context['object'] = documents[0] - context['title'] = _(u'Are you sure you wish to delete the document: %s?') % ', '.join([unicode(d) for d in documents]) - elif len(documents) > 1: - context['title'] = _(u'Are you sure you wish to delete the documents: %s?') % ', '.join([unicode(d) for d in documents]) - - return render_to_response('generic_confirm.html', context, - context_instance=RequestContext(request)) From 4322ac0a029ae07b98149c19491e997e6b990dbe Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 15 Jun 2012 19:30:08 -0400 Subject: [PATCH 24/39] Add support for selectively blocking new document version uploads when checking out documents --- apps/checkouts/__init__.py | 2 ++ apps/checkouts/forms.py | 2 +- apps/checkouts/managers.py | 12 ++++++++++-- apps/checkouts/models.py | 7 ++++++- apps/sources/views.py | 11 +++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/apps/checkouts/__init__.py b/apps/checkouts/__init__.py index 701f1f1914..adb360a66c 100644 --- a/apps/checkouts/__init__.py +++ b/apps/checkouts/__init__.py @@ -19,6 +19,7 @@ def initialize_document_checkout_extra_methods(): Document.add_to_class('check_in', lambda document: DocumentCheckout.objects.check_in_document(document)) Document.add_to_class('checkout_info', lambda document: DocumentCheckout.objects.document_checkout_info(document)) Document.add_to_class('checkout_state', lambda document: DocumentCheckout.objects.document_checkout_state(document)) + Document.add_to_class('is_new_versions_allowed', lambda document: DocumentCheckout.objects.is_document_new_versions_allowed(document)) register_top_menu(name='checkouts', link=checkout_list) register_links(Document, [checkout_info], menu_name='form_header') @@ -37,3 +38,4 @@ initialize_document_checkout_extra_methods() #TODO: forcefull check in #TODO: specify checkout option check (document.allows_new_versions()) #TODO: out check in after expiration datetime +#TODO: add checkin out history diff --git a/apps/checkouts/forms.py b/apps/checkouts/forms.py index 8f8aad89cb..d2032ed124 100644 --- a/apps/checkouts/forms.py +++ b/apps/checkouts/forms.py @@ -96,7 +96,7 @@ class DocumentCheckoutForm(forms.ModelForm): class Meta: model = DocumentCheckout - exclude = ('checkout_datetime', 'user_content_type', 'user_object_id', 'block_new_version') + exclude = ('checkout_datetime', 'user_content_type', 'user_object_id') widgets = { 'document': forms.widgets.HiddenInput(), diff --git a/apps/checkouts/managers.py b/apps/checkouts/managers.py index 75e323150c..68391106b4 100644 --- a/apps/checkouts/managers.py +++ b/apps/checkouts/managers.py @@ -26,10 +26,14 @@ class DocumentCheckoutManager(models.Manager): except self.model.DoesNotExist: raise DocumentNotCheckedOut else: + #create_history(HISTORY_DOCUMENT_DELETED, data={'user': request.user, 'document': document}) document_checkout.delete() def document_checkout_info(self, document): - return self.model.objects.get(document=document) + try: + return self.model.objects.get(document=document) + except self.model.DoesNotExist: + raise DocumentNotCheckedOut def document_checkout_state(self, document): if self.is_document_checked_out(document): @@ -37,4 +41,8 @@ class DocumentCheckoutManager(models.Manager): else: return STATE_CHECKED_IN - + def is_document_new_versions_allowed(self, document): + try: + return not self.document_checkout_info(document).block_new_version + except DocumentNotCheckedOut: + return True diff --git a/apps/checkouts/models.py b/apps/checkouts/models.py index 198cf78277..333703a746 100644 --- a/apps/checkouts/models.py +++ b/apps/checkouts/models.py @@ -23,11 +23,12 @@ class DocumentCheckout(models.Model): document = models.ForeignKey(Document, verbose_name=_(u'document'), unique=True) checkout_datetime = models.DateTimeField(verbose_name=_(u'check out date and time'), blank=True, null=True) expiration_datetime = models.DateTimeField(verbose_name=_(u'check out expiration date and time'), help_text=_(u'Amount of time to hold the document checked out in minutes.')) - block_new_version = models.BooleanField(verbose_name=_(u'block new version upload'), help_text=_(u'Do not allow new version of this document to be uploaded.')) user_content_type = models.ForeignKey(ContentType, null=True, blank=True) # blank and null added for ease of db migration user_object_id = models.PositiveIntegerField(null=True, blank=True) user_object = generic.GenericForeignKey(ct_field='user_content_type', fk_field='user_object_id') + block_new_version = models.BooleanField(default=True, verbose_name=_(u'block new version upload'), help_text=_(u'Do not allow new version of this document to be uploaded.')) + #block_metadata #block_editing #block tag add/remove @@ -47,6 +48,10 @@ class DocumentCheckout(models.Model): # raise DocumentAlreadyCheckedOut #else: raise + else: + #create_history(HISTORY_DOCUMENT_DELETED, data={'user': request.user, 'document': document}) + pass + #TODO: clean method that raises DocumentAlreadyCheckedOut diff --git a/apps/sources/views.py b/apps/sources/views.py index adb4df3611..c436e4daeb 100644 --- a/apps/sources/views.py +++ b/apps/sources/views.py @@ -15,6 +15,7 @@ from documents.permissions import (PERMISSION_DOCUMENT_CREATE, PERMISSION_DOCUMENT_NEW_VERSION) from documents.models import DocumentType, Document from documents.conf.settings import THUMBNAIL_SIZE +from documents.exceptions import NewDocumentVersionNotAllowed from metadata.api import decode_metadata_from_url, metadata_repr_as_list from permissions.models import Permission from common.utils import encapsulate @@ -174,6 +175,11 @@ def upload_interactive(request, source_type=None, source_id=None, document_pk=No messages.warning(request, _(u'File was not a compressed file, uploaded as it was.')) return HttpResponseRedirect(request.get_full_path()) + except NewDocumentVersionNotAllowed: + if not document.is_new_versions_allowed(): + messages.error(request, _(u'The check out options for document currently don\'t allow new version uploads.')) + else: + messages.error(request, _(u'This document currently don\'t allow new version uploads.')) except Exception, e: if settings.DEBUG: raise @@ -253,6 +259,11 @@ def upload_interactive(request, source_type=None, source_id=None, document_pk=No return HttpResponseRedirect(reverse('document_view_simple', args=[document.pk])) else: return HttpResponseRedirect(request.get_full_path()) + except NewDocumentVersionNotAllowed: + if not document.is_new_versions_allowed: + messages.error(request, _(u'The check out options for document currently don\'t allow new version uploads.')) + else: + messages.error(request, _(u'This document currently don\'t allow new version uploads.')) except Exception, e: if settings.DEBUG: raise From 4a1acbbaac707803e3b624a6b309d941e23a7da2 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 16 Jun 2012 01:19:22 -0400 Subject: [PATCH 25/39] Preserve filename existing extension if new filename doesn't has one Fixes issue #24 --- apps/documents/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/documents/models.py b/apps/documents/models.py index 8185bb8297..d941c86c7d 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -524,8 +524,14 @@ class DocumentVersion(models.Model): return None def rename(self, new_name): + new_filename, new_extension = os.path.splitext(new_name) name, extension = os.path.splitext(self.filename) - self.filename = u''.join([new_name, extension]) + + # Preserve existing extension if new name doesn't has one + if new_extension: + extension = new_extension + + self.filename = u''.join([new_filename, extension]) self.save() From c39f1aa8a0eab5183a2e515f4271b18a2bd1ea0c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 16 Jun 2012 02:03:14 -0400 Subject: [PATCH 26/39] Add form field clean method to detect if documents are already checked out --- apps/checkouts/forms.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/checkouts/forms.py b/apps/checkouts/forms.py index d2032ed124..cfdecee26e 100644 --- a/apps/checkouts/forms.py +++ b/apps/checkouts/forms.py @@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from django.core import validators from .models import DocumentCheckout +from .exceptions import DocumentAlreadyCheckedOut class SplitDeltaWidget(forms.widgets.MultiWidget): @@ -101,3 +102,9 @@ class DocumentCheckoutForm(forms.ModelForm): widgets = { 'document': forms.widgets.HiddenInput(), } + + def clean_document(self): + document = self.cleaned_data['document'] + if document.is_checked_out(): + raise DocumentAlreadyCheckedOut + return document From f1fc13f739504464b241cccd0693c8ba9de7b064 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 16 Jun 2012 02:07:13 -0400 Subject: [PATCH 27/39] Move widgets code from forms.py to widgets.py --- apps/checkouts/forms.py | 86 +-------------------------------------- apps/checkouts/widgets.py | 86 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 85 deletions(-) diff --git a/apps/checkouts/forms.py b/apps/checkouts/forms.py index cfdecee26e..255a52fe55 100644 --- a/apps/checkouts/forms.py +++ b/apps/checkouts/forms.py @@ -1,95 +1,11 @@ from __future__ import absolute_import -import datetime - from django import forms from django.utils.translation import ugettext_lazy as _ -from django.core import validators from .models import DocumentCheckout from .exceptions import DocumentAlreadyCheckedOut - - -class SplitDeltaWidget(forms.widgets.MultiWidget): - """ - A Widget that splits a timedelta input into three boxes. - """ - def __init__(self, attrs=None): - widgets = ( - forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 5em;', 'placeholder': _(u'Days')}), - forms.widgets.TextInput(attrs={'maxlength': 4, 'style':'width: 5em;', 'placeholder': _(u'Hours')}), - forms.widgets.TextInput(attrs={'maxlength': 5, 'style':'width: 5em;', 'placeholder': _(u'Minutes')}), - ) - super(SplitDeltaWidget, self).__init__(widgets, attrs) - - def decompress(self, value): - if value: - return [value.days, value.seconds / 3600, (value.seconds / 60) % 60] - return [None, None, None] - - def value_from_datadict(self, data, files, name): - return [data.get('expiration_datetime_0', 0) or 0, data.get('expiration_datetime_1', 0) or 0, data.get('expiration_datetime_2', 0) or 0] - - -class SplitHiddenDeltaWidget(forms.widgets.SplitDateTimeWidget): - """ - A Widget that splits a timedelta input into three inputs. - """ - is_hidden = True - - def __init__(self, attrs=None): - super(SplitHiddenDeltaWidget, self).__init__(attrs, date_format, time_format) - for widget in self.widgets: - widget.input_type = 'hidden' - widget.is_hidden = True - - -class SplitTimeDeltaField(forms.MultiValueField): - widget = SplitDeltaWidget - hidden_widget = SplitHiddenDeltaWidget - default_error_messages = { - 'invalid_days': _(u'Enter a valid number of days.'), - 'invalid_hours': _(u'Enter a valid number of hours.'), - 'invalid_minutes': _(u'Enter a valid number of minutes.'), - } - - def __init__(self, *args, **kwargs): - errors = self.default_error_messages.copy() - if 'error_messages' in kwargs: - errors.update(kwargs['error_messages']) - localize = kwargs.get('localize', False) - fields = ( - forms.IntegerField(min_value=0, - error_messages={'invalid': errors['invalid_days']}, - localize=localize - ), - forms.IntegerField(min_value=0, - error_messages={'invalid': errors['invalid_hours']}, - localize=localize - ), - forms.IntegerField(min_value=0, - error_messages={'invalid': errors['invalid_minutes']}, - localize=localize - ), - ) - super(SplitTimeDeltaField, self).__init__(fields, *args, **kwargs) - self.help_text = _(u'Amount of time to hold the document in the checked out state in days, hours and/or minutes.') - self.label = _('Check out expiration date and time') - - def compress(self, data_list): - if data_list: - # Raise a validation error if time or date is empty - # (possible if SplitDateTimeField has required=False). - if data_list[0] in validators.EMPTY_VALUES: - raise ValidationError(self.error_messages['invalid_days']) - if data_list[1] in validators.EMPTY_VALUES: - raise ValidationError(self.error_messages['invalid_hours']) - if data_list[2] in validators.EMPTY_VALUES: - raise ValidationError(self.error_messages['invalid_minutes']) - - timedelta = datetime.timedelta(days=data_list[0], hours=data_list[1], minutes=data_list[2]) - return datetime.datetime.now() + timedelta - return None +from .widgets import SplitTimeDeltaField class DocumentCheckoutForm(forms.ModelForm): diff --git a/apps/checkouts/widgets.py b/apps/checkouts/widgets.py index e1a0d38302..bc44211f4c 100644 --- a/apps/checkouts/widgets.py +++ b/apps/checkouts/widgets.py @@ -1,8 +1,12 @@ from __future__ import absolute_import +import datetime + +from django import forms from django.utils.translation import ugettext_lazy as _ from django.utils.safestring import mark_safe from django.conf import settings +from django.core import validators from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN, STATE_ICONS, STATE_LABELS @@ -15,3 +19,85 @@ def checkout_widget(document): 'widget': mark_safe(widget), 'text': STATE_LABELS[checkout_state] } + + +class SplitDeltaWidget(forms.widgets.MultiWidget): + """ + A Widget that splits a timedelta input into three boxes. + """ + def __init__(self, attrs=None): + widgets = ( + forms.widgets.TextInput(attrs={'maxlength': 3, 'style':'width: 5em;', 'placeholder': _(u'Days')}), + forms.widgets.TextInput(attrs={'maxlength': 4, 'style':'width: 5em;', 'placeholder': _(u'Hours')}), + forms.widgets.TextInput(attrs={'maxlength': 5, 'style':'width: 5em;', 'placeholder': _(u'Minutes')}), + ) + super(SplitDeltaWidget, self).__init__(widgets, attrs) + + def decompress(self, value): + if value: + return [value.days, value.seconds / 3600, (value.seconds / 60) % 60] + return [None, None, None] + + def value_from_datadict(self, data, files, name): + return [data.get('expiration_datetime_0', 0) or 0, data.get('expiration_datetime_1', 0) or 0, data.get('expiration_datetime_2', 0) or 0] + + +class SplitHiddenDeltaWidget(forms.widgets.SplitDateTimeWidget): + """ + A Widget that splits a timedelta input into three inputs. + """ + is_hidden = True + + def __init__(self, attrs=None): + super(SplitHiddenDeltaWidget, self).__init__(attrs, date_format, time_format) + for widget in self.widgets: + widget.input_type = 'hidden' + widget.is_hidden = True + + +class SplitTimeDeltaField(forms.MultiValueField): + widget = SplitDeltaWidget + hidden_widget = SplitHiddenDeltaWidget + default_error_messages = { + 'invalid_days': _(u'Enter a valid number of days.'), + 'invalid_hours': _(u'Enter a valid number of hours.'), + 'invalid_minutes': _(u'Enter a valid number of minutes.'), + } + + def __init__(self, *args, **kwargs): + errors = self.default_error_messages.copy() + if 'error_messages' in kwargs: + errors.update(kwargs['error_messages']) + localize = kwargs.get('localize', False) + fields = ( + forms.IntegerField(min_value=0, + error_messages={'invalid': errors['invalid_days']}, + localize=localize + ), + forms.IntegerField(min_value=0, + error_messages={'invalid': errors['invalid_hours']}, + localize=localize + ), + forms.IntegerField(min_value=0, + error_messages={'invalid': errors['invalid_minutes']}, + localize=localize + ), + ) + super(SplitTimeDeltaField, self).__init__(fields, *args, **kwargs) + self.help_text = _(u'Amount of time to hold the document in the checked out state in days, hours and/or minutes.') + self.label = _('Check out expiration date and time') + + def compress(self, data_list): + if data_list: + # Raise a validation error if time or date is empty + # (possible if SplitDateTimeField has required=False). + if data_list[0] in validators.EMPTY_VALUES: + raise ValidationError(self.error_messages['invalid_days']) + if data_list[1] in validators.EMPTY_VALUES: + raise ValidationError(self.error_messages['invalid_hours']) + if data_list[2] in validators.EMPTY_VALUES: + raise ValidationError(self.error_messages['invalid_minutes']) + + timedelta = datetime.timedelta(days=data_list[0], hours=data_list[1], minutes=data_list[2]) + return datetime.datetime.now() + timedelta + return None From b3c530068c0ba75f8e651205c9a75c4c1a32cd05 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 16 Jun 2012 02:07:36 -0400 Subject: [PATCH 28/39] Add TODO item --- apps/checkouts/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/checkouts/__init__.py b/apps/checkouts/__init__.py index adb360a66c..ab043da135 100644 --- a/apps/checkouts/__init__.py +++ b/apps/checkouts/__init__.py @@ -39,3 +39,4 @@ initialize_document_checkout_extra_methods() #TODO: specify checkout option check (document.allows_new_versions()) #TODO: out check in after expiration datetime #TODO: add checkin out history +#TODO: limit restrictions to non checkout user and admins? From b8e289b3d2ef96d7123fdefb24e6c7e73b65e5d7 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 16 Jun 2012 02:07:53 -0400 Subject: [PATCH 29/39] Remove remarked code --- apps/checkouts/models.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/apps/checkouts/models.py b/apps/checkouts/models.py index 333703a746..4600a99b1a 100644 --- a/apps/checkouts/models.py +++ b/apps/checkouts/models.py @@ -41,19 +41,9 @@ class DocumentCheckout(models.Model): def save(self, *args, **kwargs): if not self.pk: self.checkout_datetime = datetime.datetime.now() - try: - return super(DocumentCheckout, self).save(*args, **kwargs) - except IntegrityError, exc: - #if exc[1] == 'Column \'checkout_datetime\' cannot be null': - # raise DocumentAlreadyCheckedOut - #else: - raise - else: - #create_history(HISTORY_DOCUMENT_DELETED, data={'user': request.user, 'document': document}) - pass - - - #TODO: clean method that raises DocumentAlreadyCheckedOut + result = super(DocumentCheckout, self).save(*args, **kwargs) + #create_history(HISTORY_DOCUMENT_DELETED, data={'user': request.user, 'document': document}) + return result @models.permalink def get_absolute_url(self): From 1a2813adcbef3e607431222db69fb95755a27cf8 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 16 Jun 2012 02:08:08 -0400 Subject: [PATCH 30/39] Properly catch exception in checkout view --- apps/checkouts/views.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/checkouts/views.py b/apps/checkouts/views.py index 9df54dc145..879a244b3a 100644 --- a/apps/checkouts/views.py +++ b/apps/checkouts/views.py @@ -73,19 +73,19 @@ def checkout_document(request, document_pk): if request.method == 'POST': form = DocumentCheckoutForm(data=request.POST, initial={'document': document}) - if form.is_valid(): - try: - document_checkout = form.save(commit=False) - document_checkout.user_object = request.user - #document_checkout.clean() - document_checkout.save() - except DocumentAlreadyCheckedOut: - messages.error(request, _(u'Document already checked out.')) - except Exception, exc: - messages.error(request, _(u'Error trying to check out document; %s') % exc) - else: - messages.success(request, _(u'Document "%s" checked out successfully.') % document) - return HttpResponseRedirect(reverse('checkout_info', args=[document.pk])) + try: + if form.is_valid(): + try: + document_checkout = form.save(commit=False) + document_checkout.user_object = request.user + document_checkout.save() + except Exception, exc: + messages.error(request, _(u'Error trying to check out document; %s') % exc) + else: + messages.success(request, _(u'Document "%s" checked out successfully.') % document) + return HttpResponseRedirect(reverse('checkout_info', args=[document.pk])) + except DocumentAlreadyCheckedOut: + messages.error(request, _(u'Document already checked out.')) else: form = DocumentCheckoutForm(initial={'document': document}) From c01b375b989f11101053d5d8486e59f48266cb1d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 16 Jun 2012 23:30:48 -0400 Subject: [PATCH 31/39] Redirect to checkout info if document is already checked out --- apps/checkouts/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/checkouts/views.py b/apps/checkouts/views.py index 879a244b3a..97a28ba446 100644 --- a/apps/checkouts/views.py +++ b/apps/checkouts/views.py @@ -54,7 +54,6 @@ def checkout_info(request, document_pk): paragraphs.append(_(u'User: %s') % get_object_name(checkout_info.user_object, display_object_type=False)) paragraphs.append(_(u'Check out time: %s') % checkout_info.checkout_datetime) paragraphs.append(_(u'Check out expiration: %s') % checkout_info.expiration_datetime) - paragraphs.append(_(u'Check out expiration: %s') % checkout_info.expiration_datetime) paragraphs.append(_(u'New versions allowed: %s') % (_(u'yes') if not checkout_info.block_new_version else _(u'no'))) return render_to_response('generic_template.html', { @@ -86,6 +85,7 @@ def checkout_document(request, document_pk): return HttpResponseRedirect(reverse('checkout_info', args=[document.pk])) except DocumentAlreadyCheckedOut: messages.error(request, _(u'Document already checked out.')) + return HttpResponseRedirect(reverse('checkout_info', args=[document.pk])) else: form = DocumentCheckoutForm(initial={'document': document}) From 87917ca394d494a58b26141c9b1e0fc61c72af0e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 16 Jun 2012 23:31:13 -0400 Subject: [PATCH 32/39] Add automatic check in of documents after check out expiration happens --- apps/checkouts/__init__.py | 8 +++++++- apps/checkouts/managers.py | 8 ++++++++ apps/checkouts/tasks.py | 24 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 apps/checkouts/tasks.py diff --git a/apps/checkouts/__init__.py b/apps/checkouts/__init__.py index ab043da135..3c9cc51b0b 100644 --- a/apps/checkouts/__init__.py +++ b/apps/checkouts/__init__.py @@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from navigation.api import (register_links, register_top_menu, register_multi_item_links, register_sidebar_template) +from scheduler.api import register_interval_job from documents.models import Document from documents.permissions import PERMISSION_DOCUMENT_VIEW @@ -12,6 +13,7 @@ from acls.api import class_permissions from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE) from .links import checkout_list, checkout_document, checkout_info, checkin_document from .models import DocumentCheckout +from .tasks import task_check_expired_check_outs def initialize_document_checkout_extra_methods(): @@ -31,8 +33,10 @@ class_permissions(Document, [ PERMISSION_DOCUMENT_CHECKIN_OVERRIDE ]) -initialize_document_checkout_extra_methods() +CHECK_EXPIRED_CHECK_OUTS_INTERVAL=60 # Lowest check out expiration allowed +register_interval_job('task_check_expired_check_outs', _(u'Checks the OCR queue for pending documents.'), task_check_expired_check_outs, seconds=CHECK_EXPIRED_CHECK_OUTS_INTERVAL) +initialize_document_checkout_extra_methods() #TODO: default checkout time #TODO: forcefull check in @@ -40,3 +44,5 @@ initialize_document_checkout_extra_methods() #TODO: out check in after expiration datetime #TODO: add checkin out history #TODO: limit restrictions to non checkout user and admins? + + diff --git a/apps/checkouts/managers.py b/apps/checkouts/managers.py index 68391106b4..bd995ac43d 100644 --- a/apps/checkouts/managers.py +++ b/apps/checkouts/managers.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import datetime from django.db import models from documents.models import Document @@ -13,6 +14,13 @@ class DocumentCheckoutManager(models.Manager): def checked_out_documents(self): return Document.objects.filter(pk__in=self.model.objects.all().values_list('document__pk', flat=True)) + + def expired_check_outs(self): + return Document.objects.filter(pk__in=self.model.objects.filter(expiration_datetime__gt=datetime.datetime.now()).values_list('document__pk', flat=True)) + + def check_in_expired_check_outs(self): + for document in self.expired_check_outs(): + document.check_in() def is_document_checked_out(self, document): if self.model.objects.filter(document=document): diff --git a/apps/checkouts/tasks.py b/apps/checkouts/tasks.py new file mode 100644 index 0000000000..d97f08f54b --- /dev/null +++ b/apps/checkouts/tasks.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import + +import logging + +from lock_manager import Lock, LockError + +from .models import DocumentCheckout + +LOCK_EXPIRE = 50 +logger = logging.getLogger(__name__) + + +def task_check_expired_check_outs(): + logger.debug('executing...') + lock_id = u'task_expired_check_outs' + 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) + DocumentCheckout.objects.check_in_expired_check_outs() + lock.release() + except LockError: + logger.debug('unable to obtain lock') + pass From ec9cc0635d540c299a9f53d0f23ddef5a055bba9 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 17 Jun 2012 00:24:52 -0400 Subject: [PATCH 33/39] Rename 'Event details' to 'Additional details' and make it optional --- apps/history/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/history/views.py b/apps/history/views.py index 2212c7e5ef..7f61bf80e0 100644 --- a/apps/history/views.py +++ b/apps/history/views.py @@ -88,11 +88,11 @@ def history_view(request, object_id): AccessEntry.objects.check_access(PERMISSION_HISTORY_VIEW, request.user, history.content_object) form = HistoryDetailForm(instance=history, extra_fields=[ - {'label': _(u'Date'), 'field':lambda x: x.datetime.date()}, - {'label': _(u'Time'), 'field':lambda x: unicode(x.datetime.time()).split('.')[0]}, + {'label': _(u'Date'), 'field': lambda x: x.datetime.date()}, + {'label': _(u'Time'), 'field': lambda x: unicode(x.datetime.time()).split('.')[0]}, {'label': _(u'Object'), 'field': 'content_object'}, {'label': _(u'Event type'), 'field': lambda x: x.get_label()}, - {'label': _(u'Event details'), 'field': lambda x: x.get_processed_details()}, + {'label': _(u'Additional details'), 'field': lambda x: x.get_processed_details() or _(u'None')}, ]) return render_to_response('generic_detail.html', { From 669f699f458d0ed3a700920f2a261aebcbbc5066 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 15 Apr 2012 22:47:14 -0400 Subject: [PATCH 34/39] Fix ACL calculation when user doesn't belong to any group or role --- apps/acls/managers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/acls/managers.py b/apps/acls/managers.py index 6ba807a897..529643c7ec 100644 --- a/apps/acls/managers.py +++ b/apps/acls/managers.py @@ -137,14 +137,14 @@ class AccessEntryManager(models.Manager): content_type = ContentType.objects.get_for_model(cls) # Calculate actor role membership ACL query - total_queries = None + total_queries = Q() for role in RoleMember.objects.get_roles_for_member(actor): role_type = ContentType.objects.get_for_model(role) if related: query = Q(holder_type=role_type, holder_id=role.pk, permission=permission.get_stored_permission) else: query = Q(holder_type=role_type, holder_id=role.pk, content_type=content_type, permission=permission.get_stored_permission) - if total_queries is None: + if not total_queries: total_queries = query else: total_queries = total_queries | query @@ -161,7 +161,7 @@ class AccessEntryManager(models.Manager): query = Q(holder_type=group_type, holder_id=group.pk, permission=permission.get_stored_permission) else: query = Q(holder_type=group_type, holder_id=group.pk, content_type=content_type, permission=permission.get_stored_permission) - if total_queries is None: + if not total_queries: total_queries = query else: total_queries = total_queries | query From a6e1df9b3af28af0502e363ba64c6e03c48e7234 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 17 Jun 2012 00:38:00 -0400 Subject: [PATCH 35/39] Allow giving access to the history of specific documents via ACLs --- apps/history/__init__.py | 3 +-- apps/history/views.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/history/__init__.py b/apps/history/__init__.py index b0b4025a3e..24469fbd2c 100644 --- a/apps/history/__init__.py +++ b/apps/history/__init__.py @@ -6,7 +6,6 @@ from project_tools.api import register_tool from .permissions import PERMISSION_HISTORY_VIEW - -history_list = {'text': _(u'history'), 'view': 'history_list', 'famfam': 'book', 'icon': 'book.png', 'permissions': [PERMISSION_HISTORY_VIEW], 'children_view_regex': [r'history_[l,v]']} +history_list = {'text': _(u'history'), 'view': 'history_list', 'famfam': 'book', 'icon': 'book.png', 'children_view_regex': [r'history_[l,v]']} register_tool(history_list) diff --git a/apps/history/views.py b/apps/history/views.py index 7f61bf80e0..8b29a5d1ca 100644 --- a/apps/history/views.py +++ b/apps/history/views.py @@ -19,12 +19,22 @@ from .permissions import PERMISSION_HISTORY_VIEW from .widgets import history_entry_object_link, history_entry_summary -def history_list(request): - Permission.objects.check_permissions(request.user, [PERMISSION_HISTORY_VIEW]) +def history_list(request, object_list=None, title=None, extra_context=None): + pre_object_list = object_list if not (object_list is None) else History.objects.all() + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_HISTORY_VIEW]) + except PermissionDenied: + # If user doesn't have global permission, get a list of document + # for which he/she does hace access use it to filter the + # provided object_list + final_object_list = AccessEntry.objects.filter_objects_by_access(PERMISSION_HISTORY_VIEW, request.user, pre_object_list, related='content_object') + else: + final_object_list = pre_object_list context = { - 'object_list': History.objects.all(), - 'title': _(u'history events'), + 'object_list': final_object_list, + 'title': title if title else _(u'history events'), 'extra_columns': [ { 'name': _(u'date and time'), @@ -42,6 +52,9 @@ def history_list(request): 'hide_object': True, } + if extra_context: + context.update(extra_context) + return render_to_response('generic_list.html', context, context_instance=RequestContext(request)) From 0e47bbb10b62eccf4ae767229fe9016963bb78d3 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 17 Jun 2012 00:39:54 -0400 Subject: [PATCH 36/39] Add history events 'check out' and 'check in' logging --- apps/checkouts/__init__.py | 14 ++++++-------- apps/checkouts/events.py | 15 +++++++++++++++ apps/checkouts/managers.py | 17 ++++++++++++----- apps/checkouts/models.py | 4 +++- apps/checkouts/views.py | 2 +- 5 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 apps/checkouts/events.py diff --git a/apps/checkouts/__init__.py b/apps/checkouts/__init__.py index 3c9cc51b0b..1b3f2b740a 100644 --- a/apps/checkouts/__init__.py +++ b/apps/checkouts/__init__.py @@ -9,16 +9,18 @@ from scheduler.api import register_interval_job from documents.models import Document from documents.permissions import PERMISSION_DOCUMENT_VIEW from acls.api import class_permissions +from history.api import register_history_type from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE) from .links import checkout_list, checkout_document, checkout_info, checkin_document from .models import DocumentCheckout from .tasks import task_check_expired_check_outs +from .events import HISTORY_DOCUMENT_CHECKED_OUT, HISTORY_DOCUMENT_CHECKED_IN def initialize_document_checkout_extra_methods(): Document.add_to_class('is_checked_out', lambda document: DocumentCheckout.objects.is_document_checked_out(document)) - Document.add_to_class('check_in', lambda document: DocumentCheckout.objects.check_in_document(document)) + Document.add_to_class('check_in', lambda document, user=None: DocumentCheckout.objects.check_in_document(document, user)) Document.add_to_class('checkout_info', lambda document: DocumentCheckout.objects.document_checkout_info(document)) Document.add_to_class('checkout_state', lambda document: DocumentCheckout.objects.document_checkout_state(document)) Document.add_to_class('is_new_versions_allowed', lambda document: DocumentCheckout.objects.is_document_new_versions_allowed(document)) @@ -34,15 +36,11 @@ class_permissions(Document, [ ]) CHECK_EXPIRED_CHECK_OUTS_INTERVAL=60 # Lowest check out expiration allowed -register_interval_job('task_check_expired_check_outs', _(u'Checks the OCR queue for pending documents.'), task_check_expired_check_outs, seconds=CHECK_EXPIRED_CHECK_OUTS_INTERVAL) - +register_interval_job('task_check_expired_check_outs', _(u'Check expired check out documents and checks them in.'), task_check_expired_check_outs, seconds=CHECK_EXPIRED_CHECK_OUTS_INTERVAL) initialize_document_checkout_extra_methods() +register_history_type(HISTORY_DOCUMENT_CHECKED_OUT) +register_history_type(HISTORY_DOCUMENT_CHECKED_IN) -#TODO: default checkout time #TODO: forcefull check in -#TODO: specify checkout option check (document.allows_new_versions()) -#TODO: out check in after expiration datetime #TODO: add checkin out history #TODO: limit restrictions to non checkout user and admins? - - diff --git a/apps/checkouts/events.py b/apps/checkouts/events.py new file mode 100644 index 0000000000..6d91d8d4ea --- /dev/null +++ b/apps/checkouts/events.py @@ -0,0 +1,15 @@ +from django.utils.translation import ugettext_lazy as _ + +HISTORY_DOCUMENT_CHECKED_OUT = { + 'namespace': 'checkouts', 'name': 'document_checked_out', + 'label': _(u'Document checked out'), + 'summary': _(u'Document "%(document)s" checked out by %(fullname)s.'), + 'expressions': {'fullname': 'user.get_full_name() if user.get_full_name() else user'} +} + +HISTORY_DOCUMENT_CHECKED_IN = { + 'namespace': 'checkouts', 'name': 'document_checked_in', + 'label': _(u'Document checked in'), + 'summary': _(u'Document "%(document)s" checked in by %(fullname)s.'), + 'expressions': {'fullname': 'user.get_full_name() if user.get_full_name() else user'} +} diff --git a/apps/checkouts/managers.py b/apps/checkouts/managers.py index bd995ac43d..bd3d815a12 100644 --- a/apps/checkouts/managers.py +++ b/apps/checkouts/managers.py @@ -1,22 +1,28 @@ from __future__ import absolute_import import datetime +import logging + from django.db import models from documents.models import Document +from history.api import create_history from .exceptions import DocumentNotCheckedOut from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN +from .events import HISTORY_DOCUMENT_CHECKED_IN + +logger = logging.getLogger(__name__) class DocumentCheckoutManager(models.Manager): - #TODO: 'check_expiration' method - def checked_out_documents(self): return Document.objects.filter(pk__in=self.model.objects.all().values_list('document__pk', flat=True)) def expired_check_outs(self): - return Document.objects.filter(pk__in=self.model.objects.filter(expiration_datetime__gt=datetime.datetime.now()).values_list('document__pk', flat=True)) + expired_list = Document.objects.filter(pk__in=self.model.objects.filter(expiration_datetime__lte=datetime.datetime.now()).values_list('document__pk', flat=True)) + logger.debug('expired_list: %s' % expired_list) + return expired_list def check_in_expired_check_outs(self): for document in self.expired_check_outs(): @@ -28,13 +34,14 @@ class DocumentCheckoutManager(models.Manager): else: return False - def check_in_document(self, document): + def check_in_document(self, document, user=None): try: document_checkout = self.model.objects.get(document=document) except self.model.DoesNotExist: raise DocumentNotCheckedOut else: - #create_history(HISTORY_DOCUMENT_DELETED, data={'user': request.user, 'document': document}) + if user: + create_history(HISTORY_DOCUMENT_CHECKED_IN, source_object=document, data={'user': user, 'document': document}) document_checkout.delete() def document_checkout_info(self, document): diff --git a/apps/checkouts/models.py b/apps/checkouts/models.py index 4600a99b1a..819b8856c3 100644 --- a/apps/checkouts/models.py +++ b/apps/checkouts/models.py @@ -9,9 +9,11 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic from documents.models import Document +from history.api import create_history from .managers import DocumentCheckoutManager from .exceptions import DocumentAlreadyCheckedOut +from .events import HISTORY_DOCUMENT_CHECKED_OUT logger = logging.getLogger(__name__) @@ -42,7 +44,7 @@ class DocumentCheckout(models.Model): if not self.pk: self.checkout_datetime = datetime.datetime.now() result = super(DocumentCheckout, self).save(*args, **kwargs) - #create_history(HISTORY_DOCUMENT_DELETED, data={'user': request.user, 'document': document}) + create_history(HISTORY_DOCUMENT_CHECKED_OUT, source_object=self.document, data={'user': self.user_object, 'document': self.document}) return result @models.permalink diff --git a/apps/checkouts/views.py b/apps/checkouts/views.py index 97a28ba446..ff52336f38 100644 --- a/apps/checkouts/views.py +++ b/apps/checkouts/views.py @@ -111,7 +111,7 @@ def checkin_document(request, document_pk): if request.method == 'POST': try: - document.check_in() + document.check_in(user=request.user) except DocumentNotCheckedOut: messages.error(request, _(u'Document has not been checked out.')) except Exception, exc: From d34714bafa1ac41a9c6eaf0a93ff120874c3b450 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 17 Jun 2012 00:40:46 -0400 Subject: [PATCH 37/39] Add split time delta validation exception when empty --- apps/checkouts/forms.py | 2 +- apps/checkouts/widgets.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/checkouts/forms.py b/apps/checkouts/forms.py index 255a52fe55..80fb3091f9 100644 --- a/apps/checkouts/forms.py +++ b/apps/checkouts/forms.py @@ -23,4 +23,4 @@ class DocumentCheckoutForm(forms.ModelForm): document = self.cleaned_data['document'] if document.is_checked_out(): raise DocumentAlreadyCheckedOut - return document + return document diff --git a/apps/checkouts/widgets.py b/apps/checkouts/widgets.py index bc44211f4c..cd5f3d8f70 100644 --- a/apps/checkouts/widgets.py +++ b/apps/checkouts/widgets.py @@ -62,6 +62,7 @@ class SplitTimeDeltaField(forms.MultiValueField): 'invalid_days': _(u'Enter a valid number of days.'), 'invalid_hours': _(u'Enter a valid number of hours.'), 'invalid_minutes': _(u'Enter a valid number of minutes.'), + 'invalid_timedelta': _(u'Enter a valid time difference.'), } def __init__(self, *args, **kwargs): @@ -88,15 +89,18 @@ class SplitTimeDeltaField(forms.MultiValueField): self.label = _('Check out expiration date and time') def compress(self, data_list): + if data_list == [0, 0, 0]: + raise forms.ValidationError(self.error_messages['invalid_timedelta']) + if data_list: # Raise a validation error if time or date is empty # (possible if SplitDateTimeField has required=False). if data_list[0] in validators.EMPTY_VALUES: - raise ValidationError(self.error_messages['invalid_days']) + raise forms.ValidationError(self.error_messages['invalid_days']) if data_list[1] in validators.EMPTY_VALUES: - raise ValidationError(self.error_messages['invalid_hours']) + raise forms.ValidationError(self.error_messages['invalid_hours']) if data_list[2] in validators.EMPTY_VALUES: - raise ValidationError(self.error_messages['invalid_minutes']) + raise forms.ValidationError(self.error_messages['invalid_minutes']) timedelta = datetime.timedelta(days=data_list[0], hours=data_list[1], minutes=data_list[2]) return datetime.datetime.now() + timedelta From 4bf6310d1dbeffcef48802581357fe985da249ec Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 17 Jun 2012 01:08:47 -0400 Subject: [PATCH 38/39] Only impose check out limitations to non original user and not admin, add checkout limitation overriding permission --- apps/checkouts/__init__.py | 11 ++++++----- apps/checkouts/managers.py | 34 ++++++++++++++++++++++++++++++++-- apps/checkouts/permissions.py | 1 + apps/documents/models.py | 4 ++-- apps/sources/models.py | 2 +- apps/sources/views.py | 10 ++-------- 6 files changed, 44 insertions(+), 18 deletions(-) diff --git a/apps/checkouts/__init__.py b/apps/checkouts/__init__.py index 1b3f2b740a..2ad1a215b6 100644 --- a/apps/checkouts/__init__.py +++ b/apps/checkouts/__init__.py @@ -11,7 +11,9 @@ from documents.permissions import PERMISSION_DOCUMENT_VIEW from acls.api import class_permissions from history.api import register_history_type -from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE) +from .permissions import (PERMISSION_DOCUMENT_CHECKOUT, + PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE, + PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE) from .links import checkout_list, checkout_document, checkout_info, checkin_document from .models import DocumentCheckout from .tasks import task_check_expired_check_outs @@ -23,7 +25,7 @@ def initialize_document_checkout_extra_methods(): Document.add_to_class('check_in', lambda document, user=None: DocumentCheckout.objects.check_in_document(document, user)) Document.add_to_class('checkout_info', lambda document: DocumentCheckout.objects.document_checkout_info(document)) Document.add_to_class('checkout_state', lambda document: DocumentCheckout.objects.document_checkout_state(document)) - Document.add_to_class('is_new_versions_allowed', lambda document: DocumentCheckout.objects.is_document_new_versions_allowed(document)) + Document.add_to_class('is_new_versions_allowed', lambda document, user=None: DocumentCheckout.objects.is_document_new_versions_allowed(document, user)) register_top_menu(name='checkouts', link=checkout_list) register_links(Document, [checkout_info], menu_name='form_header') @@ -32,7 +34,8 @@ register_links(['checkout_info', 'checkout_document', 'checkin_document'], [chec class_permissions(Document, [ PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN, - PERMISSION_DOCUMENT_CHECKIN_OVERRIDE + PERMISSION_DOCUMENT_CHECKIN_OVERRIDE, + PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE ]) CHECK_EXPIRED_CHECK_OUTS_INTERVAL=60 # Lowest check out expiration allowed @@ -42,5 +45,3 @@ register_history_type(HISTORY_DOCUMENT_CHECKED_OUT) register_history_type(HISTORY_DOCUMENT_CHECKED_IN) #TODO: forcefull check in -#TODO: add checkin out history -#TODO: limit restrictions to non checkout user and admins? diff --git a/apps/checkouts/managers.py b/apps/checkouts/managers.py index bd3d815a12..c1185651ef 100644 --- a/apps/checkouts/managers.py +++ b/apps/checkouts/managers.py @@ -4,13 +4,17 @@ import datetime import logging from django.db import models +from django.core.exceptions import PermissionDenied from documents.models import Document from history.api import create_history +from permissions.models import Permission +from acls.models import AccessEntry from .exceptions import DocumentNotCheckedOut from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN from .events import HISTORY_DOCUMENT_CHECKED_IN +from .permissions import PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE logger = logging.getLogger(__name__) @@ -56,8 +60,34 @@ class DocumentCheckoutManager(models.Manager): else: return STATE_CHECKED_IN - def is_document_new_versions_allowed(self, document): + def is_document_new_versions_allowed(self, document, user=None): try: - return not self.document_checkout_info(document).block_new_version + checkout_info = self.document_checkout_info(document) except DocumentNotCheckedOut: return True + else: + if not user: + return not checkout_info.block_new_version + else: + if user.is_staff or user.is_superuser: + # Allow anything to superusers and staff + return True + + if user == checkout_info.user_object: + # Allow anything to the user who checked out this document + True + else: + # If not original user check to see if user has global or this document's PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE permission + try: + Permission.objects.check_permissions(user, [PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE]) + except PermissionDenied: + try: + AccessEntry.objects.check_accesses([PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE], user, document) + except PermissionDenied: + # Last resort check if original user enabled restriction + return not checkout_info.block_new_version + else: + return True + else: + return True + diff --git a/apps/checkouts/permissions.py b/apps/checkouts/permissions.py index 2a24c9d47d..19430381b8 100644 --- a/apps/checkouts/permissions.py +++ b/apps/checkouts/permissions.py @@ -9,4 +9,5 @@ namespace = PermissionNamespace('checkouts', _(u'Document checkout')) PERMISSION_DOCUMENT_CHECKOUT = Permission.objects.register(namespace, 'checkout_document', _(u'Check out documents')) PERMISSION_DOCUMENT_CHECKIN = Permission.objects.register(namespace, 'checkin_document', _(u'Check in documents')) PERMISSION_DOCUMENT_CHECKIN_OVERRIDE = Permission.objects.register(namespace, 'checkin_document_override', _(u'Forcefully check in documents')) +PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE = Permission.objects.register(namespace, 'checkout_restrictions_override', _(u'Allow overriding check out restrictions')) diff --git a/apps/documents/models.py b/apps/documents/models.py index d941c86c7d..eb94051902 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -170,9 +170,9 @@ class Document(models.Model): def size(self): return self.latest_version.size - def new_version(self, file, comment=None, version_update=None, release_level=None, serial=None): + def new_version(self, file, user=None, comment=None, version_update=None, release_level=None, serial=None): logger.debug('creating new document version') - if not self.is_new_versions_allowed(): + if not self.is_new_versions_allowed(user=user): raise NewDocumentVersionNotAllowed if version_update: diff --git a/apps/sources/models.py b/apps/sources/models.py index 0941abd00d..e659b4e60e 100644 --- a/apps/sources/models.py +++ b/apps/sources/models.py @@ -114,7 +114,7 @@ class BaseModel(models.Model): new_version_data = {} try: - new_version = document.new_version(file=file_object, **new_version_data) + new_version = document.new_version(file=file_object, user=user, **new_version_data) except Exception: # Don't leave the database in a broken state # document.delete() diff --git a/apps/sources/views.py b/apps/sources/views.py index c436e4daeb..0014d9b4cf 100644 --- a/apps/sources/views.py +++ b/apps/sources/views.py @@ -176,10 +176,7 @@ def upload_interactive(request, source_type=None, source_id=None, document_pk=No return HttpResponseRedirect(request.get_full_path()) except NewDocumentVersionNotAllowed: - if not document.is_new_versions_allowed(): - messages.error(request, _(u'The check out options for document currently don\'t allow new version uploads.')) - else: - messages.error(request, _(u'This document currently don\'t allow new version uploads.')) + messages.error(request, _(u'New version uploads are not allowed for this document.')) except Exception, e: if settings.DEBUG: raise @@ -260,10 +257,7 @@ def upload_interactive(request, source_type=None, source_id=None, document_pk=No else: return HttpResponseRedirect(request.get_full_path()) except NewDocumentVersionNotAllowed: - if not document.is_new_versions_allowed: - messages.error(request, _(u'The check out options for document currently don\'t allow new version uploads.')) - else: - messages.error(request, _(u'This document currently don\'t allow new version uploads.')) + messages.error(request, _(u'New version uploads are not allowed for this document.')) except Exception, e: if settings.DEBUG: raise From 9104ff4c4ba5d5a242d7a2b34138276b3ab5a75c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 17 Jun 2012 01:31:56 -0400 Subject: [PATCH 39/39] Update documentation for version 0.12.2 --- docs/index.rst | 5 ++-- docs/releases/0.12.2.rst | 50 ++++++++++++++++++++++++++++++++++++++++ docs/releases/index.rst | 3 ++- 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 docs/releases/0.12.2.rst diff --git a/docs/index.rst b/docs/index.rst index fe94d819c9..5bb72eba86 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Mayan EDMS documentation ======================== .. rubric:: `Open source`_, Django_ based document manager with custom - metadata_ indexing_, file serving integration, OCR_ capabilities, + metadata_ indexing_, file serving integration, `checking out and in`_, OCR_ capabilities, document versioning_ and `digital signature verification`_. .. _Django: http://www.djangoproject.com/ @@ -16,6 +16,7 @@ Mayan EDMS documentation .. _metadata: http://en.wikipedia.org/wiki/Metadata .. _indexing: http://en.wikipedia.org/wiki/Index_card .. _Open source: http://en.wikipedia.org/wiki/Open_source +.. _checking out and in: http://en.wikipedia.org/wiki/Revision_control On the Web ===================== @@ -54,7 +55,7 @@ Understanding Mayan EDMS Between versions ================ .. toctree:: - :maxdepth: 2 + :maxdepth: 1 releases/index diff --git a/docs/releases/0.12.2.rst b/docs/releases/0.12.2.rst new file mode 100644 index 0000000000..8220786960 --- /dev/null +++ b/docs/releases/0.12.2.rst @@ -0,0 +1,50 @@ +================================ +Mayan EDMS v0.12.2 release notes +================================ + +*June 2012* + +This is the second maintenance release of the 0.12 series. + +Overview +======== + + +As with the previous release bug fixes and minor feature were the focus +for this release too. Long standing `issue #24`_ has been fixed and document +check outs have been added too as per the feature request posted as `issue #26`_. + +What's new in Mayan EDMS v0.12.2 +================================ + +Document check outs +~~~~~~~~~~~~~~~~~~~~~ + + +Upgrading from a previous version +================================= + +Migrate existing database schema with:: + + $ ./manage.py migrate checkouts + +The upgrade procedure is now complete. + + +Backward incompatible changes +============================= +* None + +Bugs fixed +========== +* `issue #24`_ "Duplicated filename extension when uploading a new version of a document" +* `issue #26`_ "checkout feature request" + +Stuff removed +============= +* None + + + +.. _issue #24: https://github.com/rosarior/mayan/issues/24 +.. _issue #26: https://github.com/rosarior/mayan/issues/26 diff --git a/docs/releases/index.rst b/docs/releases/index.rst index 328b8f08cf..478e26bbd1 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -11,11 +11,12 @@ all the backwards-incompatible changes and deprecated features for each 'final' release from the one after your current **Mayan EDMS** version, up to and including the latest version. -Latest version (0.12.1) +Latest version (0.12.2) ----------------------- .. toctree:: :maxdepth: 1 + 0.12.2 0.12.1 0.12