diff --git a/apps/common/templatetags/settings.py b/apps/common/templatetags/settings.py new file mode 100644 index 0000000000..8f5ac4732f --- /dev/null +++ b/apps/common/templatetags/settings.py @@ -0,0 +1,36 @@ +import re + +from django.template import Node, Variable +from django.template import TemplateSyntaxError, Library, VariableDoesNotExist +from django.template.defaultfilters import stringfilter +from django.template.defaultfilters import date as datefilter +from django.conf import settings + +register = Library() + + +class SettingsNode(Node): + def __init__(self, format_string, var_name): + self.format_string = format_string + self.var_name = var_name + def render(self, context): + #context[self.var_name] = settings(self.format_string) + context[self.var_name] = getattr(settings, self.format_string, '') + return '' + + +@register.tag +def get_setting(parser, token): + # This version uses a regular expression to parse tag contents. + try: + # Splitting by None == splitting by spaces. + tag_name, arg = token.contents.split(None, 1) + except ValueError: + raise TemplateSyntaxError, "%r tag requires arguments" % token.contents.split()[0] + m = re.search(r'(.*?) as (\w+)', arg) + if not m: + raise TemplateSyntaxError, "%r tag had invalid arguments" % tag_name + format_string, var_name = m.groups() + if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")): + raise TemplateSyntaxError, "%r tag's argument should be in quotes" % tag_name + return SettingsNode(format_string[1:-1], var_name) diff --git a/apps/documents/__init__.py b/apps/documents/__init__.py index f3b21532a0..e527a75f54 100644 --- a/apps/documents/__init__.py +++ b/apps/documents/__init__.py @@ -12,15 +12,10 @@ document_view = {'text':_('details'), 'view':'document_view', 'args':'object.id' document_delete = {'text':_('delete'), 'view':'document_delete', 'args':'object.id', 'famfam':'page_delete'} document_edit = {'text':_('edit'), 'view':'document_edit', 'args':'object.id', 'famfam':'page_edit'} -#document_create_from_staging = {'text':_('upload file'), 'view':'document_create_from_staging', 'args':{'file_id':'object.id', 'document_type_id': 'document_type_id'}, 'famfam':'page_add'} - - register_links(Document, [document_view, document_edit, document_delete]) register_links(Document, [document_list, document_create, document_create_multiple], menu_name='sidebar') register_links(['document_list', 'document_create', 'document_create_multiple', 'upload_document_with_type', 'upload_multiple_documents_with_type'], [document_list, document_create, document_create_multiple], menu_name='sidebar') -#register_links(StagingFile, [document_create_from_staging]) - register_menu([ {'text':_('documents'), 'view':'document_list', 'links':[ diff --git a/apps/documents/admin.py b/apps/documents/admin.py index 1f8b6248b8..660d8b781b 100644 --- a/apps/documents/admin.py +++ b/apps/documents/admin.py @@ -1,13 +1,20 @@ from django.contrib import admin from models import MetadataType, DocumentType, Document, \ - DocumentTypeMetadataType, DocumentMetadata, DocumentTypeFilename#, \ -# DocumentFile + DocumentTypeMetadataType, DocumentMetadata, DocumentTypeFilename, \ + MetadataIndex, DocumentMetadataIndex class MetadataTypeAdmin(admin.ModelAdmin): - list_display = ('name', 'default', 'lookup') - + list_display = ('name', 'title', 'default', 'lookup') + + +class MetadataIndexInline(admin.StackedInline): + model = MetadataIndex + extra = 1 + classes = ('collapse-open',) + allow_add = True + class DocumentTypeMetadataTypeInline(admin.StackedInline): model = DocumentTypeMetadataType @@ -24,27 +31,28 @@ class DocumentTypeFilenameInline(admin.StackedInline): class DocumentTypeAdmin(admin.ModelAdmin): - inlines = [DocumentTypeMetadataTypeInline, DocumentTypeFilenameInline] + inlines = [DocumentTypeFilenameInline, DocumentTypeMetadataTypeInline, MetadataIndexInline] class DocumentMetadataInline(admin.StackedInline): model = DocumentMetadata + extra = 0 + classes = ('collapse-open',) + allow_add = False + readonly_fields = ('metadata_type', 'value') + + +class DocumentMetadataIndexInline(admin.StackedInline): + model = DocumentMetadataIndex extra = 1 classes = ('collapse-open',) - allow_add = True - - -#class DocumentFileInline(admin.StackedInline): -# model = DocumentFile -# extra = 1 -# classes = ('collapse-open',) -# allow_add = True + allow_add = True + readonly_fields = ('metadata_index', 'filename') class DocumentAdmin(admin.ModelAdmin): - #inlines = [DocumentFileInline]#, DocumentMetadataInline,] - inlines = [DocumentMetadataInline] - list_display = ('uuid',) + inlines = [DocumentMetadataInline, DocumentMetadataIndexInline] + list_display = ('uuid', 'file_filename', 'file_extension') diff --git a/apps/documents/conf/settings.py b/apps/documents/conf/settings.py index 5e0190b46d..9f353c7d42 100644 --- a/apps/documents/conf/settings.py +++ b/apps/documents/conf/settings.py @@ -33,3 +33,4 @@ STORAGE_DIRECTORY_NAME = getattr(settings, 'DOCUMENTS_STORAGE_DIRECTORY_NAME', ' FILESYSTEM_FILESERVING_ENABLE = getattr(settings, 'DOCUMENTS_FILESYSTEM_FILESERVING_ENABLE', True) FILESYSTEM_FILESERVING_PATH = getattr(settings, 'DOCUMENTS_FILESERVING_PATH', u'/tmp/mayan/documents') FILESYSTEM_SLUGIFY_PATHS = getattr(settings, 'DOCUMENTS_SLUGIFY_PATHS', False) +FILESYSTEM_MAX_RENAME_COUNT = getattr(settings, 'DOCUMENTS_FILESYSTEM_MAX_RENAME_COUNT', 200) diff --git a/apps/documents/forms.py b/apps/documents/forms.py index e3c2529db5..c63a0edf7d 100644 --- a/apps/documents/forms.py +++ b/apps/documents/forms.py @@ -1,6 +1,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext from django.http import HttpResponseRedirect from django.utils.http import urlencode from django.core.urlresolvers import reverse @@ -84,7 +85,17 @@ class MetadataForm(forms.Form): self.document_type = kwargs['initial'].pop('document_type', None) self.metadata_options = kwargs['initial'].pop('metadata_options', None) - self.fields['name'].initial=self.metadata_type.name + + required=self.document_type.documenttypemetadatatype_set.get(metadata_type=self.metadata_type).required + required_string = u'' + if required: + self.fields['value'].required=True + required_string = ' (%s)' % ugettext(u'required') + else: + #TODO: FIXME: not working correctly + self.fields['value'].required=False + + self.fields['name'].initial='%s%s' % ((self.metadata_type.title if self.metadata_type.title else self.metadata_type.name), required_string) self.fields['id'].initial=self.metadata_type.id if self.metadata_type.default: try: @@ -97,6 +108,7 @@ class MetadataForm(forms.Form): choices = eval(self.metadata_type.lookup, AVAILABLE_MODELS) self.fields['value'] = forms.ChoiceField(label=self.fields['value'].label) self.fields['value'].choices = zip(choices, choices) + self.fields['value'].required = False except Exception, err: self.fields['value'].initial = err self.fields['value'].widget=forms.TextInput(attrs={'readonly':'readonly'}) @@ -104,7 +116,7 @@ class MetadataForm(forms.Form): id = forms.CharField(label=_(u'id'), widget=forms.HiddenInput) name = forms.CharField(label=_(u'Name'), required=False, widget=forms.TextInput(attrs={'readonly':'readonly'})) - value = forms.CharField(label=_(u'Value')) + value = forms.CharField(label=_(u'Value'), required=False) class DocumentCreateWizard(BoundFormWizard): @@ -138,8 +150,9 @@ class DocumentCreateWizard(BoundFormWizard): if step == 1: self.urldata = [] for id, metadata in enumerate(form.cleaned_data): - self.urldata.append(('metadata%s_id' % id,metadata['id'])) - self.urldata.append(('metadata%s_value' % id,metadata['value'])) + if metadata['value']: + self.urldata.append(('metadata%s_id' % id,metadata['id'])) + self.urldata.append(('metadata%s_value' % id,metadata['value'])) def get_template(self, step): diff --git a/apps/documents/models.py b/apps/documents/models.py index 14d4c29f65..0e8e837a74 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -2,6 +2,7 @@ import errno import os import mimetypes from datetime import datetime +import sys from django.conf import settings from django.db import models @@ -20,6 +21,7 @@ from documents.conf.settings import STORAGE_DIRECTORY_NAME from documents.conf.settings import FILESYSTEM_FILESERVING_ENABLE from documents.conf.settings import FILESYSTEM_FILESERVING_PATH from documents.conf.settings import FILESYSTEM_SLUGIFY_PATHS +from documents.conf.settings import FILESYSTEM_MAX_RENAME_COUNT if FILESYSTEM_SLUGIFY_PATHS == False: @@ -47,7 +49,7 @@ class DocumentType(models.Model): def __unicode__(self): return self.name - + class Document(models.Model): """ Minimum fields for a document entry. @@ -57,6 +59,7 @@ class Document(models.Model): file = models.FileField(upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'file')) uuid = models.CharField(max_length=48, default=UUID_FUNCTION(), blank=True, editable=False) file_mimetype = models.CharField(max_length=64, default='', editable=False) + #FAT filename can be up to 255 using LFN file_filename = models.CharField(max_length=64, default='', editable=False) file_extension = models.CharField(max_length=16, default='', editable=False) date_added = models.DateTimeField(verbose_name=_(u'added'), auto_now_add=True) @@ -69,7 +72,6 @@ class Document(models.Model): ordering = ['-date_updated', '-date_added'] def __unicode__(self): - #return self.uuid return '%s.%s' % (self.file_filename, self.file_extension) @models.permalink @@ -89,61 +91,129 @@ class Document(models.Model): super(Document, self).save(*args, **kwargs) def delete(self, *args, **kwargs): + #TODO: Might not execute when done in bulk from a queryset + #topics/db/queries.html#topics-db-queries-delete self.delete_fs_links() super(Document, self).delete(*args, **kwargs) - def calculate_fs_links(self): - targets = [] - for metadata in self.documentmetadata_set.all(): - if metadata.metadata_type.documenttypemetadatatype_set.all()[0].create_directory_link: - target_directory = os.path.join(FILESYSTEM_FILESERVING_PATH, slugify(metadata.metadata_type.name), slugify(metadata.value)) - targets.append(os.path.join(target_directory, os.extsep.join([slugify(self.file_filename), slugify(self.file_extension)]))) - return targets - def create_fs_links(self): if FILESYSTEM_FILESERVING_ENABLE: - for target in self.calculate_fs_links(): - try: - os.makedirs(os.path.dirname(target)) - except OSError, exc: - if exc.errno == errno.EEXIST: + metadata_dict = {'document':self} + metadata_dict.update(dict([(metadata.metadata_type.name, slugify(metadata.value)) for metadata in self.documentmetadata_set.all()])) + + for metadata_index in self.document_type.metadataindex_set.all(): + if metadata_index.enabled: + try: + fabricated_directory = eval(metadata_index.expression, metadata_dict) + target_directory = os.path.join(FILESYSTEM_FILESERVING_PATH, fabricated_directory) + try: + os.makedirs(target_directory) + except OSError, exc: + if exc.errno == errno.EEXIST: + pass + else: + raise OSError(ugettext(u'Unable to create metadata indexing directory: %s') % exc) + + + next_available_filename(self, metadata_index, target_directory, slugify(self.file_filename), slugify(self.file_extension)) + except NameError: + #Error in eval + #TODO: find way to notify user pass - else: - raise OSError(ugettext(u'Unable to create metadata indexing directory: %s') % exc) - try: - os.symlink(os.path.abspath(self.file.path), target) - except OSError, exc: - if exc.errno == errno.EEXIST: - pass - else: - raise OSError(ugettext(u'Unable to create metadata indexing symbolic link: %s') % exc) - + def delete_fs_links(self): if FILESYSTEM_FILESERVING_ENABLE: - for target in self.calculate_fs_links(): + for document_metadata_index in self.documentmetadataindex_set.all(): try: - os.unlink(target) + os.unlink(document_metadata_index.filename) + document_metadata_index.delete() except OSError, exc: if exc.errno == errno.ENOENT: - pass + #No longer exits, so delete db entry anyway + document_metadata_index.delete() else: raise OSError(ugettext(u'Unable to delete metadata indexing symbolic link: %s') % exc) - + + path, filename = os.path.split(document_metadata_index.filename) + + #Cleanup directory of dead stuff + #Delete siblings that are dead links + try: + for f in os.listdir(path): + filepath = os.path.join(path, f) + if os.path.islink(filepath): + #Get link's source + source = os.readlink(filepath) + if os.path.isabs(source): + if not os.path.exists(source): + #link's source is absolute and doesn't exit + os.unlink(filepath) + else: + os.unlink(os.path.join(path, filepath)) + elif os.path.isdir(filepath): + #is a directory, try to delete it + try: + os.removedirs(path) + except: + pass + except OSError, exc: + pass + #Remove the directory if it is empty + try: + os.removedirs(path) + except: + pass + +def next_available_filename(document, metadata_index, path, filename, extension, suffix=0): + target = filename + if suffix: + target = '_'.join([filename, unicode(suffix)]) + filepath = os.path.join(path, os.extsep.join([target, extension])) + matches=DocumentMetadataIndex.objects.filter(filename=filepath) + if matches.count() == 0: + document_metadata_index = DocumentMetadataIndex( + document=document, metadata_index=metadata_index, + filename=filepath) + try: + os.symlink(os.path.abspath(document.file.path), filepath) + document_metadata_index.save() + except OSError, exc: + if exc.errno == errno.EEXIST: + #This link should not exist, try to delete it + try: + os.unlink(filepath) + #Try again with same suffix + return next_available_filename(document, metadata_index, path, filename, extension, suffix) + except Exception, exc: + raise Exception(ugettext(u'Unable to create symbolic link, filename clash: %s; %s') % (filepath, exc)) + + else: + raise OSError(ugettext(u'Unable to create symbolic link: %s; %s') % (filepath, exc)) + + return filepath + else: + if suffix > FILESYSTEM_MAX_RENAME_COUNT: + raise Exception(ugettext(u'Maximum rename count reached, not creating symbolic link')) + return next_available_filename(document, metadata_index, path, filename, extension, suffix+1) + + available_functions_string = (_(u' Available functions: %s') % ','.join(['%s()' % name for name, function in AVAILABLE_FUNCTIONS.items()])) if AVAILABLE_FUNCTIONS else '' available_models_string = (_(u' Available models: %s') % ','.join([name for name, model in AVAILABLE_MODELS.items()])) if AVAILABLE_MODELS else '' class MetadataType(models.Model): name = models.CharField(max_length=48, verbose_name=_(u'name')) -# title = models.CharField(max_length=48, verbose_name=_(u'title'), blank=True, null=True) + title = models.CharField(max_length=48, verbose_name=_(u'title'), blank=True, null=True) default = models.CharField(max_length=128, blank=True, null=True, - verbose_name=_(u'default'), help_text=_(u'Enter a string to be evaluated.%s') % available_functions_string) + verbose_name=_(u'default'), + help_text=_(u'Enter a string to be evaluated.%s') % available_functions_string) lookup = models.CharField(max_length=128, blank=True, null=True, - verbose_name=_(u'lookup'), help_text=_(u'Enter a string to be evaluated. Example: [user.get_full_name() for user in User.objects.all()].%s') % available_models_string) - #datatype = models. + verbose_name=_(u'lookup'), + help_text=_(u'Enter a string to be evaluated. Example: [user.get_full_name() for user in User.objects.all()].%s') % available_models_string) + #TODO: datatype? def __unicode__(self): -# return self.title if self.title else self.name + #return '%s - %s' % (self.name, self.title if self.title else self.name) return self.name class Meta: @@ -151,25 +221,11 @@ class MetadataType(models.Model): verbose_name_plural = _(u'metadata types') -#class MetadataIndexing(models.Model): -# metadata_type = models.ForeignKey(MetadataType, verbose_name=_(u'metadata type')) -# indexing_string = models.CharField( -# -# -# def __unicode__(self): -# return unicode(self.metadata_type) -# -# class Meta: -# verbose_name = _(u'metadata type') -# verbose_name_plural = _(u'metadata types') - - class DocumentTypeMetadataType(models.Model): document_type = models.ForeignKey(DocumentType, verbose_name=_(u'document type')) metadata_type = models.ForeignKey(MetadataType, verbose_name=_(u'metadata type')) - create_directory_link = models.BooleanField(verbose_name=_(u'create directory link')) - #override default - #required? -bool + required = models.BooleanField(default=True, verbose_name=_(u'required')) + #TODO: override default for this document type def __unicode__(self): return unicode(self.metadata_type) @@ -179,6 +235,35 @@ class DocumentTypeMetadataType(models.Model): verbose_name_plural = _(u'document type metadata type connectors') +class MetadataIndex(models.Model): + document_type = models.ForeignKey(DocumentType, verbose_name=_(u'document type')) + expression = models.CharField(max_length=128, + verbose_name=_(u'indexing expression'), + help_text=_(u'Enter a python string expression to be evaluated. The slash caracter "/" acts as a directory delimiter.')) + enabled = models.BooleanField(default=True, verbose_name=_(u'enabled')) + + def __unicode__(self): + return unicode(self.expression) + + class Meta: + verbose_name = _(u'metadata index') + verbose_name_plural = _(u'metadata indexes') + + +class DocumentMetadataIndex(models.Model): + document = models.ForeignKey(Document, verbose_name=_(u'document')) + metadata_index = models.ForeignKey(MetadataIndex, verbose_name=_(u'metadata index')) + filename = models.CharField(max_length=128, verbose_name=_(u'filename')) + suffix = models.PositiveIntegerField(default=0, verbose_name=_(u'suffix')) + + def __unicode__(self): + return unicode(self.filename) + + class Meta: + verbose_name = _(u'document metadata index') + verbose_name_plural = _(u'document metadata indexes') + + class DocumentMetadata(models.Model): document = models.ForeignKey(Document, verbose_name=_(u'document')) metadata_type = models.ForeignKey(MetadataType, verbose_name=_(u'metadata type')) diff --git a/apps/documents/staging.py b/apps/documents/staging.py index 0ceeb31d29..3df19d8c30 100644 --- a/apps/documents/staging.py +++ b/apps/documents/staging.py @@ -11,7 +11,7 @@ from django.utils.translation import ugettext from documents.conf.settings import STAGING_DIRECTORY HASH_FUNCTION = lambda x: hashlib.sha256(x).hexdigest() - +#TODO: Do benchmarks #func = lambda:[StagingFile.get_all() is None for i in range(100)] #t1=time.time();func();t2=time.time();print '%s took %0.3f ms' % (func.func_name, (t2-t1)*1000.0) diff --git a/apps/documents/urls.py b/apps/documents/urls.py index 52e3008830..163ee75be4 100644 --- a/apps/documents/urls.py +++ b/apps/documents/urls.py @@ -12,5 +12,4 @@ urlpatterns = patterns('documents.views', url(r'^document/(?P\d+)/$', 'document_view', (), 'document_view'), url(r'^document/(?P\d+)/delete/$', 'document_delete', (), 'document_delete'), url(r'^document/(?P\d+)/edit/$', 'document_edit', (), 'document_edit'), - #url(r'^document/type/(?P\d+)/upload/from/staging/(?P\d+)/single/$', 'document_create_from_staging', {'multiple':True}, 'document_create_from_staging'), ) diff --git a/apps/documents/views.py b/apps/documents/views.py index a2bb9430f7..eee4c4351f 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -20,6 +20,8 @@ from staging import StagingFile from documents.conf.settings import DELETE_STAGING_FILE_AFTER_UPLOAD from documents.conf.settings import USE_STAGING_DIRECTORY +from documents.conf.settings import FILESYSTEM_FILESERVING_ENABLE + def document_list(request): return object_list( @@ -29,8 +31,6 @@ def document_list(request): extra_context={ 'title':_(u'documents'), 'extra_columns':[ - #{'name':_(u'filename'), 'attribute':'file_filename'}, - #{'name':_(u'extension'), 'attribute':'file_extension'}, {'name':_(u'mimetype'), 'attribute':'file_mimetype'}, {'name':_(u'added'), 'attribute':lambda x: x.date_added.date()}, ], @@ -189,10 +189,7 @@ def document_view(request, document_id): {'label':_(u'Exists in storage'), 'field':'exists'} ]) - return render_to_response('generic_detail.html', { - 'form':form, - 'object':document, - 'subtemplates_dict':[ + subtemplates_dict = [ { 'name':'generic_list_subtemplate.html', 'title':_(u'metadata'), @@ -200,7 +197,19 @@ def document_view(request, document_id): 'extra_columns':[{'name':_(u'value'), 'attribute':'value'}], 'hide_link':True, }, - ], + ] + + if FILESYSTEM_FILESERVING_ENABLE: + subtemplates_dict.append({ + 'name':'generic_list_subtemplate.html', + 'title':_(u'index links'), + 'object_list':document.documentmetadataindex_set.all(), + 'hide_link':True}) + + return render_to_response('generic_detail.html', { + 'form':form, + 'object':document, + 'subtemplates_dict':subtemplates_dict, }, context_instance=RequestContext(request)) @@ -255,39 +264,3 @@ def document_edit(request, document_id): 'object':document, }, context_instance=RequestContext(request)) - -''' -def document_create_from_staging(request, file_id, document_type_id, multiple=True): - if USE_STAGING_DIRECTORY: - document_type = get_object_or_404(DocumentType, pk=document_type_id) - staging_file = StagingFile.get(id=int(file_id)) - - try: - document = Document(file=staging_file.upload(), document_type=document_type) - document.save() - except Exception, e: - messages.error(request, e) - else: - url = urlparse(request.META['HTTP_REFERER']) - #Take the url parameter defining the metadata values and turn - # then into a dictionary - params = dict([part.split('=') for part in url[4].split('&')]) - _save_metadata(params, document) - messages.success(request, _(u'Staging file: %s, uploaded successfully.') % staging_file.filename) - try: - document.create_fs_links() - except Exception, e: - messages.error(request, e) - - if DELETE_STAGING_FILE_AFTER_UPLOAD: - try: - staging_file.delete() - messages.success(request, _(u'Staging file: %s, deleted successfully.') % staging_file.filename) - except Exception, e: - messages.error(request, e) - - if multiple: - return HttpResponseRedirect(request.META['HTTP_REFERER']) - else: - return HttpResponseRedirect(reverse('document_list')) -''' diff --git a/apps/main/templates/base.html b/apps/main/templates/base.html index dbb2f618f2..317d89d834 100755 --- a/apps/main/templates/base.html +++ b/apps/main/templates/base.html @@ -2,6 +2,7 @@ {% load i18n %} {% load project_tags %} {% load navigation %} +{% load settings %} {% block html_title %}{% project_name %}{% block title %}{% endblock %}{% endblock %} @@ -48,7 +49,8 @@ -
  • {% if user.is_anonymous %}{% trans 'Login' %}{% else %}{% trans 'Logout' %}{% endif %}
  • + {% get_setting "LOGIN_URL" as login_url %} +
  • {% if user.is_anonymous %}{% trans 'Login' %}{% else %}{% trans 'Logout' %}{% endif %}
  • {% endblock %} {% block web_theme_main_navigation %} @@ -107,6 +109,7 @@ {% block sidebar %}{% endblock %} {% endblock %} +{% block web_theme_messages %}{% block messages %}{% endblock %}{% endblock %} {% block web_theme_content %}{% block content %}{% endblock %}{% endblock %} diff --git a/docs/TODO b/docs/TODO index 0732a90419..adb36ffcd4 100644 --- a/docs/TODO +++ b/docs/TODO @@ -10,6 +10,8 @@ * Jquery upload document upload form with ajax widget - NOT NEEDED (commit: b0f31f2a8f82ff0daca081005f2fcae3f5573df5) * Rename dropbox from document edit view - DONE * Ability to rename staging file during upload - DONE +* Implement single sign on or LDAP for intranets - DEFERRED, provided by Django AuthBackends +* Database storage backend (sql, nosql: [mongodb]) - DEFERRED, provided by https://bitbucket.org/david/django-storages/wiki/Home * Document list filtering by metadata * Filterform date filtering widget * Validate GET data before saving file @@ -20,13 +22,25 @@ * Show last 5 recent metadata setups for easy switch * Change to use model signals * Allow document type to be changed in document edit view -* Implement single sign on or LDAP for intranets * Encrypting storage backend * Indicate in generic list which document don't exist in storage backend -* Database storage backend (sql, nosql: [mongodb]) * Add css grids * Document previews * Recognize multi-page documents * Staging file previews * Autodelete empty fs directories * Auto check and delete dead sym links +* Document model's delete method might not get called when deleting in bulk + from a queryset +* Allow metadata entry form to mix required and non required metadata +* Link to delete and recreate all document links +* MuliThreading deferred OCR +* Document previews on demand w/ imagemagick +* Versioning support +* Generic document anotations using layer overlays +* Permissions +* Roles +* Workflows +* Scheduled maintenance (cleanup, deferred OCR's) +* Show document metadata in document list +* Show abbreviated uuid in document list diff --git a/settings.py b/settings.py index c7befe6700..db1b85bb02 100644 --- a/settings.py +++ b/settings.py @@ -181,6 +181,7 @@ LOGIN_EXEMPT_URLS = ( #DOCUMENTS_FILESYSTEM_FILESERVING_ENABLE = True #DOCUMENTS_FILESYSTEM_FILESERVING_PATH = u'/tmp/mayan/documents' #DOCUMENTS_FILESYSTEM_SLUGIFY_PATHS = False +#DOCUMENTS_FILESYSTEM_MAX_RENAME_COUNT = 200 #======== End of configuration options ======= try: