add extra senders capability (#49185)

This commit is contained in:
Emmanuel Cazenave 2020-12-10 12:07:08 +01:00
parent baebc42524
commit a950176a47
13 changed files with 350 additions and 17 deletions

View File

@ -6,7 +6,15 @@ import logging
import collections
from django.forms import ModelForm, Form, Textarea, EmailField, CharField, ModelChoiceField
from django.forms import (
ModelForm,
Form,
Textarea,
EmailField,
CharField,
ModelChoiceField,
ModelMultipleChoiceField,
)
from django import forms
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _
@ -39,7 +47,8 @@ from docbow_project.docbow.validators import phone_normalize, validate_fr_be_pho
from docbow_project.docbow.middleware import get_extra
from docbow_project.docbow.utils import mime_types_to_extensions, truncate_filename, a2_wscall
from docbow_project.docbow import fields, app_settings, models, widgets
from docbow_project.docbow import notification
from docbow_project.docbow import notification, pyuca
from docbow_project.docbow.widgets import FilteredSelectMultiple
logger = logging.getLogger(__name__)
@ -121,10 +130,11 @@ class FileForm(RecipientForm, ModelForm):
class Meta:
model = Document
exclude = ('filetype', 'date', 'to_user', 'to_list', '_timestamp', 'real_sender', 'reply_to')
widgets = {'extra_senders': FilteredSelectMultiple(_('Extra Senders'), False)}
class Media:
css = {'all': ('docbow/css/send-file.css', 'docbow/css/send_file_form.css')}
js = ('js/askdirtyform.js', 'js/url-preload.js')
js = ('js/askdirtyform.js', 'js/url-preload.js', 'js/foldable.js')
def __init__(self, *args, **kwargs):
'''Initialize the form.'''
@ -138,6 +148,8 @@ class FileForm(RecipientForm, ModelForm):
doc = self.reply_to
initial['sender'] = kwargs.get('user', None)
initial['recipients'] = ['user-%s' % doc.sender.id]
if doc.extra_senders.exists():
initial['recipients'] += ['user-%s' % sender.pk for sender in doc.extra_senders.all()]
initial['comment'] = u'Re: ' + doc.comment
super(FileForm, self).__init__(*args, **kwargs)
@ -179,6 +191,17 @@ class FileForm(RecipientForm, ModelForm):
if not app_settings.PRIVATE_DOCUMENTS:
del self.fields['private']
if self.reply_to or not settings.EXTRA_SENDERS:
del self.fields['extra_senders']
else:
self.fields['extra_senders'].required = False
extra_senders_qs = User.objects.filter(is_active=True).exclude(docbowprofile__is_guest=True)
if self.user:
extra_senders_qs = extra_senders_qs.exclude(pk=self.user.pk)
extra_senders = [(user.pk, username(user)) for user in extra_senders_qs]
extra_senders = sorted(list(extra_senders), key=lambda x: pyuca.collator.sort_key(x[1]))
self.fields['extra_senders'].choices = extra_senders
def template_content_fields(self):
return [self[name] for name, _ in self.content_fields]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-10 14:33
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('docbow', '0005_soft_delete'),
]
operations = [
migrations.AddField(
model_name='document',
name='extra_senders',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,5 +1,6 @@
import os
import datetime as dt
import itertools
import random
import hashlib
import html
@ -232,6 +233,7 @@ class Document(Model):
sender = ForeignKey(User, verbose_name=_('Sender'), on_delete=CASCADE, related_name='documents_sent')
real_sender = CharField(max_length=64, blank=True, verbose_name=_('Real sender'))
extra_senders = ManyToManyField(User)
date = DateTimeField(default=now, verbose_name=_("Date d'envoi"))
to_user = ManyToManyField(
User, related_name='directly_received_documents', blank=True, verbose_name=_('Users to send to')
@ -389,14 +391,15 @@ class Document(Model):
document=self,
recipient=user,
)
# Deliver to ouput mailbox of the sender
Mailbox.objects.get_or_create(owner=self.sender, outbox=True, document=self)
django_journal.record(
'delivery',
'deliver document {document} in output mailbox of user {recipient}',
document=self,
recipient=self.sender,
)
# Deliver to ouput mailbox of the senders
for sender in itertools.chain([self.sender], self.extra_senders.all()):
Mailbox.objects.get_or_create(owner=sender, outbox=True, document=self)
django_journal.record(
'delivery',
'deliver document {document} in output mailbox of user {recipient}',
document=self,
recipient=self.sender,
)
# Push notifications
Notification.objects.notify(document=self, users=to)
if forward:
@ -429,6 +432,9 @@ class Document(Model):
def sender_display(self):
return username(self.sender)
def extra_senders_display(self):
return " ".join([username(sender) for sender in self.extra_senders.all()])
def url(self):
return urlparse.urljoin(
app_settings.BASE_URL, reverse('inbox-message', kwargs=dict(mailbox_id=self.id))

View File

@ -32,3 +32,10 @@
margin: 1%;
}
div#div_id_extra_senders.foldable {
cursor: pointer;
}
div#div_id_extra_senders.foldable > div.folded {
display: none !important;
}

