Merge branch 'pfwb' into stable

This commit is contained in:
Benjamin Dauvergne 2014-01-31 16:09:30 +01:00
commit c6e6f54597
30 changed files with 1292 additions and 327 deletions

View File

@ -190,12 +190,24 @@ one is potentially localized, for example to add a link to google, add this line
Settings
--------
All settings must be donne in the file ``/etc/docbow/local_settings.py``. Available settings arr:
All settings must be donne in the file ``/etc/docbow/local_settings.py``. Available settings are:
* ``DOCBOW_ORGANIZATION``: an unicode string giving a description of the
organization providing the platform. It's used as a signature in mail and
sms notifications.
* ``DOCBOW_BASE_URL``: the base URL of the application. It's used for building
URL in notifications, emails or SMS.
* ``DOCBOW_MENU``: description of the left column menu; see previous section
for a description and the default value.
* ``DOCBOW_MAILBOX_PER_PAGE``: the number of message to show on listing pages.
Default is 20.
* ``RAVEN_CONFIG_DSN``: the URL of the sentry project for gathering exceptions.
* ``DOCBOW_MAX_FILE_SIZE``: the maximum file size for attached files, as
bytes. Default is 10 Mo.
* ``DOCBOW_TRUNCATE_FILENAME``: the maximum length for filenames. Default is
80 unicode characters (codepoints).
* ``DOCBOW_TIMESTAMP_PROVIDER``: the timestamp provider to use. Default is certum.
Other possibilities are fedit and e_szigno.
Customizing templates
---------------------

View File

@ -55,6 +55,9 @@ class MailingListAdmin(admin.ModelAdmin):
actions = [ actions.export_as_csv ]
def get_actions(self, request):
'''Show delete actions only if user has delete rights
Show activation actions only if user has rights to change mailing lists
'''
a = super(MailingListAdmin, self).get_actions(request)
if request.user.has_perm('docbow.delete_mailinglist'):
a['delete_selected'] = self.get_action('delete_selected')
@ -131,7 +134,7 @@ class DocbowGroupAdmin(auth_admin.GroupAdmin):
class MailboxAdmin(admin.ModelAdmin):
list_display = [ 'owner', 'document', 'date', 'seen' ]
list_display = [ 'owner', 'document', 'date' ]
list_filter = [ 'owner', 'outbox' ]
def lookup_allowed(self, *args, **kwargs):
@ -140,9 +143,9 @@ class MailboxAdmin(admin.ModelAdmin):
class InboxAdmin(MailboxAdmin):
list_display = [ 'date', 'owner', 'document', 'seen', 'deleted' ]
fields = [ 'date', 'owner', 'document', 'seen', 'deleted' ]
readonly_fields = [ 'date', 'owner', 'document', 'seen' ]
list_display = [ 'date', 'owner', 'document' ]
fields = [ 'date', 'owner', 'document' ]
readonly_fields = [ 'date', 'owner', 'document' ]
def queryset(self, request):
'''Only show input mailboxes'''

View File

@ -36,6 +36,10 @@ class AppSettings(object):
def TRUNCATE_FILENAME(self):
return getattr(self.settings, 'DOCBOW_TRUNCATE_FILENAME', 80)
@property
def MAX_FILE_SIZE(self):
return getattr(self.settings, 'DOCBOW_MAX_FILE_SIZE', 10*1024*1024)
import sys
app_settings = AppSettings()

View File

@ -13,7 +13,11 @@ def order_choices(choices):
def order_field_choices(field):
'''Order choices of this field'''
field.choices = order_choices(field.choices)
choices = list(field.choices)
field.choices = [choice for choice in choices if choice[1].startswith('---')] \
+ order_choices([choice for choice in field.choices if not choice[1].startswith('---')])
print field.choices
class Func2Iter(object):
'''Transform a generator producing function into an iterator'''

View File

