Merge remote-tracking branch 'origin/feature/document_checkout' into hotfix/v0.12.2

This commit is contained in:
Roberto Rosario
2012-06-17 08:14:28 -04:00
32 changed files with 936 additions and 18 deletions

View File

@@ -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

View 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
View 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'}
}

View 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
View 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
View 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]}

View 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'),
}

View 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

View 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']

View File

@@ -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']

View File

56
apps/checkouts/models.py Normal file
View 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')

View 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'))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

24
apps/checkouts/tasks.py Normal file
View 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
View 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
View 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
View 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

View 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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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', {

View File

@@ -0,0 +1,5 @@
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
class PermissionDenied(DjangoPermissionDenied):
pass

View File

@@ -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()

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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',

View File

@@ -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')),
)