View File

@ -228,4 +228,3 @@ font-weight:normal;
.selector h2 {
margin-bottom:0;
}

View File

@ -34,7 +34,7 @@ var SelectFilter = {
// <div class="selector"> or <div class="selector stacked">
var selector_div = quickElement('div', from_box.parentNode);
selector_div.className = is_stacked ? 'selector stacked' : 'selector';
selector_div.className = is_stacked ? 'selector stacked' : 'selector folded';
// <div class="selector-available">
var selector_available = quickElement('div', selector_div, '');

View File

@ -47,6 +47,9 @@ class OutboxBaseTable(tables.Table):
order_by=('sender__last_name', 'sender__first_name', 'sender__username'),
verbose_name=_('official_sender_header'),
)
extra_senders = tables.TemplateColumn(
'{{ record.extra_senders_display }}', verbose_name=_('Additional senders'),
)
recipients = tables.Column(accessor='recipients', verbose_name=_('recipients_header'), orderable=False)
real_sender = tables.TemplateColumn(
'{% load i18n %}{% if record.real_sender %}{{ record.real_sender }}{% else %}{% trans "self" %}{% endif %}',
@ -76,7 +79,15 @@ class OutboxTrashTable(OutboxBaseTable):
class Meta:
model = models.Document
fields = ('official_sender',)
sequence = ('recipients', 'official_sender', 'real_sender', 'filetype', 'filenames', '...')
sequence = (
'recipients',
'official_sender',
'extra_senders',
'real_sender',
'filetype',
'filenames',
'...',
)
attrs = {"class": "paleblue mailbox-table refresh", "id": "outbox-table"}
empty_text = _('No message')
@ -94,7 +105,16 @@ class OutboxTable(OutboxBaseTable):
class Meta:
model = models.Document
fields = ('official_sender',)
sequence = ('select', 'recipients', 'official_sender', 'real_sender', 'filetype', 'filenames', '...')
sequence = (
'select',
'recipients',
'official_sender',
'extra_senders',
'real_sender',
'filetype',
'filenames',
'...',
)
attrs = {"class": "paleblue mailbox-table refresh", "id": "outbox-table"}
empty_text = _('No message')
@ -127,6 +147,9 @@ class InboxBaseTable(tables.Table):
order_by=('sender__last_name', 'sender__first_name', 'sender__username'),
verbose_name=_('sender_header'),
)
extra_senders = tables.TemplateColumn(
'{{ record.extra_senders_display }}', verbose_name=_('Additional senders'),
)
date = tables.TemplateColumn(
'{% load humantime %}{{ record.date|humantime }}', verbose_name=_('date_header')
)

View File