@ -30,6 +30,7 @@ from . import notification
from .validators import phone_normalize, validate_fr_be_phone
from .middleware import get_extra
from .utils import mime_types_to_extensions, truncate_filename
from . import fields
logger = logging.getLogger(__name__)
@ -81,7 +82,8 @@ class ForwardingForm(RecipientForm, Form):
self.default_sender = self.user
delegations = User.objects.filter(
Q(id=self.user.id) |
Q(delegations_to__to=self.user)).distinct()
Q(delegations_to__to=self.user)) \
.order_by('last_name', 'first_name', 'username').distinct()
super(ForwardingForm, self).__init__(*args, **kwargs)
if len(delegations) > 1:
self.fields['sender'].queryset = delegations
@ -173,6 +175,7 @@ class FileForm(RecipientForm, ModelForm):
if len(self.delegations) > 1:
self.fields['sender'].queryset = self.delegations
self.fields['sender'].label_from_instance = lambda y: username(y)
fields.order_field_choices(self.fields['sender'])
else:
self.layout.fields.remove('sender')
del self.fields['sender']

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,187 @@
# -*- 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):
# Deleting field 'Mailbox.seen'
db.delete_column(u'docbow_mailbox', 'seen')
def backwards(self, orm):
# Adding field 'Mailbox.seen'
db.add_column(u'docbow_mailbox', 'seen',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'ordering': "['username']", '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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'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'}),
u'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'})
},
u'docbow.attachedfile': {
'Meta': {'object_name': 'AttachedFile'},
'content': ('django.db.models.fields.files.FileField', [], {'max_length': '300'}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attached_files'", 'to': u"orm['docbow.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'kind': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.FileTypeAttachedFileKind']", 'null': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '300'})
},
u'docbow.automaticforwarding': {
'Meta': {'object_name': 'AutomaticForwarding'},
'filetypes': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'forwarding_rules'", 'symmetrical': 'False', 'to': u"orm['docbow.FileType']"}),
'forward_to_list': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'as_recipient_forwarding_rules'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['docbow.MailingList']"}),
'forward_to_user': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'as_recipient_forwarding_rules'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['auth.User']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'originaly_to_user': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'as_original_recipient_forwarding_rules'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['auth.User']"})
},
u'docbow.content': {
'Meta': {'ordering': "['description']", 'object_name': 'Content'},
'description': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
u'docbow.delegation': {
'Meta': {'ordering': "['by']", 'unique_together': "(('by', 'to'),)", 'object_name': 'Delegation', 'db_table': "'auth_delegation'"},
'by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'delegations_to'", 'to': u"orm['auth.User']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'to': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'delegations_by'", 'to': u"orm['auth.User']"})
},
u'docbow.deleteddocument': {
'Meta': {'ordering': "('-document',)", 'object_name': 'DeletedDocument'},
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
},
u'docbow.deletedmailbox': {
'Meta': {'object_name': 'DeletedMailbox'},
'delegate': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.Mailbox']"})
},
u'docbow.docbowprofile': {
'Meta': {'object_name': 'DocbowProfile'},
'accept_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_guest': ('django.db.models.fields.BooleanField', [], {}),
'mobile_phone': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
'personal_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
u'docbow.document': {
'Meta': {'ordering': "['-date']", 'object_name': 'Document'},
'_timestamp': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'filetype': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.FileType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'real_sender': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': u"orm['docbow.Document']"}),
'sender': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'documents_sent'", 'to': u"orm['auth.User']"}),
'to_list': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['docbow.MailingList']", 'null': 'True', 'blank': 'True'}),
'to_user': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'directly_received_documents'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['auth.User']"})
},
u'docbow.documentforwarded': {
'Meta': {'object_name': 'DocumentForwarded'},
'automatic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'from_document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'document_forwarded_to'", 'to': u"orm['docbow.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'to_document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'document_forwarded_from'", 'to': u"orm['docbow.Document']"})
},
u'docbow.filetype': {
'Meta': {'ordering': "['name']", 'object_name': 'FileType'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'})
},
u'docbow.filetypeattachedfilekind': {
'Meta': {'ordering': "('file_type', 'position', 'name')", 'unique_together': "(('name', 'file_type'),)", 'object_name': 'FileTypeAttachedFileKind'},
'cardinality': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
'file_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.FileType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mime_types': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'minimum': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {})
},
u'docbow.mailbox': {
'Meta': {'ordering': "['-date']", 'object_name': 'Mailbox'},
'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'deleted': ('django.db.models.fields.BooleanField', [], {}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'mailboxes'", 'to': u"orm['docbow.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'outbox': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'documents'", 'to': u"orm['auth.User']"})
},
u'docbow.mailinglist': {
'Meta': {'ordering': "['name']", 'object_name': 'MailingList'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'mailing_list_members': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'members_lists'", 'blank': 'True', 'to': u"orm['docbow.MailingList']"}),
'members': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'mailing_lists'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['auth.User']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
},
u'docbow.notification': {
'Meta': {'ordering': "('-id',)", 'object_name': 'Notification'},
'create_dt': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'ctx': ('picklefield.fields.PickledObjectField', [], {'null': 'True', 'blank': 'True'}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.Document']", 'null': 'True', 'blank': 'True'}),
'done': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'failure': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'kind': ('django.db.models.fields.CharField', [], {'default': "'new-document'", 'max_length': '32'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'docbow.seendocument': {
'Meta': {'ordering': "('-document',)", 'object_name': 'SeenDocument'},
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
},
u'docbow.sendinglimitation': {
'Meta': {'object_name': 'SendingLimitation'},
'filetypes': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'filetype_limitation'", 'blank': 'True', 'to': u"orm['docbow.FileType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lists': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'lists_limitation'", 'symmetrical': 'False', 'to': u"orm['docbow.MailingList']"}),
'mailing_list': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['docbow.MailingList']", 'unique': 'True'})
}
}
complete_apps = ['docbow']

View File

@ -0,0 +1,186 @@
# -*- 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):
# Deleting field 'Mailbox.deleted'
db.delete_column(u'docbow_mailbox', 'deleted')
def backwards(self, orm):
# Adding field 'Mailbox.deleted'
db.add_column(u'docbow_mailbox', 'deleted',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'ordering': "['username']", '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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'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'}),
u'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'})
},
u'docbow.attachedfile': {
'Meta': {'object_name': 'AttachedFile'},
'content': ('django.db.models.fields.files.FileField', [], {'max_length': '300'}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attached_files'", 'to': u"orm['docbow.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'kind': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.FileTypeAttachedFileKind']", 'null': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '300'})
},
u'docbow.automaticforwarding': {
'Meta': {'object_name': 'AutomaticForwarding'},
'filetypes': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'forwarding_rules'", 'symmetrical': 'False', 'to': u"orm['docbow.FileType']"}),
'forward_to_list': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'as_recipient_forwarding_rules'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['docbow.MailingList']"}),
'forward_to_user': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'as_recipient_forwarding_rules'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['auth.User']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'originaly_to_user': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'as_original_recipient_forwarding_rules'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['auth.User']"})
},
u'docbow.content': {
'Meta': {'ordering': "['description']", 'object_name': 'Content'},
'description': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
u'docbow.delegation': {
'Meta': {'ordering': "['by']", 'unique_together': "(('by', 'to'),)", 'object_name': 'Delegation', 'db_table': "'auth_delegation'"},
'by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'delegations_to'", 'to': u"orm['auth.User']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'to': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'delegations_by'", 'to': u"orm['auth.User']"})
},
u'docbow.deleteddocument': {
'Meta': {'ordering': "('-document',)", 'object_name': 'DeletedDocument'},
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
},
u'docbow.deletedmailbox': {
'Meta': {'object_name': 'DeletedMailbox'},
'delegate': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.Mailbox']"})
},
u'docbow.docbowprofile': {
'Meta': {'object_name': 'DocbowProfile'},
'accept_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_guest': ('django.db.models.fields.BooleanField', [], {}),
'mobile_phone': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
'personal_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
u'docbow.document': {
'Meta': {'ordering': "['-date']", 'object_name': 'Document'},
'_timestamp': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'filetype': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.FileType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'real_sender': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': u"orm['docbow.Document']"}),
'sender': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'documents_sent'", 'to': u"orm['auth.User']"}),
'to_list': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['docbow.MailingList']", 'null': 'True', 'blank': 'True'}),
'to_user': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'directly_received_documents'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['auth.User']"})
},
u'docbow.documentforwarded': {
'Meta': {'object_name': 'DocumentForwarded'},
'automatic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'from_document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'document_forwarded_to'", 'to': u"orm['docbow.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'to_document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'document_forwarded_from'", 'to': u"orm['docbow.Document']"})
},
u'docbow.filetype': {
'Meta': {'ordering': "['name']", 'object_name': 'FileType'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'})
},
u'docbow.filetypeattachedfilekind': {
'Meta': {'ordering': "('file_type', 'position', 'name')", 'unique_together': "(('name', 'file_type'),)", 'object_name': 'FileTypeAttachedFileKind'},
'cardinality': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
'file_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.FileType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mime_types': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'minimum': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {})
},
u'docbow.mailbox': {
'Meta': {'ordering': "['-date']", 'object_name': 'Mailbox'},
'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'mailboxes'", 'to': u"orm['docbow.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'outbox': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'documents'", 'to': u"orm['auth.User']"})
},
u'docbow.mailinglist': {
'Meta': {'ordering': "['name']", 'object_name': 'MailingList'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'mailing_list_members': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'members_lists'", 'blank': 'True', 'to': u"orm['docbow.MailingList']"}),
'members': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'mailing_lists'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['auth.User']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
},
u'docbow.notification': {
'Meta': {'ordering': "('-id',)", 'object_name': 'Notification'},
'create_dt': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'ctx': ('picklefield.fields.PickledObjectField', [], {'null': 'True', 'blank': 'True'}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.Document']", 'null': 'True', 'blank': 'True'}),
'done': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'failure': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'kind': ('django.db.models.fields.CharField', [], {'default': "'new-document'", 'max_length': '32'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'docbow.seendocument': {
'Meta': {'ordering': "('-document',)", 'object_name': 'SeenDocument'},
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docbow.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
},
u'docbow.sendinglimitation': {
'Meta': {'object_name': 'SendingLimitation'},
'filetypes': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'filetype_limitation'", 'blank': 'True', 'to': u"orm['docbow.FileType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lists': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'lists_limitation'", 'symmetrical': 'False', 'to': u"orm['docbow.MailingList']"}),
'mailing_list': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['docbow.MailingList']", 'unique': 'True'})
}
}
complete_apps = ['docbow']

View File

@ -385,9 +385,12 @@ class Document(Model):
blob['files'].append(d)
return blob
def sender_display(self):
return username(self.sender)
def url(self):
return urlparse.urljoin(app_settings.BASE_URL,
reverse('inbox', kwargs=dict(mailbox_id=self.id)))
reverse('inbox-message', kwargs=dict(mailbox_id=self.id)))
class DeletedMailbox(Model):
@ -647,8 +650,6 @@ class Mailbox(Model):
related_name='documents')
document = ForeignKey(Document, verbose_name=('Document'),
related_name='mailboxes')
seen = BooleanField(verbose_name=_('Seen'), blank=True)
deleted = BooleanField(verbose_name=_('Deleted'), blank=True)
outbox = BooleanField(verbose_name=_('Outbox message'), blank=True,
default=False, db_index=True)
date = DateTimeField(auto_now_add=True)
@ -659,26 +660,10 @@ class Mailbox(Model):
verbose_name_plural = _('Mailboxes')
def __unicode__(self):
if self.seen and self.deleted:
return _(u'seen and deleted mailbox entry {id} of user {user}:{user.id} created on '
u'{date} for {document}').format(id=self.id, user=self.owner,
date=self.date,
document=self.document)
elif self.deleted:
return _(u'deleted mailbox entry {id} of user {user}:{user.id} created on '
u'{date} for {document}').format(id=self.id, user=self.owner,
date=self.date,
document=self.document)
elif self.seen:
return _(u'seen mailbox entry {id} of user {user}:{user.id} created on '
u'{date} for {document}').format(id=self.id, user=self.owner,
date=self.date,
document=self.document)
else:
return _(u'mailbox entry {id} of user {user}:{user.id} created on '
u'{date} for {document}').format(id=self.id, user=self.owner,
date=self.date,
document=self.document)
return _(u'mailbox entry {id} of user {user}:{user.id} created on '
u'{date} for {document}').format(id=self.id, user=self.owner,
date=self.date,
document=self.document)
class DocbowUser(User):
class Meta:
@ -734,7 +719,8 @@ class SendingLimitation(Model):
class DocbowProfile(Model):
'''Hold extra user attributes'''
user = OneToOneField(User, unique=True)
is_guest = BooleanField(verbose_name=_('Guest user'), blank=True)
is_guest = BooleanField(verbose_name=_('Guest user'), blank=True,
default=False)
mobile_phone = CharField(max_length=32, verbose_name=_('Mobile phone'),
blank=True, validators=[validate_phone])
personal_email = EmailField(_('personal email address'), blank=True,

View File

@ -131,7 +131,8 @@ def get_notifiers():
def process_notifications():
notifiers = get_notifiers()
for notification in models.Notification.objects.select_for_update().filter(done=False):
for notification in models.Notification.objects.order_by('id') \
.select_for_update().filter(done=False):
for notifier in notifiers:
failures = []
try:

View File

@ -0,0 +1,66 @@
from django.db import connection
def get_sql(sql, params):
'''
Execute an SQL query and return the associated cursor.
'''
cursor = connection.cursor()
cursor.execute(sql, params)
return cursor
def get_sql_count(sql, params):
'''
Execute a count on SUB-SELECT and return the count.
'''
cursor = get_sql('''SELECT COUNT(*) FROM (%s) AS CNT''' % sql, params)
return cursor.fetchone()[0]
def get_sql_ids(sql, params):
'''
Retrieve a list of numerical ids.
'''
cursor = get_sql(sql, params)
return (row[0] for row in cursor.fetchall())
def get_complex_join(qs, sql, params):
ids = list(get_sql_ids(sql, params))
return qs.filter(pk__in=ids)
def get_unseen_documents_count(related_users, user):
return get_sql_count(GET_UNSEEN_DOCUMENTS_SQL,
(
tuple(related_users.values_list('id', flat=True)),
user.pk,
))
def get_documents(qs, related_users, user, outbox):
qs = get_complex_join(qs,
GET_DOCUMENTS_SQL,
( outbox, tuple(related_users.values_list('id', flat=True)), ))
qs = qs.prefetch_related('to_list', 'to_user', 'mailboxes__owner')
qs = qs.extra(select={ 'seen': SEEN_DOCUMENT % user.pk })
return qs
GET_UNSEEN_DOCUMENTS_SQL = '''SELECT d.id
FROM docbow_document AS d
INNER JOIN docbow_mailbox AS mb
ON mb.outbox = FALSE AND mb.document_id = d.id AND mb.owner_id IN %s
LEFT JOIN docbow_seendocument as sd
ON sd.document_id = d.id AND sd.user_id = %s
WHERE sd.id IS NULL
GROUP BY d.id, d.date
ORDER BY d.date
'''
GET_DOCUMENTS_SQL = '''SELECT d.id
FROM docbow_document AS d
INNER JOIN docbow_mailbox AS mb ON
mb.outbox = %s AND mb.document_id = d.id AND mb.owner_id IN %s
GROUP BY d.id, d.date
ORDER BY d.date'''
SEEN_DOCUMENT = '''SELECT COUNT(*) > 0
FROM docbow_seendocument
WHERE
docbow_seendocument.document_id = docbow_document.id
and docbow_seendocument.user_id = %s'''

View File

@ -135,8 +135,8 @@
$(this).fadeOut(function () {
if (data.errorThrown !== 'abort') {
var file = data.files[index];
file.error = file.error || data.errorThrown
|| true;
file.error = file.error || 'httpError'
|| 'unknownError';
that._renderDownload([file])
.css('display', 'none')
.replaceAll(this)
@ -639,4 +639,4 @@
});
}(jQuery));
}(jQuery));

View File

@ -103,7 +103,7 @@ class InboxTable(tables.Table):
<span>{{ recipient_user|username }}</span>{% if not forloop.last %},{% endif %}
{% endfor %}''', orderable=False, verbose_name=_('recipients_header'))
sender = tables.Column(
accessor='sender', verbose_name=_('sender_header'))
accessor='sender_display', verbose_name=_('sender_header'))
#date = tables.Column(
# accessor='date', verbose_name=_('date_header'))
date = tables.TemplateColumn('{% load humantime %}{{ record.date|humantime }}',

View File

@ -24,11 +24,13 @@
<td class="name">${name}</td>
<td class="size">${sizef}</td>
{{open_tv}}if error{{close_tv}}
<td class="error" colspan="2">Error:
{{open_tv}}if error === 'maxFileSize'{{close_tv}}File is too big
{{open_tv}}else error === 'minFileSize'{{close_tv}}File is too small
{{open_tv}}else error === 'acceptFileTypes'{{close_tv}}Filetype not allowed
{{open_tv}}else error === 'maxNumberOfFiles'{{close_tv}}Max number of files exceeded
<td class="error" colspan="2">{% trans "Error" %}&nbsp;:
{{open_tv}}if error === 'maxFileSize'{{close_tv}}{% blocktrans %}File is too big, limit is {{ max_file_size }} bytes{% endblocktrans %}
{{open_tv}}else error === 'minFileSize'{{close_tv}}{% trans "File is too small" %}
{{open_tv}}else error === 'acceptFileTypes'{{close_tv}}{% trans "Filetype not allowed" %}
{{open_tv}}else error === 'maxNumberOfFiles'{{close_tv}}{% trans "Max number of files exceeded" %}
{{open_tv}}else error === 'httpError'{{close_tv}}{% trans "HTTP Error" %}
{{open_tv}}else error === 'unknownError'{{close_tv}}{% trans "Unknown error" %}
{{open_tv}}else{{close_tv}}${error}
{{open_tv}}/if{{close_tv}}
</td>
@ -83,7 +85,7 @@
<script type="text/javascript">
$(function () {
'use strict';
$('#{{name}}-fileupload').fileupload({url: '{{upload_url}}', autoUpload: true, dropZone: $(document)});
$('#{{name}}-fileupload').fileupload({url: '{{upload_url}}', maxFileSize: {{max_file_size}}, autoUpload: true, dropZone: $(document)});
$.getJSON('{{ upload_url }}', function (files) {
var fu = $('#{{name}}-fileupload').data('fileupload');
fu._adjustMaxNumberOfFiles(-files.length);

View File

@ -4,9 +4,7 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from .. import views
from .. import models
from .. import app_settings
from .. import views, models, app_settings, sql
register = template.Library()
@ -46,9 +44,8 @@ def menu(context):
count = 0
current = False
if view_name == 'inbox':
seen_documents = models.SeenDocument.objects.filter(user=request.user) \
.values_list('document_id', flat=True)
count = views.get_documents(request).exclude(id__in=seen_documents).count()
related_users = views.get_related_users(request)
count = sql.get_unseen_documents_count(related_users, request.user)
# remove delegation view for guest accounts
if models.is_guest(context['user']) and 'delegate' in view_name:
continue

View File

@ -92,13 +92,12 @@ class BaseTestCase(TestCase):
User.objects.create(username='user-%s' % i,
email='user-%s@example.com' % i))
DocbowProfile.objects.create(user=self.users[-1],
personal_email='personal-email-user-%s@example.com')
personal_email='personal-email-user-%s@example.com' % i)
self.filetypes = []
for i in range(10):
self.filetypes.append(
FileType.objects.create(name='filetype-%s' % i))
self.documents = []
self.files = []
for i in range(10):
self.documents.append(
Document.objects.create(sender=self.users[(i+2) % 10],
@ -106,7 +105,6 @@ class BaseTestCase(TestCase):
self.documents[-1].to_user = [self.users[i % 10],
self.users[(i+1) % 10]]
for j in range(2):
self.files.append(tempfile.NamedTemporaryFile())
attached_file = AttachedFile(name='file%s' % j,
document=self.documents[-1],
kind=None)
@ -114,6 +112,23 @@ class BaseTestCase(TestCase):
attached_file.save()
self.documents[-1].post()
class BasicTestCase(BaseTestCase):
def test_notification_mail(self):
from django.core import mail
from django.core import management
import re
management.call_command('notify')
self.assertEquals(len(mail.outbox), 20)
outbox = sorted(mail.outbox, key=lambda m: tuple(sorted(m.to)))
MAIL_LINK_RE = re.compile('https?://[^/]+/inbox/\d+/')
for message, i in zip(outbox, range(20)):
recipient = self.users[i / 2]
emails = [recipient.docbowprofile.personal_email, recipient.email]
self.assertEquals(set(message.to), set(emails))
match = MAIL_LINK_RE.search(message.body)
self.assertIsNotNone(match)
class UtilsTestCase(BaseTestCase):
def setUp(self):
super(UtilsTestCase, self).setUp()

View File

@ -11,6 +11,7 @@ from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from .models import FileTypeAttachedFileKind
from . import app_settings
def get_paths_for_id(upload_id):
storage = DefaultStorage()
@ -70,6 +71,11 @@ def upload(request, transaction_id, file_kind=None):
if request.method == 'POST' and request.FILES is not None:
data = []
for uploaded_file in request.FILES.values():
if uploaded_file.size > app_settings.MAX_FILE_SIZE:
message = _('File is too big, limit is %(max_file_size)s bytes')
message = message % {'max_file_size': app_settings.MAX_FILE_SIZE}
data.append({'name': uploaded_file.name, 'error': unicode(message) })
continue
if file_kind:
if not file_kind.match_file(uploaded_file):
message = _('invalid file type, check required '

View File

@ -39,6 +39,7 @@ from . import app_settings
from . import unicodecsv
from . import profile_views
from .utils import date_to_aware_datetime
from . import sql
gettext_noop = lambda x: x
@ -145,11 +146,13 @@ def send_file(request, file_type_id):
except KeyError:
reply_to = None
if hasattr(request.user, 'delegate'):
delegators = []
delegators = User.objects.none()
else:
delegators = User.objects.filter(
Q(id=request.user.id) |
Q(delegations_to__to=request.user)).distinct()
Q(delegations_to__to=request.user)) \
.order_by('last_name', 'first_name', 'username') \
.distinct()
real_user = getattr(request.user, 'delegate', request.user)
limitations = get_filetype_limitation(request.user)
if limitations:
@ -519,30 +522,16 @@ def get_related_users(request):
delegations = request.user.delegations_by.select_related('by')
if delegations:
users.extend(delegation.by for delegation in delegations)
request.session['related_users'] = users
return request.session['related_users']
def get_documents(request, qs=None, outbox=False):
'''Retrieve visible documents for the current user'''
if qs is None:
qs = Document.objects.all()
related_users = get_related_users(request)
mailboxes = Mailbox.objects.filter(owner__in=related_users,
outbox=outbox)
qs = qs.filter(mailboxes__in=mailboxes) \
.exclude(deleteddocument__user=request.user)
qs = qs.distinct()
qs = qs.prefetch_related('to_list', 'to_user', 'mailboxes__owner')
return qs
request.session['related_users'] = [user.pk for user in users]
return User.objects.filter(pk__in=request.session['related_users'])
class MailboxQuerysetMixin(object):
def get_queryset(self):
qs = super(MailboxQuerysetMixin, self).get_queryset()
# manual join using private API, children don't do this at home
qs.query.join((None, 'docbow_document', None, None))
qs.query.join(('docbow_document', 'docbow_seendocument', 'id', 'document_id'), promote=True)
qs = qs.extra(select={'seen': 'docbow_seendocument.id is not null'})
return get_documents(self.request, qs=qs, outbox=self.outbox)
if not hasattr(self, '_qs'):
self._qs = sql.get_documents(qs, get_related_users(self.request),
self.request.user, self.outbox).distinct()
return self._qs
def get_context_data(self, **kwargs):
ctx = super(MailboxQuerysetMixin, self).get_context_data(**kwargs)

View File

@ -9,6 +9,8 @@ from django.template.loader import render_to_string
from upload_views import get_files_for_id
from . import app_settings
class TextInpuWithPredefinedValues(MultiWidget):
CLIENT_CODE = '''
<script type="text/javascript">
@ -71,6 +73,7 @@ class JqueryFileUploadFileInput(MultiFileInput):
'open_tv': '{{',
'close_tv': '}}',
'upload_url': self.url,
'max_file_size': app_settings.MAX_FILE_SIZE,
'extensions': self.extensions,
'attached_file_kind': self.attached_file_kind,
'files': self.files,

View File

@ -81,7 +81,21 @@ Pour archive dans /opt/archive les documents d'il y a plus d'un an::
/etc/init.d/docbow manage archive2 /opt/archive 366
Un répertoire /opt/archive/2013-01-01T16:34:34.343434/doc/ sera créé. Le
sous-répertoire doc contiendra les fichiers attachés ainsi que les métadonnées
du document tandis que le journal sera sauvegardé dans le fichier
Un répertoire, nommé /opt/archive/2013-01-01T16:34:34.343434/doc/ par exemple,
sera créé contenant un sous-répertoire par document. Chaque sous répertoire
est nommé d'après l'identifiant du document en base il contient au moins 3
fichiers:
Nom Description
======================= ==================
document.json la sérialisation JSON du modèle du document faite par
Django
attached_file_<id>.json la sérialisation JSON du modèle d'un fichier attaché
<filename>.<ext> le contenu d'un fichier attaché, le même nom de
fichier apparait dans le document JSON
Si plusieurs fichiers sont attachés à un même document, il y aura un fichier
JSON et un fichier de contenu pour chacun d'entre eux.
Les lignes du journal seront sauvegardées dans le fichier
/opt/archive/2013-01-01T16:34:34.343434/journal.txt

View File

@ -2,21 +2,21 @@ import os.path
import json
import datetime
from app_settings import PFWB_GED_DIRECTORY
from ..docbow.models import AttachedFile
import models
from . import app_settings, models
def push_document(signal, sender, instance, **kwargs):
'''post-save signal handler, to push new documents inside the GED directory'''
attached_file = instance
document = attached_file.document
if not PFWB_GED_DIRECTORY:
if not app_settings.PFWB_GED_DIRECTORY:
return
try:
plone_file_type = document.filetype.plonefiletype
except models.PloneFileType.DoesNotExist:
return
tpl = '{sender.first_name} {sender.last_name} ({sender.username})'
tpl = u'{sender.first_name} {sender.last_name} ({sender.username})'
sender = tpl.format(sender=document.sender)
metadata = {
'document_id': document.id,
@ -31,8 +31,8 @@ def push_document(signal, sender, instance, **kwargs):
name = os.path.basename(attached_file.name)
name = '%s-%s-%s' % (document.id, datetime.datetime.now().isoformat(), name)
json_name = name + '.json'
path = os.path.join(PFWB_GED_DIRECTORY, name)
path_json = os.path.join(PFWB_GED_DIRECTORY, json_name)
path = os.path.join(app_settings.PFWB_GED_DIRECTORY, name)
path_json = os.path.join(app_settings.PFWB_GED_DIRECTORY, json_name)
with open(path, 'w') as f:
f.write(attached_file.content.read())
with open(path_json, 'w') as f:

View File

@ -1,21 +1,48 @@
from django.conf import settings
# -*- coding: utf-8 -*-
# directory where ged files are stored
PFWB_GED_DIRECTORY = getattr(settings, 'DOCBOW_PFWB_GED_DIRECTORY', None)
# default type id for documents received by SMTP when given type does not exist
PFWB_SENDMAIL_DEFAULT_TYPE_ID = getattr(settings, 'DOCBOW_PFWB_SENDMAIL_DEFAULT_TYPE_ID', None)
# default type name if default type id does not exist
PFWB_SENDMAIL_DEFAULT_TYPE_NAME = getattr(settings, 'DOCBOW_PFWB_SENDMAIL_DEFAULT_TYPE_NAME', 'Divers')
# sender email for document received from tabellio expedition by SMTP
PFWB_SENDMAIL_TABELLIO_EXPEDITION_EMAIL = getattr(settings,
'DOCBOW_PFWB_SENDMAIL_TABELLIO_EXPEDITION_EMAIL', 'commande.documents@pfwb.be')
# user id of senders for document received from tabellio expedition by SMTP
PFWB_SENDMAIL_TABELLIO_EXPEDITION_USER_ID = getattr(settings,
'DOCBOW_PFWB_SENDMAIL_TABELLIO_EXPEDITION_USER_ID', None)
# sender email for document received by SMTP (generic code)
PFWB_SENDMAIL_ATTACHED_FILE_EMAIL = getattr(settings,
'DOCBOW_PFWB_SENDMAIL_ATTACHED_FILE_EMAIL', None)
# user id of senders for document received by SMTP (generic code)
PFWB_SENDMAIL_ATTACHED_FILE_USER_ID = getattr(settings,
'DOCBOW_PFWB_SENDMAIL_ATTACHED_FILE_USER_ID', None)
class AppSettings(object):
'''Thanks django-allauth'''
__DEFAULTS = dict(
# directory where ged files are stored
PFWB_GED_DIRECTORY = None,
# default type id for documents received by SMTP when given type does
# not exist
PFWB_SENDMAIL_DEFAULT_TYPE_ID = None,
# default type name if default type id does not exist
PFWB_SENDMAIL_DEFAULT_TYPE_NAME = 'Divers',
# sender email for document received from tabellio expedition by SMTP
PFWB_SENDMAIL_TABELLIO_EXPEDITION_EMAIL = 'commande.documents@pfwb.be',
# user id of senders for document received from tabellio expedition by SMTP
PFWB_SENDMAIL_TABELLIO_EXPEDITION_USER_ID = None,
# sender email for document received by SMTP (generic code)
PFWB_SENDMAIL_ATTACHED_FILE_EMAIL = None,
# user id of senders for document received by SMTP (generic code)
PFWB_SENDMAIL_ATTACHED_FILE_USER_ID = None,
)
def __init__(self, prefix):
self.prefix = prefix
@property
def settings(self):
from django.conf import settings
return settings
def __getattr__(self, key):
if key in self.__DEFAULTS:
return getattr(self.settings,
self.prefix+key, self.__DEFAULTS[key])
else:
from django.core.exceptions import ImproperlyConfigured
try:
return getattr(self.settings, self.prefix+key)
except AttributeError:
raise ImproperlyConfigured('settings %s is missing' % self.prefix+key)
app_settings = AppSettings('DOCBOW_')
app_settings.__name__ = __name__
app_settings.__file__ = __file__
import sys
sys.modules[__name__] = app_settings

View File

@ -11,7 +11,6 @@ import urllib2
from django.core.management.base import BaseCommand
import django.contrib.auth.models as auth_models
from django.core.files.base import ContentFile
from django.db import transaction
from django.core.exceptions import MultipleObjectsReturned
from django.utils.timezone import utc, make_aware
from django.template.defaultfilters import slugify
@ -19,6 +18,10 @@ from django.template.defaultfilters import slugify
from docbow_project.docbow import models, timestamp, utils
from docbow_project.docbow.email_utils import u2u_decode
from django_journal import record
try:
from django.db.transaction import atomic
except ImportError:
from django.db.transaction import commit_on_success as atomic
from ... import app_settings
@ -96,7 +99,7 @@ In case of failure the following return value is returned:
return None
return self.mailing_lists.get(username[len('liste-'):])
@transaction.commit_on_success
@atomic
def handle_mail(self, mail, mail_recipients, **options):
content_errors = []
attachments = []

View File

@ -9,6 +9,9 @@ import tempfile
import sys
from contextlib import contextmanager
from StringIO import StringIO
from functools import wraps
MEDIA_ROOT = tempfile.mkdtemp()
@contextmanager
def captured_output():
@ -25,6 +28,7 @@ class stderr_output(object):
self.output = output
def __call__(self, func):
@wraps(func)
def f(testcase, *args, **kwargs):
with captured_output() as (out, err):
ret = func(testcase, *args, **kwargs)
@ -32,13 +36,27 @@ class stderr_output(object):
return ret
return f
class stdout_output(object):
def __init__(self, output):
self.output = output
def __call__(self, func):
@wraps(func)
def f(testcase, *args, **kwargs):
with captured_output() as (out, err):
ret = func(testcase, *args, **kwargs)
testcase.assertEqual(self.output, out.getvalue())
return ret
return f
from django.test import TestCase
from django.test.utils import override_settings
from django.core import management
from django.contrib.auth.models import User
from docbow_project.pfwb.models import TabellioDocType
from docbow_project.docbow.models import FileType, MailingList, Document
from docbow_project.pfwb.models import TabellioDocType, PloneFileType
from docbow_project.docbow.models import (FileType, MailingList, Document,
AttachedFile)
import django_journal
@ -50,12 +68,10 @@ RECIPIENT_LIST_EMAIL = 'liste-ma-liste@example.com'
@override_settings(DOCBOW_PFWB_SENDMAIL_DEFAULT_TYPE_NAME='Default')
@override_settings(DOCBOW_PFWB_SENDMAIL_TABELLIO_EXPEDITION_EMAIL=EXPEDITION_EMAIL)
@override_settings(DOCBOW_PFWB_SENDMAIL_TABELLIO_EXPEDITION_USER_ID=1)
@override_settings(PFWB_GED_DIRECTORY='/tmp')
@override_settings(DOCBOW_PFWB_GED_DIRECTORY=None)
@override_settings(MEDIA_ROOT=MEDIA_ROOT)
class SendMailTestCase(TestCase):
def setUp(self):
import app_settings
reload(app_settings)
self.pjd_filetype = FileType.objects.create(name='PJD', id=2)
self.tabellio_doc_type = TabellioDocType.objects.create(filetype=self.pjd_filetype,
tabellio_doc_type='PJD')
@ -174,3 +190,179 @@ Coucou''', EXPEDITION_EMAIL, RECIPIENT_EMAIL, 'PJD', 'Mouais: monfichier.pdf')
self.assertEquals(Document.objects.get().to_user.count(), 0)
self.assertEquals(Document.objects.get().to_list.count(), 1)
self.assertEquals(Document.objects.get().to_list.all()[0], self.to_list)
@override_settings(DOCBOW_PFWB_SENDMAIL_DEFAULT_TYPE_ID=1)
@override_settings(DOCBOW_PFWB_SENDMAIL_DEFAULT_TYPE_NAME='Default')
@override_settings(DOCBOW_PFWB_SENDMAIL_ATTACHED_FILE_EMAIL=EXPEDITION_EMAIL)
@override_settings(DOCBOW_PFWB_SENDMAIL_ATTACHED_FILE_USER_ID=1)
@override_settings(DOCBOW_PFWB_GED_DIRECTORY=None)
@override_settings(MEDIA_ROOT=MEDIA_ROOT)
class SendMailAttachedFileTestCase(TestCase):
def setUp(self):
self.pjd_filetype = FileType.objects.create(name='PJD', id=2)
self.tabellio_doc_type = TabellioDocType.objects.create(filetype=self.pjd_filetype,
tabellio_doc_type='PJD')
self.expedition_user = User.objects.create(username='expedition', id=1)
self.to_user = User.objects.create(username='recipient', email=RECIPIENT_EMAIL, id=2)
self.to_list = MailingList.objects.create(name='ma liste')
self.to_list.members.add(self.to_user)
def build_message(self, filetype, to_addr, from_addr, content, attached_files):
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import random
message = MIMEMultipart()
message['Subject'] = Header(unicode(filetype), 'utf-8')
message['To'] = Header(to_addr, 'utf-8')
message['From'] = Header(from_addr, 'utf-8')
message['Message-ID'] = '<%s@example.com>' % random.random()
msg = MIMEText(content, _subtype='plain')
message.attach(msg)
for filename, content in attached_files:
msg = MIMEBase('application', 'octet-stream')
msg.set_payload(content)
msg.add_header('Content-Disposition', 'attachment', filename=filename)
encoders.encode_base64(msg)
message.attach(msg)
return message.as_string()
@stderr_output('')
def test_attached_file1(self):
with tempfile.NamedTemporaryFile() as f:
f.write(self.build_message(
self.pjd_filetype,
EXPEDITION_EMAIL,
RECIPIENT_EMAIL,
'coucou',
(('attached-file', 'content'),)))
f.flush()
management.call_command('sendmail', RECIPIENT_EMAIL, file=f.name,
sender=EXPEDITION_EMAIL)
self.assertEqual(Document.objects.count(), 1)
document = Document.objects.get()
self.assertEquals(document.attached_files.count(), 1)
self.assertEquals(document.comment, 'coucou')
self.assertEquals(document.attached_files.get().name, 'attached-file')
self.assertEquals(document.attached_files.get().content.read(),
'content')
self.assertEquals(document.to_user.count(), 1)
self.assertEquals(document.to_list.count(), 0)
self.assertEquals(document.to_user.get(), self.to_user)
@override_settings(MEDIA_ROOT=MEDIA_ROOT)
class PushDocumentTestCase(TestCase):
def setUp(self):
self.ged_dir = tempfile.mkdtemp()
def tearDown(self):
import shutil
shutil.rmtree(self.ged_dir)
def test_push_document1(self):
from django.core.files.base import ContentFile
import os.path
from glob import glob
with self.settings(DOCBOW_PFWB_GED_DIRECTORY=self.ged_dir):
FROM_USERNAME = 'from_user'
FROM_FIRST_NAME = 'from_first_name'
FROM_LAST_NAME = 'from_last_name'
self.from_user = User.objects.create(username=FROM_USERNAME,
first_name=FROM_FIRST_NAME, last_name=FROM_LAST_NAME)
self.to_user = User.objects.create(username='to_user')
self.filetype = FileType.objects.create(name='filetype')
self.plone_filetype = PloneFileType.objects.create(filetype=self.filetype,
plone_portal_type='plone-portal-type')
DESCRIPTION = 'description'
self.document = Document.objects.create(sender=self.from_user,
filetype=self.filetype,
comment=DESCRIPTION)
self.attached_file = AttachedFile(name='attached-file',
document=self.document, kind=None)
CONTENT = 'content'
self.attached_file.content.save('attached-file',
ContentFile(CONTENT))
pattern1 = '{0}-*-{1}.json'.format(self.document.id,
self.attached_file.name)
pattern2 = '{0}-*-{1}'.format(self.document.id,
self.attached_file.name)
files1 = glob(os.path.join(self.ged_dir, pattern1))
files2 = glob(os.path.join(self.ged_dir, pattern2))
self.assertEquals(len(files1), 1)
self.assertEquals(len(files2), 1)
with file(files2[0]) as f:
self.assertEquals(f.read(), CONTENT)
import json
with file(files1[0]) as f:
json_content = json.loads(f.read())
self.assertIsNotNone(json_content)
self.assertEquals(json_content['document_id'], self.document.id)
self.assertEquals(json_content['plone_portal_type'],
self.plone_filetype.plone_portal_type)
self.assertEquals(json_content['title'],
unicode(self.filetype))
self.assertEquals(json_content['description'], DESCRIPTION)
self.assertEquals(json_content['sender'],
u'{0} {1} ({2})'.format(FROM_FIRST_NAME,
FROM_LAST_NAME, FROM_USERNAME))
@override_settings(MEDIA_ROOT=MEDIA_ROOT)
@override_settings(DOCBOW_PFWB_GED_DIRECTORY=None)
class ArchiveTestCase(TestCase):
def setUp(self):
import datetime
from django.core.files.base import ContentFile
self.archive_dir = tempfile.mkdtemp()
FROM_USERNAME = 'from_user'
FROM_FIRST_NAME = 'from_first_name'
FROM_LAST_NAME = 'from_last_name'
self.from_user = User.objects.create(username=FROM_USERNAME,
first_name=FROM_FIRST_NAME, last_name=FROM_LAST_NAME)
self.to_user = User.objects.create(username='to_user')
self.filetype = FileType.objects.create(name='filetype')
self.plone_filetype = PloneFileType.objects.create(filetype=self.filetype,
plone_portal_type='plone-portal-type')
DESCRIPTION = 'description'
self.document = Document.objects.create(sender=self.from_user,
filetype=self.filetype,
comment=DESCRIPTION,
date=datetime.date.today()-datetime.timedelta(days=366))
self.attached_file = AttachedFile(name='attached-file',
document=self.document, kind=None)
CONTENT = 'content'
self.attached_file.content.save('attached-file',
ContentFile(CONTENT))
def tearDown(self):
import shutil
shutil.rmtree(self.archive_dir)
def test_archive(self):
import os.path
import glob
import datetime
with captured_output() as (out, err):
management.call_command('archive2', self.archive_dir, 100)
l = glob.glob(os.path.join(self.archive_dir, '*'))
self.assertEquals(len(l), 1)
self.assertTrue(l[0].split('T')[0],
datetime.datetime.today().isoformat())
archive_dir = os.path.join(self.archive_dir, l[0])
self.assertTrue(os.path.exists(os.path.join(archive_dir, 'doc')))
self.assertTrue(os.path.exists(os.path.join(archive_dir, 'doc',
str(self.document.id))))
self.assertTrue(os.path.exists(os.path.join(archive_dir, 'doc',
str(self.document.id), 'document.json')))
self.assertTrue(os.path.exists(os.path.join(archive_dir, 'doc',
str(self.document.id), 'attached_file_%s.json' %
self.attached_file.id)))
self.assertTrue(os.path.exists(os.path.join(archive_dir,
'journal.txt')))

View File

@ -67,7 +67,7 @@ __ENVIRONMENT_DEFAULTS = dict(
'docbow_project.docbow.notification.MailNotifier',
'docbow_project.docbow.notification.SMSNotifier',
),
DOCBOW_PFWB_GED_DIRECTORY='/var/lib/%s/ged/',
DOCBOW_PFWB_GED_DIRECTORY='/var/lib/docbow/ged/',
DOCBOW_PFWB_SENDMAIL_DEFAULT_TYPE_ID=None,
DOCBOW_PFWB_SENDMAIL_DEFAULT_TYPE_NAME=u'Divers',
DOCBOW_PFWB_SENDMAIL_TABELLIO_EXPEDITION_EMAIL='commande.documents@pfwb.be',
@ -79,6 +79,7 @@ __ENVIRONMENT_DEFAULTS = dict(
CRISPY_TEMPLATE_PACK='uni_form',
DOCBOW_TIMESTAMP_PROVIDER='certum',
RAVEN_CONFIG_DSN='',
DOCBOW_MAX_FILE_SIZE=10*1024*1024,
)
for key, default in __ENVIRONMENT_DEFAULTS.iteritems():
@ -98,6 +99,8 @@ for key, default in __ENVIRONMENT_DEFAULTS.iteritems():
value = [ unicode(x, 'utf8') for x in value ]
elif isinstance(default, unicode):
value = unicode(value, 'utf8')
elif isinstance(default, int):
value = int(value)
except KeyError:
value = default
globals()[key] = value
@ -129,6 +132,8 @@ else:
'django.template.loaders.app_directories.Loader',)),
)
ATOMIC_REQUESTS = True
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
@ -265,7 +270,6 @@ else:
"email": "mail"
}
DEBUG_TOOLBAR_CONFIG = {}
try:
from local_settings import *
@ -293,10 +297,7 @@ if DEBUG and 'SECRET_KEY' not in globals():
if USE_DEBUG_TOOLBAR:
try:
import debug_toolbar
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
INSTALLED_APPS += ('debug_toolbar',)
if 'INTERCEPT_REDIRECTS' not in DEBUG_TOOLBAR_CONFIG:
DEBUG_TOOLBAR_CONFIG.update({ 'INTERCEPT_REDIRECTS': False })
except ImportError:
print "Debug toolbar missing, not loaded"

View File

@ -11,9 +11,10 @@ pyasn1<1.0.0
pyasn1-modules<1.0.0
rfc3161==0.1.9
gunicorn
django_journal<2.0.0
django_journal>=1.23.0,<2.0.0
django-picklefield==0.3.0
git+http://repos.entrouvert.org/python-entrouvert.git/#egg=python-entrouvert-99999>=2
http://repos.entrouvert.org/python-entrouvert.git/snapshot/python-entrouvert-master.tar.bz2#egg=python-entrouvert-99999>=2
django-tables2==0.13.0
python-magic<0.5
raven
pytz

View File

@ -26,7 +26,7 @@ class compile_translations(Command):
continue
curdir = os.getcwd()
os.chdir(os.path.realpath(path))
compile_messages(stderr=sys.stderr)
compile_messages(sys.stderr)
os.chdir(curdir)
class build(_build):

9
tools/update.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
cd /home/docbow/source/
su docbow -c 'git pull'
cd docbow_project/docbow
su docbow -c '/home/docbow/env/bin/pip install -r ../../requirements.txt'
su docbow -c '/home/docbow/env/bin/python ../../docbow-ctl compilemessages'
/etc/init.d/docbow manage syncdb --migrate
/etc/init.d/docbow manage collectstatic --noinput
/etc/init.d/docbow restart

98
tools/vis.html Normal file
View File

@ -0,0 +1,98 @@
<html>
<head>
<title>Candlestick Chart</title>
<script type="text/javascript" src="http://cachedcommons.org/cache/protovis/3.2.0/javascripts/protovis.js"></script>
<script type="text/javascript" src="vix.js"></script>
<style type="text/css">
#fig {
position: relative;
margin: auto;
width: 900px;
height: 220px;
}
</style>
</head>
<body>
<table border="1">
<thead>
<tr>
<th>Path</th>
<th>Min</th>
<th>Mean</th>
<th>Std</th>
<th>Max</th>
</tr>
</thead>
<tbody>
<script>
for (var i = 0; i < vix.length; i++) {
var d = vix[i];
document.write("<tr><td>" + d.path + "</td><td>" + d.min + "</td><td>" + d.mean + "</td><td>" + d.std + "</td><td>" + d.max + "</td></tr>");
}
</script>
</tbody>
<caption>Datas</caption>
</table>
<div id="center"><div id="fig">
<script type="text/javascript+protovis">
/* Scales. */
var w = 840,
h = 200,
x = pv.Scale.ordinal(vix, function(d) d.path).split(20, w-20),
y = pv.Scale.linear(vix, function(d) d.min, function(d) d.max).range(0, h).nice();
var vis = new pv.Panel()
.width(w)
.height(h)
.margin(10)
.left(30);
/* Paths. */
vis.add(pv.Rule)
.data(vix)
.strokeStyle("#eee")
.anchor("bottom").add(pv.Label)
.left(function (d) x(d.path))
.text(function (d) d.path);
/* Time. */
vis.add(pv.Rule)
.data(y.ticks(7))
.bottom(y)
.left(-10)
.right(-10)
.strokeStyle(function(d) d % 10 ? "#ccc" : "#333")
.anchor("left").add(pv.Label)
.textStyle(function(d) d % 10 ? "#999" : "#333")
.text(y.tickFormat)
.anchor("top").add(pv.Label)
.top(-12)
.left(-20)
.font("bold 10px sans-serif")
.text("ms");
/* Candlestick. */
vis.add(pv.Rule)
.data(vix)
.left(function(d) x(d.path))
.bottom(function(d) y(d.mean - 2*Math.sqrt(d.std)))
.height(function(d) 4*Math.sqrt(d.std))
.strokeStyle("#ae13ff")
.lineWidth(10)
.add(pv.Rule)
.bottom(function(d) y(Math.min(d.min, d.max)))
.height(function(d) Math.abs(y(d.max) - y(d.min)))
.strokeStyle("#ae1325")
.lineWidth(3);
vis.render();
</script>
</div></div></body>
</html>

99
tools/vix.js Normal file
View File

@ -0,0 +1,99 @@
var vix =
[
{
"std": 6.2838809338783808,
"min": 48.87890815734863,
"max": 117.35892295837402,
"median": 51.397800445556641,
"path": "/inbox/",
"mean": 52.986168217014622
},
{
"std": 5.0712567658069432,
"min": 25.820016860961914,
"max": 93.6269760131836,
"median": 27.652978897094727,
"path": "/outbox/",
"mean": 28.302782935065192
},
{
"std": 17.959943957167141,
"min": 9.320974349975586,
"max": 77.15797424316406,
"median": 13.239860534667969,
"path": "/inbox/66/",
"mean": 22.619637927493535
},
{
"std": 16.905766906795069,
"min": 9.452104568481445,
"max": 104.50601577758789,
"median": 55.394887924194336,
"path": "/inbox/28/",
"mean": 53.443717956542969
},
{
"std": 20.719964490781539,
"min": 9.491920471191406,
"max": 110.88180541992188,
"median": 14.102935791015625,
"path": "/inbox/55/",
"mean": 24.875629270398939
},
{
"std": 19.056132820228122,
"min": 9.421825408935547,
"max": 74.40400123596191,
"median": 13.787031173706055,
"path": "/inbox/73/",
"mean": 23.699891889417493
},
{
"std": 19.59120154967297,
"min": 9.75489616394043,
"max": 70.6789493560791,
"median": 14.936923980712891,
"path": "/inbox/81/",
"mean": 25.634705053793418
},
{
"std": 19.731671888538365,
"min": 9.202957153320312,
"max": 86.27200126647949,
"median": 14.432907104492188,
"path": "/inbox/69/",
"mean": 24.82308697056126
},
{
"std": 19.466554907855816,
"min": 9.569168090820312,
"max": 67.42000579833984,
"median": 14.648199081420898,
"path": "/inbox/45/",
"mean": 24.809984258703285
},
{
"std": 20.643612271940331,
"min": 10.020017623901367,
"max": 77.55303382873535,
"median": 15.462160110473633,
"path": "/inbox/60/",
"mean": 26.355472770897119
},
{
"std": 20.20322737222353,
"min": 9.835958480834961,
"max": 76.26891136169434,
"median": 15.053033828735352,
"path": "/inbox/74/",
"mean": 25.443387675929714
},
{
"std": 20.45568343764522,
"min": 9.819984436035156,
"max": 79.49686050415039,
"median": 15.069961547851562,
"path": "/inbox/62/",
"mean": 25.949336387015677
}
];