import smtplib from itertools import count import socket import os.path import collections import operator import datetime 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, HttpResponseRedirect, Http404 from django.db.models.query import Q from django.contrib.auth.models import User from django.contrib import messages from django.core.mail import EmailMessage from django.utils.translation import ugettext as _, ugettext_noop as N_, ungettext from django.forms.forms import NON_FIELD_ERRORS from django.views.generic.list import MultipleObjectMixin, ListView from django.views.generic.base import View from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.cache import never_cache from django.utils import timezone from BeautifulSoup import BeautifulSoup import django_tables2.views as tables_views from .forms import (FileForm, AnonymousContactForm, ContactForm, ForwardingForm, FilterForm) from .models import (Mailbox, username, AttachedFile, SendingLimitation, MailingList, DeletedMailbox, is_guest, Document, FileType) from .decorators import no_delegate, never_cache, as_delegate from .logger_adapter import get_logger from . import tables from . import app_settings from . import unicodecsv from . import profile_views from .utils import date_to_aware_datetime 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 mailboxes(request): return Mailbox.objects.filter(Q(owner=request.user) |Q(owner__delegations_to__to=request.user)).distinct() def get_mailbox(request, mailbox_id, back='inbox'): owners = list(request.user.delegations_by \ .values_list('by__id', flat=True)) owners.append(request.user.id) try: return Mailbox.objects.get(pk=mailbox_id, owner__in=owners, outbox=back!='inbox') except Mailbox.DoesNotExist: return None def user_mailing_list_names(user): return user.mailing_lists.values_list('name', flat=True) def get_file_form_kwargs(request): user = request.user kwargs = {} user_lists = MailingList.objects.is_member_of(user) if SendingLimitation.objects.filter(mailing_list__in=user_lists): lists = MailingList.objects \ .filter(lists_limitation__mailing_list__in=user_lists) \ .distinct() \ .order_by('name') users = User.objects.filter(mailing_lists__in=lists, is_active=True) if lists: kwargs['list_qs'] = lists kwargs['user_qs'] = users return kwargs def get_filetype_limitation(user): # find delegation relations if is_guest(user): user = user.delegations_by.get().by delegators = [] else: delegators = User.objects.filter( Q(id=user.id) | Q(delegations_to__to=user)).distinct() # if user has basically no limitation, do not limit him user_lists = MailingList.objects.is_member_of(user) own_limitations = FileType.objects \ .filter(filetype_limitation__mailing_list__in=user_lists) \ .distinct() \ .order_by('name') if not own_limitations.exists(): return FileType.objects.none() if delegators: user_lists = MailingList.objects.are_member_of([user] + list(delegators)) return FileType.objects \ .filter(filetype_limitation__mailing_list__in=user_lists) \ .distinct() \ .order_by('name') else: return own_limitations @login_required @never_cache def send_file(request, file_type_id): file_type = get_object_or_404(FileType, id=file_type_id) reply_to = None if 'reply_to' in request.GET: reply_to = get_mailbox(request, request.GET['reply_to']) if is_guest(request.user): default_sender = request.user.delegations_by.all()[0].by delegators = [] else: default_sender = request.user delegators = User.objects.filter( Q(id=request.user.id) | Q(delegations_to__to=request.user)).distinct() limitations = get_filetype_limitation(request.user) if limitations: if not limitations.filter(id=file_type.id).exists(): return redirect('send-file-selector') if request.method == 'POST': if 'send' not in request.POST: return redirect('outbox') form = FileForm(request.POST, request.FILES, default_sender=default_sender, user=request.user, delegations=delegators, reply_to=reply_to, file_type=file_type, **get_file_form_kwargs(request)) try: if form.is_valid(): new_send = form.save(commit=False) 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 recipients_count = new_send.post() request.record('create-document', 'sent document {document} ' 'delivered to {recipients_count}', document=new_send, recipients_count=recipients_count) return redirect('outbox') except Exception: logger = get_logger(request) logger.exception('unable to create a new document') form._errors.setdefault(NON_FIELD_ERRORS, form.error_class()) \ .append(_('An internal error occured, administrators ' 'have been notified; sending seems blocked at the moment. You should ' 'try agrain later. If it still does not work then, contact ' 'your administrator.')) else: form = FileForm(default_sender=default_sender, user=request.user, delegations=delegators, reply_to=reply_to, file_type=file_type, **get_file_form_kwargs(request)) if reply_to: form.helper.form_action = '?reply_to=%s' % reply_to.id return render(request, 'docbow/send_file.html', {'form': form, 'view_name': 'send-file', 'reply_to': reply_to}) @never_cache def upload(request, attached_file): response = HttpResponse(attached_file.content.chunks(), mimetype='application/octet-stream') response['Content-disposition'] = 'attachment' return response @login_required @never_cache def message_attached_file(request, mailbox_id, attached_file, back='inbox'): ''' Download attached files, verify that the user has access to the document before, otherwise return 404. ''' mailbox = get_mailbox(request, mailbox_id, back=back) if not mailbox: raise Http404 attached_file = get_object_or_404(AttachedFile, document__mailboxes__id=mailbox_id, pk=attached_file) Mailbox.objects.filter(pk=mailbox_id).update(seen=True) delegate = mailbox.owner if mailbox.owner != request.user else None request.record('download', 'download attached file {attached_file} of document {document}', attached_file=attached_file, document=attached_file.document, user=mailbox.owner, delegate=delegate) return upload(request, attached_file) 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 @login_required @never_cache def message(request, mailbox_id, back='inbox'): mailbox = get_mailbox(request, mailbox_id, back) if not mailbox: raise Http404 document = mailbox.document if back == 'inbox': back_pair = (reverse('inbox'), gettext_noop('back to inbox')) else: back_pair = (reverse('outbox'), gettext_noop('back to outbox')) ctx = { 'document': document, 'view_name': back, 'back': back_pair, 'mailbox': mailbox } if back == 'inbox': if request.method == 'POST': form = ForwardingForm(request.POST, user=request.user) if form.is_valid(): users, lists = form_to_user_and_list(form) recipients_count, forwarded_document = document.forward( form.cleaned_data['sender'], lists, users) request.record('forward-document', 'forwarded document ' '{document} as new document {new_document}', document=forwarded_document.from_document, new_document=forwarded_document.to_document) msg = ungettext('Document forwarded to {recipients_count} ' 'recipient.', 'Document forwarded to {recipients_count} ' 'recipients.', recipients_count) \ .format(recipients_count=recipients_count) messages.info(request, msg) return HttpResponseRedirect('') else: form = ForwardingForm(user=request.user) ctx['form'] = form ctx['delegated'] = mailbox.owner != request.user delegate = mailbox.owner if ctx['delegated'] else None ctx['attached_files'] = attached_files = [] for attached_file in document.attached_files.order_by('kind__position', 'kind__name', 'id'): if attached_files and attached_files[-1][0] == attached_file.kind: attached_files[-1][1].append(attached_file) else: attached_files.append((attached_file.kind, [attached_file])) request.record('message-view', 'looked at document {document}', document=document, user=mailbox.owner, delegate=delegate) return render(request, 'docbow/message.html', ctx) def get_help_content(pagename): filepath = os.path.join(settings.STATIC_ROOT, 'help', pagename) parsed_doc = BeautifulSoup(file(filepath).read()) page = parsed_doc.findAll(True, role='main')[0] for t in page.findAll('h4'): t.name = 'h6' for t in page.findAll('h3'): t.name = 'h5' for t in page.findAll('h2'): t.name = 'h4' for t in page.findAll('h1'): t.name = 'h3' return unicode(page) @login_required def help(request, pagename='index.html'): if pagename.endswith('.html'): return render(request, 'docbow/help.html', { 'view_name': 'help', 'content': get_help_content(pagename) }) else: filepath = os.path.join(settings.STATIC_ROOT, 'help', pagename) response = HttpResponse(content=file(filepath)) response['Content-Type'] = 'image/png' return response 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): msg = N_('unable to send the contact mail because there is no ' 'administrator group to receive them') request.error_record('error', msg) messages.error(request, _(msg)) 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)) request.record('contact', 'sent mail to administrators with ' 'subject "{subject}" and message "{message}", reply should be sent to email ' '{reply_to} or phone number "{phone}"', subject=subject, message=message, reply_to=reply_to, phone=cleaned_data.get('phone_number')) except smtplib.SMTPException, socket.error: logger.exception('unable to send mail to administrators') request.error_record('error', 'unable to send mail to administrators with ' 'subject "{subject}" and message "{message}", reply should be sent to email ' '{reply_to} or phone number "{phone}"', subject=subject, message=message, reply_to=reply_to, phone=cleaned_data.get('phone_number')) 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 @never_cache def delete(request, mailbox_id, back='inbox'): '''Remove a document from the inbox''' if is_guest(request.user): raise Http404 page = request.GET.get('page', 1) viewname = back + '-message-delete' back_pair = ('%s?page=%s' % (reverse(back), page), gettext_noop('back to %s' % back)) owners = list(request.user.delegations_by \ .values_list('by__id', flat=True)) owners.append(request.user.id) m = get_object_or_404(Mailbox, owner__id__in=owners, pk=mailbox_id) if request.method == 'GET': return render(request, 'docbow/delete.html', { 'document': m.document, 'back': back_pair, 'view_name': viewname }) else: if m.owner == request.user: m.deleted = True m.save() DeletedMailbox.objects.filter(mailbox=m).delete() request.record('delete-document', 'marked mailbox entry {mailbox} / document {document} as deleted', mailbox=m, document=m.document) else: DeletedMailbox.objects.create(mailbox=m, delegate=request.user) request.record('delete-document', 'marked mailbox entry {mailbox} / document {document} as deleted', mailbox=m, user=m.owner, delegate=request.user, document=m.document) 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 @login_required @never_cache def inbox_by_document(request, document_id): mailbox = get_object_or_404(mailboxes(request), document=document_id, outbox=False) return redirect('inbox-message', mailbox.id, permanent=True) @login_required @never_cache def outbox_by_document(request, document_id): mailbox = get_object_or_404(mailboxes(request), document=document_id, outbox=True) return redirect('outbox-message', mailbox.id, permanent=True) from django.contrib.auth import SESSION_KEY from django import http def su(request, username, redirect_url='/'): '''Allows changing user for super-users. Super-user status is kept using a flag on the session. ''' if request.user.is_superuser or request.session.get('has_superuser_power'): su_user = get_object_or_404(User, username=username) if su_user.is_active: request.session[SESSION_KEY] = su_user.id request.session['has_superuser_power'] = True return http.HttpResponseRedirect(redirect_url) else: return http.HttpResponseRedirect('/') @never_cache def mailing_lists(request, template='docbow/mailing-lists.html'): mailing_lists = MailingList.objects.active().filter(Q(members__isnull=False) | Q(mailing_list_members__isnull=False)).distinct().order_by('name') return render(request, template, { 'mailing_lists': mailing_lists }) def robots(request): output = '''User-Agent: * Disallow: / ''' return HttpResponse(output, mimetype='text/plain') def password_reset_done(request): messages.info(request, _('Email with password reset instruction has been sent.')) return redirect('auth_login') class DateFilterMixinView(object): def get_filter_form(self): if not hasattr(self, '_form'): if 'clear' in self.request.GET: form = FilterForm(request=self.request) else: form = FilterForm(data=self.request.GET, request=self.request) self._form = form return self._form def get_context_data(self, **kwargs): context = super(DateFilterMixinView, self).get_context_data(**kwargs) context['filter_form'] = self.get_filter_form() return context def get_queryset(self): qs = super(DateFilterMixinView, self).get_queryset() filter_form = self.get_filter_form() if filter_form.is_valid(): not_before = filter_form.cleaned_data.get('not_before') not_after = filter_form.cleaned_data.get('not_after') if not_before is not None: not_before = date_to_aware_datetime(not_before) qs = qs.filter(document__date__gte=not_before) if not_after is not None: not_after += datetime.timedelta(days=1) not_after = date_to_aware_datetime(not_after) qs = qs.filter(document__date__lt=not_after) return qs class ExtraContextMixin(object): extra_ctx = {} def get_context_data(self, **kwargs): context = super(ExtraContextMixin, self).get_context_data(**kwargs) context.update(self.extra_ctx) return context class MailboxQuerysetMixin(object): def get_queryset(self): qs = super(MailboxQuerysetMixin, self).get_queryset() user = self.request.user delegations = user.delegations_by.select_related('by') if delegations: delegators = [delegation.by for delegation in delegations] delegators.append(user) deleted_ids = DeletedMailbox.objects.all() deleted_ids = deleted_ids.filter(delegate=user) deleted_ids = deleted_ids.values_list('mailbox_id', flat=True) qs = qs.filter(deleted=False, owner__in=delegators, outbox=self.outbox) \ .exclude(id__in=deleted_ids) else: qs = qs.filter(deleted=False, owner=user, outbox=self.outbox) return qs class MailboxView(ExtraContextMixin, MailboxQuerysetMixin, tables_views.SingleTableView): model = Mailbox table_class = tables.MailboxTable table_pagination = { 'per_page': app_settings.DOCBOW_MAILBOX_PER_PAGE, } class CSVMultipleObjectMixin(object): mapping = () filename = '' def get_header(self): return [ column['caption'] for column in self.mapping ] def get_cell(self, mailbox, attribute): value = operator.attrgetter(attribute)(mailbox) if callable(value): value = value() return unicode(value) def get_row(self, mailbox): for column in self.mapping: yield self.get_cell(mailbox, column['attribute']) def get_rows(self): qs = self.get_queryset() for mailbox in qs: yield list(self.get_row(mailbox)) def get(self, request, *args, **kwargs): response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = 'attachment; filename="%s"' % self.filename writer = unicodecsv.UnicodeWriter(response, encoding='utf-8') writer.writerow(self.get_header()) writer.writerows(self.get_rows()) return response class CSVMailboxView(CSVMultipleObjectMixin, ExtraContextMixin, MailboxQuerysetMixin, MultipleObjectMixin, tables_views.SingleTableMixin, View): model = Mailbox @property def filename(self): return '{prefix}-{user}-{date}.csv'.format( prefix=self.filename_prefix, user=self.request.user, date=datetime.date.today()) def get_header(self): table = self.get_table() for column in table.columns: yield column.header def get_row(self, row): for column, cell in row.items(): yield cell def get_rows(self): table = self.get_table() for table_row in table.rows: yield self.get_row(table_row) class CSVInboxView(CSVMailboxView): outbox = False table_class = tables.InboxCsvTable filename_prefix = 'inbox' inbox_csv = CSVInboxView.as_view() class CSVOutboxView(CSVMailboxView): outbox = True table_class = tables.OutboxCsvTable filename_prefix = 'outbox' outbox_csv = CSVOutboxView.as_view() class InboxView(DateFilterMixinView, MailboxView): outbox = False table_class = tables.InboxTable template_name = 'docbow/inbox_list.html' extra_ctx = { 'view_name': 'inbox', } inbox_view = login_required(InboxView.as_view()) class OutboxView(DateFilterMixinView, MailboxView): outbox = True table_class = tables.OutboxTable template_name = 'docbow/outbox_list.html' extra_ctx = { 'view_name': 'outbox', } outbox_view = login_required(OutboxView.as_view()) class SendFileSelectorView(ListView): template_name = 'docbow/send_file_selector.html' model = FileType def get_queryset(self): qs = super(SendFileSelectorView, self).get_queryset() limitations = get_filetype_limitation(self.request.user) if limitations: return limitations return qs send_file_selector = login_required(SendFileSelectorView.as_view()) delegate = login_required(no_delegate(never_cache(profile_views.DelegateView.as_view()))) password_change = sensitive_post_parameters()(never_cache(login_required(as_delegate(profile_views.PasswordChangeView.as_view())))) profile = login_required(profile_views.FullProfileView.as_view())