@ -0,0 +1,26 @@
{% load docbow %}
{% load i18n %}
{% if field.is_hidden %}
{{ field }}
{% else %}
<div id="div_{{ field.auto_id }}" class="ctrlHolder{% if wrapper_class %} {{ wrapper_class }}{% endif %}{% if field.errors %} error{% endif %}{% if field|is_checkbox %} checkbox{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %} foldable">
{% for error in field.errors %}
<p id="error_{{ forloop.counter }}_{{ field.auto_id }}" class="errorField">
{{ error }}
</p>
{% endfor %}
{% if field.label %}
<label for="{{ field.id_for_label }}" {% if field.field.required %}class="requiredField"{% endif %}>
{{ field.label|safe }}<span class="asteriskField">({% blocktrans %}click{% endblocktrans %})</span>
</label>
{% endif %}
{{ field }}
{% if field.help_text %}
<div id="hint_{{ field.auto_id }}" class="formHint">{{ field.help_text|safe }}</div>
{% endif %}
</div>
{% endif %}

View File

@ -1,7 +1,7 @@
{% load i18n %}
{% load docbow %}
{% if document.sender != user %}
{% blocktrans with date=document.date|date:"SHORT_DATE_FORMAT" sender=document.sender|username type=document.filetype time=document.date|time:"H:i" %}{{type}} sent on {{date}} at {{time}} by {{sender}}{% endblocktrans %}
{% blocktrans with date=document.date|date:"SHORT_DATE_FORMAT" sender=document.sender|username type=document.filetype time=document.date|time:"H:i" %}{{type}} sent on {{date}} at {{time}} by {{sender}}{% endblocktrans %}{% if extra_senders %}{% blocktrans with extra_senders=extra_senders%} and {{extra_senders}}{% endblocktrans %}{% endif %}
{% else %}
{% blocktrans with date=document.date|date:"SHORT_DATE_FORMAT" sender=document.sender|username type=document.filetype time=document.date|time:"H:i" %}{{type}} sent on {{date}} at {{time}} by you{% endblocktrans %}
{% blocktrans with date=document.date|date:"SHORT_DATE_FORMAT" sender=document.sender|username type=document.filetype time=document.date|time:"H:i" %}{{type}} sent on {{date}} at {{time}} by you{% endblocktrans %}{% if extra_senders %}{% blocktrans with extra_senders=extra_senders%} and {{extra_senders}}{% endblocktrans %}{% endif %}
{% endif %}

View File

@ -35,6 +35,8 @@
{% include "docbow/field.html" with field=form.recipients %}
{% include "docbow/extra_senders_field.html" with field=form.extra_senders %}
{% include "docbow/field.html" with field=form.comment %}
<div class="buttonHolder">

View File

