docbow/docbow_project/docbow/views.py

574 lines
25 KiB
Python

import smtplib
from itertools import count
import socket
import urllib
import os.path
import logging
from django.contrib.auth.decorators import login_required
import django.contrib.auth as auth
from django.shortcuts import render, redirect, get_object_or_404
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.db.models.query import Q
from django.contrib.auth.models import User
from django.db import transaction
from django.contrib import messages
from django.utils.translation import ugettext as _, ungettext
from django.core.mail import EmailMultiAlternatives, EmailMessage
from django.template.loader import render_to_string
from django.core.validators import validate_email, ValidationError
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import int_to_base36
from BeautifulSoup import BeautifulSoup
from forms import FileForm, AnonymousContactForm, ContactForm, DelegationForm, ForwardingForm
from models import Mailbox, username, Delegation, AttachedFile, \
SendingLimitation, Document, DocumentForwarded, AutomaticForwarding
from docbow_project.notification import NotificationFailure, notify
from decorator import no_delegate
from logger_adapter import get_logger
import timestamp
gettext_noop = lambda x: x
@login_required
def homepage(request):
return redirect('inbox')
class Row(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def listing(request, qs, template, view_name='', headers=None,
show_forwarding_status=False):
qs = qs.select_related(depth=2).order_by('-document__date')
paginator = Paginator(qs, settings.FILE_PER_PAGE)
page_number = request.GET.get('page', 1)
try:
page = paginator.page(page_number)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
page = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
page = paginator.page(paginator.num_pages)
file_list = []
if not headers:
headers = [ 'new_header', 'type_header', 'filename_header',
'sender_header', 'date_header', 'delete_column' ]
for item in page.object_list:
d = { 'href': reverse(view_name+'-message', args=(item.id,)) }
try:
d['seen'] = item.seen
except Mailbox.DoesNotExist:
d['seen'] = False
if view_name == 'outbox':
d['seen'] = True
d['comment'] = item.document.comment
values = {
gettext_noop('new_header'): ('X' if not d['seen'] else ' '),
gettext_noop('filename_header'): item.document.filenames(),
gettext_noop('type_header'): item.document.filetype.name,
gettext_noop('sender_header'): username(item.document.sender),
gettext_noop('comment_header'): item.document.comment,
gettext_noop('date_header'): item.document.date,
gettext_noop('delete_column'): 1,
}
cols = []
d['extra'] = []
for header in headers:
if header == 'comment_header':
d['extra'].append(values[header])
else:
cols.append(Row(id=header, caption=values[header]))
d['columns'] = cols
if show_forwarding_status:
d['forwards'] = item.document.document_forwarded_to.all()
file_list.append(d)
context = {
'page': page,
'view_name': view_name,
'file_list': file_list,
'headers': headers,
}
return render(request, template, context)
@login_required
def inbox(request):
qs = Mailbox.objects.filter(deleted=False, owner=request.user,
outbox=False)
headers = [ 'new_header', 'type_header', 'comment_header',
'filename_header', 'sender_header', 'date_header', 'delete_column'
]
specialize = request.session.get('ministre_or_parlementaire', None)
if specialize or request.user.groups.filter(Q(name='Parlementaires') | Q(name='Ministres')):
if not specialize:
request.session['ministre_or_parlementaire'] = True
headers = [ 'new_header', 'type_header', 'comment_header',
'filename_header', 'date_header', 'delete_column' ]
return listing(request, qs, 'docbow/inbox.html', view_name='inbox',
headers=headers, show_forwarding_status=can_forward(request))
@login_required
def outbox(request):
qs = Mailbox.objects.filter(deleted=False, owner=request.user, outbox=True)
return listing(request, qs, 'docbow/outbox.html', view_name='outbox',
headers=['type_header', 'comment_header', 'filename_header',
'date_header', 'delete_column'])
def send_mail_notifications(document, base_url,
subject_template='docbow/notify-email-subject.txt',
body_template='docbow/notify-email-body.txt',
html_body_template='docbow/notify-email-body.html',
logger=None):
if logger is None:
logger = logging.getLogger('docbow')
notify_emails = []
for user in document.to():
try:
validate_email(user.email)
notify_emails.append(user.email)
except ValidationError:
pass
location = reverse('inbox-by-document-message',
kwargs={'document_id': document.id})
url = base_url + location.lstrip('/')
context = {'document': document, 'url': url }
try:
mail = EmailMultiAlternatives(bcc=notify_emails,
subject=render_to_string(subject_template, context).strip(),
body=render_to_string(body_template, context))
mail.attach_alternative(render_to_string(html_body_template,
context), 'text/html')
mail.send(fail_silently=False)
logger.info('notification for document %(document_id)s \
to %(email_list)s', { 'document_id': document.id,
'email_list': ', '.join(notify_emails)})
return True
except (smtplib.SMTPException, socket.error), e:
logger.exception('unable to send one or more notifications for \
document %(document_id)s: %(error)s', { 'document_id': document.id, 'error': e })
return False
def user_mailing_list_names(user):
return user.mailing_lists.values_list('name', flat=True)
def get_file_form_kwargs(request):
limitation = SendingLimitation.objects.select_related().filter(mailing_list__members=request.user)
kwargs = {}
if limitation:
limitation = limitation[0]
if limitation.filetypes:
kwargs['filetype_qs'] = limitation.filetypes.all()
if limitation.lists:
kwargs['list_qs'] = limitation.lists.all()
kwargs['user_qs'] = User.objects.filter(mailing_lists=limitation.lists.all()).distinct()
return kwargs
def try_automatic_fowarding(document, logger, base_url):
'''Try to apply any automatic forwarding rule found matching the current
document.
'''
rules = AutomaticForwarding.objects.filter(filetypes=document.filetype,
originaly_to_user__in=list(document.to()))
if rules:
for rule in rules:
# choose a new sender
matching_recipients = rule.originaly_to_user.distinct() & document.to()
first_matching_recipient = matching_recipients[0]
try:
notification_count = forward_document(document, first_matching_recipient,
rule.forward_to_user.all(),
rule.forward_to_list.all(), logger, base_url,
automatic=True)
except Exception:
logger.exception('Unhandled exception during '
'application of automatic forwarding')
else:
if not notification_count:
logger.error('automatic forwarding failed '
'because we were unable to send '
'some notifications')
@login_required
def send_file(request):
logger = get_logger(request)
if request.method == 'POST':
if 'send' not in request.POST:
return redirect('outbox')
form = FileForm(request.POST, request.FILES, user=request.user,
**get_file_form_kwargs(request))
if form.is_valid():
with transaction.commit_on_success():
new_send = form.save(commit=False)
new_send.sender = request.user
to_user = []
to_list = []
for recipient in form.cleaned_data['recipients']:
if recipient.startswith('list-'):
i = recipient.split('-')[1]
to_list.append(int(i))
elif recipient.startswith('user-'):
i = recipient.split('-')[1]
to_user.append(int(i))
new_send.save()
form.save_attachments()
new_send.to_user = to_user
new_send.to_list = to_list
notification_count = new_send.post()
base_url = request.build_absolute_uri('/')
if not send_mail_notifications(new_send, base_url, logger=logger):
transaction.rollback()
messages.error(request, _('Document send failed because '
'some notifications could not be sent, administrators '
'have been informed.'))
else:
blob = new_send.timestamp_blob()
tst = timestamp.timestamp_json(blob)
logger.info('sent %s, timestamp %s' % (new_send, tst))
msg = ungettext(
'New document sent to %d recipient.',
'New document sent to %d recipients.',
notification_count)
messages.info(request, msg % notification_count)
try_automatic_fowarding(new_send, logger, base_url)
return redirect('outbox')
else:
form = FileForm(user=request.user, **get_file_form_kwargs(request))
return render(request, 'docbow/send_file.html',
{'form': form, 'view_name': 'send-file'})
def upload(request, attached_file):
response = HttpResponse(attached_file.content.chunks(), mimetype='application/octet-stream')
response['Content-disposition'] = 'attachment'
return response
@login_required
@transaction.commit_on_success
def message_attached_file(request, mailbox_id, attached_file):
'''
Download attached files, verify that the user has access to the document
before, otherwise return 404.
'''
logger = get_logger(request)
mailbox = get_object_or_404(Mailbox.objects.select_related(),
id=mailbox_id, owner=request.user)
document = mailbox.document
try:
attached_file = document.attached_files.get(pk=attached_file)
mailbox.seen = True
mailbox.save()
document_id = document.id
logger.info(_('download attached file %(attached_file)s from sent document %(document)s named %(filename)s')
% { 'document': document_id,
'attached_file': attached_file.id,
'filename': attached_file.filename() })
return upload(request, attached_file)
except AttachedFile.DoesNotExist:
raise Http404
def can_forward(request):
'''Is the current user allowed to forward a Document ?'''
return request.user.username == 'Greffe' or \
bool(request.user.mailing_lists.filter(name__in=('Greffe',))) or \
bool(request.user.groups.filter(name__in=('Administrateurs',)))
def user_who_can_forward_filter():
'''Return the list of users who have access to the forwarding feature'''
return User.objects.filter(Q(username='Greffe') |
Q(mailing_lists__name='Greffe') |
Q(groups__name='Administrateurs'))
def mark_document_read(document, logger=None):
'''
Mark all mailboxes object for this document of user who can forward as
read.
'''
users = user_who_can_forward_filter()
mboxes = Mailbox.objects.filter(owner__in=users, document=document)
if not logger:
logger = logging.getLogger('docbow')
logger.info(_('Marking mailboxes %s as read because document %s was forwarded'),
'. '.join([str(mbox.id) for mbox in mboxes]), document.id)
mboxes.update(seen=True)
def form_to_user_and_list(form):
to_user = []
to_list = []
for recipient in form.cleaned_data['recipients']:
if recipient.startswith('list-'):
i = recipient.split('-')[1]
to_list.append(int(i))
elif recipient.startswith('user-'):
i = recipient.split('-')[1]
to_user.append(int(i))
return to_user, to_list
def forward_document(document, sender, to_user, to_list, logger, base_url,
automatic=False):
new_document = Document()
new_document.sender = sender
new_document.filetype = document.filetype
new_document.comment = document.comment
new_document.save()
new_document.to_user = to_user
new_document.to_list = to_list
new_document.save()
for attached_file in document.attached_files.all():
AttachedFile.objects.get_or_create(
name=attached_file.name,
content=attached_file.content,
document=new_document)
notification_count = new_document.post()
DocumentForwarded.objects.get_or_create(from_document=document,
to_document=new_document, automatic=automatic)
if not send_mail_notifications(new_document, base_url, logger=logger):
transaction.rollback()
return False
else:
blob = new_document.timestamp_blob()
tst = timestamp.timestamp_json(blob)
is_automatic = 'automatically' if automatic else ''
logger.info('document %s forwarded %s as document %s, timestamp %s' %
(document, is_automatic, new_document, tst))
mark_document_read(document, logger=logger)
return notification_count
@login_required
def message(request, mailbox_id, back='inbox'):
mailbox_content = get_object_or_404(Mailbox, pk=mailbox_id,
owner=request.user, outbox=back!='inbox')
document = mailbox_content.document
logger = get_logger(request)
if back == 'inbox':
back_pair = (reverse('inbox'), gettext_noop('back to inbox'))
else:
back_pair = (reverse('outbox'), gettext_noop('back to outbox'))
logger.info(_('view details of document %(document)s')
% { 'user': request.user, 'document': document.pk })
ctx = { 'document': document,
'view_name': back, 'back': back_pair }
if back == 'inbox' and can_forward(request):
if request.method == 'POST':
form = ForwardingForm(request.POST, user=request.user)
if form.is_valid():
with transaction.commit_on_success():
to_user, to_list = form_to_user_and_list(form)
base_url = request.build_absolute_uri('/')
notification_count = forward_document(document, request.user, to_user,
to_list, logger, base_url)
if notification_count:
msg = ungettext(
'Forwarded document sent to %d recipient.',
'Forwarded document sent to %d recipients.',
notification_count)
messages.info(request, msg % notification_count)
return HttpResponseRedirect('')
else:
messages.error(request, _('Document forwarding failed because '
'some notifications could not be sent, administrators '
'have been informed.'))
else:
form = ForwardingForm(user=request.user)
ctx['form'] = form
return render(request, 'docbow/message.html', ctx)
__HELP_CONTENT = None
__HELP_URL = 'http://wiki.entrouvert.org/Docbow'
__HELP_PREFIX = 'http://wiki.entrouvert.org'
def get_help_content():
global __HELP_CONTENT
if __HELP_CONTENT is None:
doc = urllib.urlopen(__HELP_URL).read()
parsed_doc = BeautifulSoup(doc)
page = parsed_doc.findAll(True, id='page')[0]
for img in page.findAll('img'):
name = img['src'].split('target=')[1]
img['src'] = __HELP_PREFIX + img['src']
# pull
cache_dir = os.path.join(settings.STATIC_ROOT, 'cache')
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
cached_name = os.path.join(cache_dir, name)
open(cached_name, 'w').write(urllib.urlopen(img['src']).read())
img['src'] = '/static/cache/%s' % name
for t in page.findAll('span'):
if len(t.contents) == 0:
t.extract()
for t in page.findAll(id='pageinfo'):
t.extract()
for t in page.findAll('h1'):
t.name = 'h4'
for t in page.findAll('h2'):
t.name = 'h5'
for t in page.findAll('p', { 'class': 'table-of-contents-heading' }):
t.contents[0].replaceWith('<h4>%s</h4>' % t.contents[0])
__HELP_CONTENT = unicode(page)
return __HELP_CONTENT
@login_required
def help(request):
return render(request, 'docbow/help.html', { 'view_name': 'help', 'content': get_help_content() })
def contact(request, template='docbow/contact.html',
form_class=AnonymousContactForm):
logger = get_logger(request)
user = request.user
if user.is_authenticated():
template = 'docbow/contact_user.html'
form_class = ContactForm
contacts = User.objects.filter(groups__name__in=settings.CONTACT_GROUPS)
if not bool(contacts):
messages.error(request, _('No administror group is configured to receive contact mails'))
return redirect('inbox')
if request.method == 'POST':
form = form_class(request.POST)
if form.is_valid():
cleaned_data = form.cleaned_data
to = [ contact.email for contact in contacts ]
subject = settings.CONTACT_SUBJECT_PREFIX + cleaned_data['subject']
message = cleaned_data['message']
if user.is_authenticated():
reply_to = user.email
body = _('Message from %(name)s <%(email)s>') % { 'name':
user.get_full_name(), 'email': reply_to } + '\n\n%s' % message
else:
reply_to = cleaned_data['email']
body = _('Message from %(name)s <%(email)s>') % cleaned_data
if cleaned_data.get('phone_number'):
body += _('\nPhone number: %s') % cleaned_data['phone_number']
body += '\n\n%s' % message
try:
EmailMessage(to=to, subject=subject, body=body,
headers={'Reply-To': reply_to}).send()
messages.info(request, _('Your message was sent to %d administrators') % len(to))
logger.info('new contact mail sent: %s', cleaned_data)
except smtplib.SMTPException, socket.error:
logger.exception('unable to send the contact mail: %s' % cleaned_data)
return redirect('inbox')
else:
form = form_class()
return render(request, template, { 'form': form, 'view_name': 'contact' })
def logout(request):
auth.logout(request)
return redirect('inbox')
@login_required
def delete(request, mailbox_id, back='inbox'):
'''Remove a document from the inbox'''
logger = get_logger(request)
page = request.GET.get('page', 1)
viewname = back + '-message-delete'
back_pair = ('%s?page=%s' % (reverse(back), page),
gettext_noop('back to %s' % back))
m = get_object_or_404(Mailbox, pk=mailbox_id)
if request.method == 'GET':
return render(request, 'docbow/delete.html', { 'document': m.document, 'back': back_pair, 'view_name': viewname })
else:
m.deleted=True
m.save()
logger.info('mailbox entry %(mailbox_id)s marked as deleted', {
'mailbox_id': mailbox_id })
return redirect(back_pair[0])
def get_free_delegation_number(user):
'''Allocate a new delegation username'''
for i in count(1):
delegate_username = user.username + '-%d' % i
try:
User.objects.get(username=delegate_username)
except User.DoesNotExist:
return delegate_username
def make_password_reset_url(request, user):
uid = int_to_base36(user.id)
token = default_token_generator.make_token(user)
return request.build_absolute_uri(
reverse('auth_password_reset_confirm',
kwargs={'uidb36': uid, 'token': token}))
@login_required
@no_delegate
def delegate(request, domain_override=None,):
logger = get_logger(request)
delegations = Delegation.objects \
.select_related().filter(by=request.user)
form = DelegationForm()
if request.method == 'POST':
if 'create' in request.POST:
form = DelegationForm(request.POST)
if form.is_valid():
try:
with transaction.commit_on_success():
delegate_username = get_free_delegation_number(request.user)
delegate_user = User(username=delegate_username,
**form.cleaned_data)
delegate_user.save()
Delegation.objects.get_or_create(by=request.user,
to=delegate_user, guest_delegate=True)
ctx = {
'user': request.user,
'delegate': delegate_user,
'password_reset_link':
make_password_reset_url(request, delegate_user),
}
notify(request, 'new-delegation-mail', ctx=ctx, to=form.cleaned_data['email'],
reply_to=request.user.email)
except NotificationFailure:
messages.error(request, _('The delegation creation failed when notifying your new delegate, could you check the email you provided ?'))
except:
raise
logger.exception('unable to create a delegation')
messages.error(request, _('The delegation creation failed, an administrator has been notified.'))
else:
messages.info(request, _('New delegation to user %s created.') % delegate_user.username)
messages.info(request, _('A notification was sent to %s about this new delegation') % delegate_user.email)
return redirect('delegate')
else:
for delegation in delegations:
if 'delete-%s.x' % delegation.to.username in request.POST:
with transaction.commit_on_success():
delegate_user = delegation.to
delegation.delete()
if delegation.guest_delegate:
delegate_user.delete()
messages.info(request, _('Delegation %s supressed') % delegate_user)
ctx = {
'user': request.user,
'delegate': delegate_user,
}
try:
notify(request, 'delete-delegation-mail',
ctx=ctx, to=delegate_user.email,
reply_to=request.user.email,
with_html=False)
messages.info(request, _('%s has been notified.') % delegate_user.email)
except NotificationFailure:
messages.warning(request, _('Unable to notify %s of \
the delegation suppression, you should do it yourself') % delegate_user.email)
return redirect('delegate')
ctx = {
'delegations': delegations,
'form': form,
}
return render(request, 'docbow/delegate.html', ctx)
@login_required
def inbox_by_document(request, document_id):
mailbox = get_object_or_404(Mailbox, owner=request.user, outbox=False,
document=document_id)
return redirect('inbox-message', mailbox.id, permanent=True)