Merge remote-tracking branch 'origin/feature/document_checkout' into hotfix/v0.12.2
This commit is contained in:
@@ -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
|
||||
|
||||
47
apps/checkouts/__init__.py
Normal file
47
apps/checkouts/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
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 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,
|
||||
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
|
||||
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, 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, 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')
|
||||
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,
|
||||
PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE
|
||||
])
|
||||
|
||||
CHECK_EXPIRED_CHECK_OUTS_INTERVAL=60 # Lowest check out expiration allowed
|
||||
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: forcefull check in
|
||||
15
apps/checkouts/events.py
Normal file
15
apps/checkouts/events.py
Normal file
@@ -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'}
|
||||
}
|
||||
11
apps/checkouts/exceptions.py
Normal file
11
apps/checkouts/exceptions.py
Normal file
@@ -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
|
||||
26
apps/checkouts/forms.py
Normal file
26
apps/checkouts/forms.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .models import DocumentCheckout
|
||||
from .exceptions import DocumentAlreadyCheckedOut
|
||||
from .widgets import SplitTimeDeltaField
|
||||
|
||||
|
||||
class DocumentCheckoutForm(forms.ModelForm):
|
||||
expiration_datetime = SplitTimeDeltaField()
|
||||
|
||||
class Meta:
|
||||
model = DocumentCheckout
|
||||
exclude = ('checkout_datetime', 'user_content_type', 'user_object_id')
|
||||
|
||||
widgets = {
|
||||
'document': forms.widgets.HiddenInput(),
|
||||
}
|
||||
|
||||
def clean_document(self):
|
||||
document = self.cleaned_data['document']
|
||||
if document.is_checked_out():
|
||||
raise DocumentAlreadyCheckedOut
|
||||
return document
|
||||
21
apps/checkouts/links.py
Normal file
21
apps/checkouts/links.py
Normal file
@@ -0,0 +1,21 @@
|
||||
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, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE)
|
||||
|
||||
|
||||
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'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]}
|
||||
16
apps/checkouts/literals.py
Normal file
16
apps/checkouts/literals.py
Normal file
@@ -0,0 +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'),
|
||||
}
|
||||
93
apps/checkouts/managers.py
Normal file
93
apps/checkouts/managers.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
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):
|
||||
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():
|
||||
document.check_in()
|
||||
|
||||
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, user=None):
|
||||
try:
|
||||
document_checkout = self.model.objects.get(document=document)
|
||||
except self.model.DoesNotExist:
|
||||
raise DocumentNotCheckedOut
|
||||
else:
|
||||
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):
|
||||
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):
|
||||
return STATE_CHECKED_OUT
|
||||
else:
|
||||
return STATE_CHECKED_IN
|
||||
|
||||
def is_document_new_versions_allowed(self, document, user=None):
|
||||
try:
|
||||
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
|
||||
|
||||
122
apps/checkouts/migrations/0001_initial.py
Normal file
122
apps/checkouts/migrations/0001_initial.py
Normal file
@@ -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'], 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)),
|
||||
))
|
||||
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', [], {'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'})
|
||||
},
|
||||
'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']
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- 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 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):
|
||||
# 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': {
|
||||
'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', [], {'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'}),
|
||||
'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'"},
|
||||
'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']
|
||||
0
apps/checkouts/migrations/__init__.py
Normal file
0
apps/checkouts/migrations/__init__.py
Normal file
56
apps/checkouts/models.py
Normal file
56
apps/checkouts/models.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
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
|
||||
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__)
|
||||
|
||||
|
||||
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'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.'))
|
||||
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
|
||||
|
||||
objects = DocumentCheckoutManager()
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.document)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.pk:
|
||||
self.checkout_datetime = datetime.datetime.now()
|
||||
result = super(DocumentCheckout, self).save(*args, **kwargs)
|
||||
create_history(HISTORY_DOCUMENT_CHECKED_OUT, source_object=self.document, data={'user': self.user_object, 'document': self.document})
|
||||
return result
|
||||
|
||||
@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')
|
||||
13
apps/checkouts/permissions.py
Normal file
13
apps/checkouts/permissions.py
Normal file
@@ -0,0 +1,13 @@
|
||||
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'))
|
||||
PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE = Permission.objects.register(namespace, 'checkout_restrictions_override', _(u'Allow overriding check out restrictions'))
|
||||
|
||||
BIN
apps/checkouts/static/images/icons/basket_put.png
Normal file
BIN
apps/checkouts/static/images/icons/basket_put.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
apps/checkouts/static/images/icons/basket_remove.png
Normal file
BIN
apps/checkouts/static/images/icons/basket_remove.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
apps/checkouts/static/images/icons/traffic_lights_green.png
Normal file
BIN
apps/checkouts/static/images/icons/traffic_lights_green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
24
apps/checkouts/tasks.py
Normal file
24
apps/checkouts/tasks.py
Normal file
@@ -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
|
||||
8
apps/checkouts/urls.py
Normal file
8
apps/checkouts/urls.py
Normal file
@@ -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<document_pk>\d+)/check/out/$', 'checkout_document', (), 'checkout_document'),
|
||||
url(r'^(?P<document_pk>\d+)/check/in/$', 'checkin_document', (), 'checkin_document'),
|
||||
url(r'^(?P<document_pk>\d+)/check/info/$', 'checkout_info', (), 'checkout_info'),
|
||||
)
|
||||
134
apps/checkouts/views.py
Normal file
134
apps/checkouts/views.py
Normal file
@@ -0,0 +1,134 @@
|
||||
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.core.exceptions import PermissionDenied
|
||||
|
||||
from documents.views import document_list
|
||||
from documents.models import Document
|
||||
|
||||
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
|
||||
from .forms import DocumentCheckoutForm
|
||||
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'),
|
||||
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):
|
||||
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_accesses([PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN], request.user, document)
|
||||
|
||||
paragraphs = [checkout_widget(document)]
|
||||
|
||||
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'Check out time: %s') % checkout_info.checkout_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,
|
||||
'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(data=request.POST, initial={'document': document})
|
||||
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.'))
|
||||
return HttpResponseRedirect(reverse('checkout_info', args=[document.pk]))
|
||||
else:
|
||||
form = DocumentCheckoutForm(initial={'document': document})
|
||||
|
||||
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)
|
||||
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(user=request.user)
|
||||
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 in successfully.') % document)
|
||||
return HttpResponseRedirect(next)
|
||||
|
||||
context = {
|
||||
'object_name': _(u'document'),
|
||||
'delete_view': False,
|
||||
'previous': previous,
|
||||
'next': next,
|
||||
'form_icon': u'basket_remove.png',
|
||||
'object': document,
|
||||
'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))
|
||||
107
apps/checkouts/widgets.py
Normal file
107
apps/checkouts/widgets.py
Normal file
@@ -0,0 +1,107 @@
|
||||
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
|
||||
|
||||
|
||||
def checkout_widget(document):
|
||||
checkout_state = document.checkout_state()
|
||||
|
||||
widget = (u'<img style="vertical-align: middle;" src="%simages/icons/%s" />' % (settings.STATIC_URL, STATE_ICONS[checkout_state]))
|
||||
return _(u'Document status: %(widget)s %(text)s') % {
|
||||
'widget': mark_safe(widget),
|
||||
'text': STATE_LABELS[checkout_state]
|
||||
}
|
||||
|
||||
|
||||
class SplitDeltaWidget(forms.widgets.MultiWidget):
|
||||
"""
|
||||
A Widget that splits a timedelta input into three <input type="text"> 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 <input type="hidden"> 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.'),
|
||||
'invalid_timedelta': _(u'Enter a valid time difference.'),
|
||||
}
|
||||
|
||||
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 == [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 forms.ValidationError(self.error_messages['invalid_days'])
|
||||
if data_list[1] in validators.EMPTY_VALUES:
|
||||
raise forms.ValidationError(self.error_messages['invalid_hours'])
|
||||
if data_list[2] in validators.EMPTY_VALUES:
|
||||
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
|
||||
return None
|
||||
6
apps/documents/exceptions.py
Normal file
6
apps/documents/exceptions.py
Normal file
@@ -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
|
||||
@@ -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()
|
||||
@@ -169,8 +170,11 @@ 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(user=user):
|
||||
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)
|
||||
@@ -520,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()
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -88,11 +101,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', {
|
||||
|
||||
5
apps/permissions/exceptions.py
Normal file
5
apps/permissions/exceptions.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
|
||||
|
||||
|
||||
class PermissionDenied(DjangoPermissionDenied):
|
||||
pass
|
||||
@@ -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()
|
||||
|
||||
@@ -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,8 @@ 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:
|
||||
messages.error(request, _(u'New version uploads are not allowed for this document.'))
|
||||
except Exception, e:
|
||||
if settings.DEBUG:
|
||||
raise
|
||||
@@ -253,6 +256,8 @@ 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:
|
||||
messages.error(request, _(u'New version uploads are not allowed for this document.'))
|
||||
except Exception, e:
|
||||
if settings.DEBUG:
|
||||
raise
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
50
docs/releases/0.12.2.rst
Normal file
50
docs/releases/0.12.2.rst
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user