@ -56,6 +56,7 @@ from docbow_project.docbow.models import (
DeletedDocument,
SeenDocument,
non_guest_users,
username,
)
from docbow_project.docbow.decorators import as_delegate
from docbow_project.docbow import tables
@ -182,6 +183,13 @@ def get_filetype_limitation(user):
return own_limitations
def get_table_kwargs():
res = {}
if not settings.EXTRA_SENDERS:
res['exclude'] = ('extra_senders',)
return res
@login_required
@never_cache
@watson.update_index()
@ -233,6 +241,7 @@ def send_file(request, file_type_id):
i = recipient.split('-')[1]
to_user.append(int(i))
new_send.save()
form.save_m2m()
form.save_attachments()
new_send.to_user.set(to_user)
new_send.to_list.set(to_list)
@ -404,6 +413,11 @@ def message(request, mailbox_id, outbox=False):
attached_files.append((attached_file.kind, [attached_file]))
request.record('message-view', 'looked at document {document}', document=document)
ctx['related_users'] = get_related_users(request)
extra_senders = ''
if document.extra_senders.exists():
extra_senders = ", ".join([username(sender) for sender in document.extra_senders.all()])
ctx['extra_senders'] = extra_senders
return render(request, 'docbow/message.html', ctx)
@ -711,6 +725,9 @@ class MailboxView(ExtraContextMixin, MailboxQuerysetMixin, tables_views.SingleTa
context['show_trash'] = settings.TRASH_DURATION > 0
return context
def get_table_kwargs(self):
return get_table_kwargs()
class TrashMailboxView(ExtraContextMixin, TrashMailboxQuerysetMixin, tables_views.SingleTableView):
model = Document
@ -719,6 +736,9 @@ class TrashMailboxView(ExtraContextMixin, TrashMailboxQuerysetMixin, tables_view
'per_page': app_settings.DOCBOW_MAILBOX_PER_PAGE,
}
def get_table_kwargs(self):
return get_table_kwargs()
class CSVMultipleObjectMixin(object):
mapping = ()

View File

@ -224,6 +224,9 @@ ZIP_DOWNLOAD = False
# Duration of trash retention in days
TRASH_DURATION = 0
EXTRA_SENDERS = False
local_settings_file = os.environ.get('DOCBOW_SETTINGS_FILE', 'local_settings.py')
if os.path.exists(local_settings_file):
exec(open(local_settings_file).read())

View File

@ -127,6 +127,208 @@ def test_sendfile(app, filetypes, users, settings):
assert doc.sender == sender
def test_sendfile_extra_senders(app, filetypes, users, settings):
settings.MEDIA_ROOT = MEDIA_ROOT
settings.EXTRA_SENDERS = True
sender = User.objects.get(username='user-1')
recipient1, recipient2 = User.objects.get(username='user-2'), User.objects.get(username='user-3')
extra_sender1, extra_sender2 = User.objects.get(username='user-4'), User.objects.get(username='user-5')
app.login()
ft = FileType.objects.first()
resp = app.get('/send_file/%s/' % ft.pk)
form = resp.form
form['content_1'] = Upload('readme.rst', b'data')
form['recipients'] = ['user-%s' % recipient1.pk, 'user-%s' % recipient2.pk]
form['extra_senders'] = ['%s' % extra_sender1.pk, '%s' % extra_sender2.pk]
resp = form.submit('send')
assert resp.status_code == 302
assert resp.location.endswith('/outbox/')
doc = Document.objects.first()
assert len(doc.delivered_to()) == 2
assert recipient1 in doc.delivered_to()
assert recipient2 in doc.delivered_to()
assert doc.filenames() == 'readme.rst'
assert doc.sender == sender
assert doc.extra_senders.count() == 2
assert doc.extra_senders.filter(pk=extra_sender1.pk).exists()
assert doc.extra_senders.filter(pk=extra_sender2.pk).exists()
# extra senders in outbox column
resp = app.get('/outbox/')
assert extra_sender1.username in resp.text
assert extra_sender2.username in resp.text
# extra senders in outbox message title
resp = app.get('/outbox/%s/' % doc.pk)
assert extra_sender1.username in resp.text
assert extra_sender2.username in resp.text
# delete doc
resp = app.get('/outbox/')
assert resp.status_code == 200
delete_form = resp.forms[2]
delete_form['selection'] = doc.pk
resp = delete_form.submit('delete')
assert resp.status_code == 302
assert resp.location.endswith('/outbox/')
# extra senders in trash outbox column
resp = app.get('/outbox/trash/')
assert extra_sender1.username in resp.text
assert extra_sender2.username in resp.text
# doc show up in extra sender outbox
assert_can_see_doc(app, doc, extra_sender1, inbox=False)
assert_can_see_doc(app, doc, extra_sender2, inbox=False)
# extra senders in inbox column
app.login(recipient1.username)
resp = app.get('/inbox/')
assert extra_sender1.username in resp.text
assert extra_sender2.username in resp.text
# extra senders in inbox message title
resp = app.get('/inbox/%s/' % doc.pk)
assert extra_sender1.username in resp.text
assert extra_sender2.username in resp.text
# delete doc
resp = app.get('/inbox/')
assert resp.status_code == 200
delete_form = resp.forms[2]
delete_form['selection'] = doc.pk
resp = delete_form.submit('delete')
assert resp.status_code == 302
assert resp.location.endswith('/inbox/')
# extra senders in trash inbox column
resp = app.get('/inbox/trash/')
assert extra_sender1.username in resp.text
assert extra_sender2.username in resp.text
def test_extra_senders_disabled_by_default(app, filetypes, users, settings):
settings.MEDIA_ROOT = MEDIA_ROOT
recipient = User.objects.get(username='user-2')
app.login()
ft = FileType.objects.first()
resp = app.get('/send_file/%s/' % ft.pk)
form = resp.form
assert 'extra_senders' not in form.fields
form['content_1'] = Upload('readme.rst', b'data')
form['recipients'] = ['user-%s' % recipient.pk]
resp = form.submit('send')
assert resp.status_code == 302
assert resp.location.endswith('/outbox/')
resp = app.get('/outbox/')
assert 'Additional senders' not in resp.text
resp = app.get('/outbox/trash/')
assert 'Additional senders' not in resp.text
resp = app.get('/inbox/')
assert 'Additional senders' not in resp.text
resp = app.get('/inbox/trash/')
assert 'Additional senders' not in resp.text
def test_sendfile_reply_to(app, filetypes, users, settings):
settings.MEDIA_ROOT = MEDIA_ROOT
sender = User.objects.get(username='user-1')
recipient = User.objects.get(username='user-2')
app.login()
ft = FileType.objects.first()
resp = app.get('/send_file/%s/' % ft.pk)
form = resp.form
form['content_1'] = Upload('readme.rst', b'data')
form['recipients'] = ['user-%s' % recipient.pk]
resp = form.submit('send')
assert resp.status_code == 302
assert resp.location.endswith('/outbox/')
doc = Document.objects.first()
assert len(doc.delivered_to()) == 1
assert recipient in doc.delivered_to()
assert doc.filenames() == 'readme.rst'
assert doc.sender == sender
app.login(recipient.username)
resp = app.get('/inbox/%s/' % doc.pk)
assert resp.status_code == 200
# reply
resp = resp.forms[0].submit()
assert resp.status_code == 200
resp = resp.click(filetypes[0].name)
form = resp.form
form['content_1'] = Upload('readme.rst', b'data')
resp = form.submit('send')
assert resp.status_code == 302
assert resp.location.endswith('/outbox/')
doc = Document.objects.exclude(pk=doc.pk).first()
assert len(doc.delivered_to()) == 1
assert sender in doc.delivered_to()
assert doc.filenames() == 'readme.rst'
assert doc.sender == recipient
assert_can_see_doc(app, doc, recipient, inbox=False)
assert_can_see_doc(app, doc, sender, inbox=True)
def test_sendfile_reply_to_extra_senders(app, filetypes, users, settings):
settings.MEDIA_ROOT = MEDIA_ROOT
settings.EXTRA_SENDERS = True
sender = User.objects.get(username='user-1')
recipient = User.objects.get(username='user-2')
extra_sender1, extra_sender2 = User.objects.get(username='user-4'), User.objects.get(username='user-5')
app.login()
ft = FileType.objects.first()
resp = app.get('/send_file/%s/' % ft.pk)
form = resp.form
form['content_1'] = Upload('readme.rst', b'data')
form['recipients'] = ['user-%s' % recipient.pk]
form['extra_senders'] = ['%s' % extra_sender1.pk, '%s' % extra_sender2.pk]
resp = form.submit('send')
assert resp.status_code == 302
assert resp.location.endswith('/outbox/')
doc = Document.objects.first()
assert len(doc.delivered_to()) == 1
assert recipient in doc.delivered_to()
assert doc.filenames() == 'readme.rst'
assert doc.sender == sender
app.login(recipient.username)
resp = app.get('/inbox/%s/' % doc.pk)
assert resp.status_code == 200
# reply
resp = resp.forms[0].submit()
assert resp.status_code == 200
resp = resp.click(filetypes[0].name)
form = resp.form
form['content_1'] = Upload('readme.rst', b'data')
# no extra sender on reply
assert 'extra_senders' not in form.fields
resp = form.submit('send')
assert resp.status_code == 302
assert resp.location.endswith('/outbox/')
doc = Document.objects.exclude(pk=doc.pk).first()
assert len(doc.delivered_to()) == 3
assert sender in doc.delivered_to()
assert extra_sender1 in doc.delivered_to()
assert extra_sender2 in doc.delivered_to()
assert doc.filenames() == 'readme.rst'
assert doc.sender == recipient
assert_can_see_doc(app, doc, sender, inbox=True)
assert_can_see_doc(app, doc, extra_sender1, inbox=True)
assert_can_see_doc(app, doc, extra_sender2, inbox=True)
def test_sendfile_mailing_list(app, filetypes, users, settings):
settings.MEDIA_ROOT = MEDIA_ROOT
sender = User.objects.get(username='user-1')