OpenSlides/openslides/utils/views.py

627 lines
19 KiB
Python
Raw Normal View History

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
openslides.utils.views
~~~~~~~~~~~~~~~~~~~~~~
2012-04-25 22:29:19 +02:00
Views for OpenSlides.
2013-09-25 12:53:44 +02:00
:copyright: 20112013 by OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
2012-12-14 14:21:53 +01:00
import json
2013-02-02 10:59:07 +01:00
from cStringIO import StringIO
from reportlab.platypus import SimpleDocTemplate, Spacer
from reportlab.lib.units import cm
2013-09-25 12:53:44 +02:00
from django.conf import settings
2012-03-16 14:31:59 +01:00
from django.contrib import messages
2012-02-20 17:46:45 +01:00
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
2013-09-25 12:53:44 +02:00
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.core.urlresolvers import reverse
2012-03-18 17:11:58 +01:00
from django.dispatch import receiver
2013-09-25 12:53:44 +02:00
from django.http import (HttpResponse, HttpResponseRedirect,
HttpResponseServerError)
from django.template import RequestContext
from django.template.loader import render_to_string
2013-09-25 12:53:44 +02:00
from django.utils.decorators import method_decorator
from django.utils.importlib import import_module
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from django.views import generic as django_views
2012-02-20 17:46:45 +01:00
from django.views.generic.detail import SingleObjectMixin
2012-04-13 11:35:53 +02:00
from django.views.generic.list import TemplateResponseMixin
2013-09-25 12:53:44 +02:00
from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Spacer
2012-02-20 17:46:45 +01:00
2013-09-25 12:53:44 +02:00
from .pdf import firstPage, laterPages
from .signals import template_manipulation
from .utils import html_strong
2012-04-15 11:24:40 +02:00
2012-04-14 18:13:55 +02:00
NO_PERMISSION_REQUIRED = 'No permission required'
2012-02-20 17:46:45 +01:00
2013-09-25 12:53:44 +02:00
View = django_views.View
2012-04-13 11:35:53 +02:00
2012-02-20 17:46:45 +01:00
class LoginMixin(object):
2013-09-25 12:53:44 +02:00
"""
Mixin for Views, that only can be viseted from users how are logedin.
"""
2012-02-20 17:46:45 +01:00
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
2013-09-25 12:53:44 +02:00
"""
Check if the user is loged in.
"""
2012-02-20 17:46:45 +01:00
return super(LoginMixin, self).dispatch(request, *args, **kwargs)
class PermissionMixin(object):
2013-09-25 12:53:44 +02:00
"""
Mixin for views, that only can be visited from users with special rights.
Set the attribute 'permission_required' to the required permission string.
"""
2012-04-14 18:13:55 +02:00
permission_required = NO_PERMISSION_REQUIRED
2012-02-20 17:46:45 +01:00
2012-10-24 12:45:40 +02:00
def has_permission(self, request, *args, **kwargs):
2013-09-25 12:53:44 +02:00
"""
Checks if the user has the required permission.
"""
2012-04-14 18:13:55 +02:00
if self.permission_required == NO_PERMISSION_REQUIRED:
2012-04-15 11:24:40 +02:00
return True
2012-02-20 17:46:45 +01:00
else:
2012-04-15 11:24:40 +02:00
return request.user.has_perm(self.permission_required)
2012-02-20 17:46:45 +01:00
2012-04-15 11:24:40 +02:00
def dispatch(self, request, *args, **kwargs):
2013-09-25 12:53:44 +02:00
"""
Check if the user has the permission.
If the user is not logged in, redirect the user to the login page.
"""
2012-10-24 12:45:40 +02:00
if not self.has_permission(request, *args, **kwargs):
2012-03-18 14:33:53 +01:00
if not request.user.is_authenticated():
path = request.get_full_path()
return HttpResponseRedirect(
"%s?next=%s" % (settings.LOGIN_URL, path))
2012-02-20 17:46:45 +01:00
else:
2013-09-25 12:53:44 +02:00
raise PermissionDenied
2013-04-28 09:37:15 +02:00
return super(PermissionMixin, self).dispatch(request, *args, **kwargs)
2012-02-20 17:46:45 +01:00
class AjaxMixin(object):
2013-09-25 12:53:44 +02:00
"""
Mixin to response to an ajax request with an json object.
"""
def get_ajax_context(self, **kwargs):
2013-09-25 12:53:44 +02:00
"""
Returns a dictonary with the context for the ajax response.
"""
return kwargs
def ajax_get(self, request, *args, **kwargs):
2013-09-25 12:53:44 +02:00
"""
Returns the HttpResponse.
"""
return HttpResponse(json.dumps(self.get_ajax_context()))
2013-01-06 12:07:37 +01:00
class ExtraContextMixin(object):
2013-09-25 12:53:44 +02:00
"""
Mixin to send the signal 'template_manipulation' to add extra content to the
context of the view.
For example this is used to add the main menu of openslides.
"""
2013-01-06 12:07:37 +01:00
def get_context_data(self, **kwargs):
2013-09-25 12:53:44 +02:00
"""
Sends the signal.
"""
2013-01-06 12:07:37 +01:00
context = super(ExtraContextMixin, self).get_context_data(**kwargs)
template_manipulation.send(
sender=self.__class__, request=self.request, context=context)
return context
class UrlMixin(object):
url_name_args = None
2013-09-25 12:53:44 +02:00
def get_url(self, url_name=None, url=None, args=None, use_absolute_url_link=None):
"""
Returns an url.
2013-09-25 12:53:44 +02:00
Tries
1. to use the reverse for the attribute 'url_name',
2. to use the attribute 'url' or
3. to use self.object.get_absolute_url().
2013-01-26 16:33:55 +01:00
2013-09-25 12:53:44 +02:00
Uses the attribute 'use_absolute_url_link' as argument for
get_absolute_url in the third step. If the attribute is 'None' then
the default value of get_absolute_url is used.
Raises ImproperlyConfigured if no url can be found.
"""
if url_name:
value = reverse(url_name, args=args or [])
elif url:
value = url
2013-01-06 12:07:37 +01:00
else:
try:
2013-09-25 12:53:44 +02:00
if use_absolute_url_link is None:
value = self.object.get_absolute_url()
else:
value = self.object.get_absolute_url(use_absolute_url_link)
2013-01-06 12:07:37 +01:00
except AttributeError:
raise ImproperlyConfigured(
2013-09-25 12:53:44 +02:00
'No url to redirect to. See openslides.utils.views.UrlMixin for more details.')
return value
2013-02-01 12:51:01 +01:00
def get_url_name_args(self):
"""
2013-09-25 12:53:44 +02:00
Returns the arguments for the url name.
Default is an empty list or [self.object.pk] if this exist.
"""
if self.url_name_args is not None:
2013-09-25 12:53:44 +02:00
value = self.url_name_args
else:
try:
pk = self.object.pk
except AttributeError:
value = []
else:
if pk:
value = [pk]
else:
value = []
return value
2013-01-06 12:07:37 +01:00
2013-09-25 12:53:44 +02:00
class FormMixin(UrlMixin):
"""
2013-09-25 12:53:44 +02:00
Mixin for views with forms.
"""
2013-03-18 12:34:47 +01:00
2013-09-25 12:53:44 +02:00
use_apply = True
success_url_name = None
success_url = None
success_message = None
apply_url_name = None
apply_url = None
error_message = ugettext_lazy('Please check the form for errors.')
2013-09-25 12:53:44 +02:00
def get_apply_url(self):
"""
2013-09-25 12:53:44 +02:00
Returns the url when the user clicks on 'apply'.
"""
2013-09-25 12:53:44 +02:00
return self.get_url(self.apply_url_name, self.apply_url,
args=self.get_url_name_args(),
use_absolute_url_link='update')
2013-09-25 12:53:44 +02:00
def get_success_url(self):
2013-03-18 12:34:47 +01:00
"""
2013-09-25 12:53:44 +02:00
Returns the url when the user submits a form.
Redirects to get_apply_url if self.use_apply is True
2013-03-18 12:34:47 +01:00
"""
2013-09-25 12:53:44 +02:00
if self.use_apply and 'apply' in self.request.POST:
value = self.get_apply_url()
else:
value = self.get_url(self.success_url_name, self.success_url,
args=self.get_url_name_args())
return value
2013-09-25 12:53:44 +02:00
def form_valid(self, form):
value = super(FormMixin, self).form_valid(form)
messages.success(self.request, self.get_success_message())
return value
2013-09-25 12:53:44 +02:00
def form_invalid(self, form):
value = super(FormMixin, self).form_invalid(form)
messages.error(self.request, self.get_error_message())
return value
2013-09-25 12:53:44 +02:00
def get_success_message(self):
return self.success_message
2013-09-25 12:53:44 +02:00
def get_error_message(self):
return self.error_message
2012-10-28 19:59:41 +01:00
2013-09-25 12:53:44 +02:00
class ModelFormMixin(FormMixin):
"""
Mixin for FormViews.
"""
def form_valid(self, form):
"""
Called if the form is valid.
1. saves the form into the model,
2. calls 'self.manipulate_object,
3. saves the object in the database,
4. calls self.post_save.
"""
self.object = form.save(commit=False)
self.manipulate_object(form)
self.object.save()
self.post_save(form)
messages.success(self.request, self.get_success_message())
return HttpResponseRedirect(self.get_success_url())
2012-10-28 19:59:41 +01:00
2013-09-25 12:53:44 +02:00
def manipulate_object(self, form):
"""
Called before the object is saved into the database.
"""
2012-10-28 19:59:41 +01:00
pass
2013-09-25 12:53:44 +02:00
def post_save(self, form):
"""
Called after the object is saved into the database.
"""
form.save_m2m()
2013-09-25 12:53:44 +02:00
class TemplateView(PermissionMixin, ExtraContextMixin, django_views.TemplateView):
"""
View to return with an template.
"""
2013-01-06 12:07:37 +01:00
pass
2012-02-20 17:46:45 +01:00
2012-04-11 10:58:59 +02:00
2013-09-25 12:53:44 +02:00
class ListView(PermissionMixin, ExtraContextMixin, django_views.ListView):
"""
View to show a list of model objects.
"""
2013-01-06 12:07:37 +01:00
pass
2012-04-11 10:58:59 +02:00
2013-02-02 10:52:13 +01:00
class AjaxView(PermissionMixin, AjaxMixin, View):
2013-09-25 12:53:44 +02:00
"""
View for ajax requests.
"""
2012-04-12 14:20:05 +02:00
def get(self, request, *args, **kwargs):
return self.ajax_get(request, *args, **kwargs)
2012-04-12 14:20:05 +02:00
2013-09-25 12:53:44 +02:00
class RedirectView(PermissionMixin, AjaxMixin, UrlMixin, django_views.RedirectView):
"""
View to redirect to another url.
The initial value of url_name_args is None, but the default given by
the used get_url_name_args method is [self.object.pk] if it exist, else
an empty list. Set url_name_args to an empty list, if you use an url
name which does not need any arguments.
"""
2012-02-20 17:46:45 +01:00
permanent = False
allow_ajax = False
2013-02-01 12:51:01 +01:00
url_name = None
2012-02-20 17:46:45 +01:00
def pre_redirect(self, request, *args, **kwargs):
2013-09-25 12:53:44 +02:00
"""
Called before the redirect.
"""
# TODO: Also call this method on post-request.
# Add pre_get_redirect for get requests.
2012-02-20 17:46:45 +01:00
pass
def pre_post_redirect(self, request, *args, **kwargs):
2013-09-25 12:53:44 +02:00
"""
Called before the redirect, if it is a post request.
"""
2012-02-20 17:46:45 +01:00
pass
def get(self, request, *args, **kwargs):
if request.method == 'GET':
self.pre_redirect(request, *args, **kwargs)
2012-02-20 17:46:45 +01:00
elif request.method == 'POST':
self.pre_post_redirect(request, *args, **kwargs)
2013-09-25 12:53:44 +02:00
if request.is_ajax() and self.allow_ajax:
return self.ajax_get(request, *args, **kwargs)
2012-02-20 17:46:45 +01:00
return super(RedirectView, self).get(request, *args, **kwargs)
def get_redirect_url(self, **kwargs):
2013-09-25 12:53:44 +02:00
"""
Returns the url to which the redirect should go.
"""
return self.get_url(self.url_name, self.url,
args=self.get_url_name_args())
class QuestionView(RedirectView):
"""
Mixin for questions to the requesting user.
The BaseView has to be a RedirectView.
"""
question_message = ugettext_lazy('Are you sure?')
final_message = ugettext_lazy('Thank you for your answer.')
answer_options = [('yes', ugettext_lazy("Yes")), ('no', ugettext_lazy("No"))]
question_url_name = None
question_url = None
def get_redirect_url(self, **kwargs):
"""
Returns the url to which the view should redirect.
"""
if self.request.method == 'GET':
url = self.get_url(self.question_url_name, self.question_url,
args=self.get_url_name_args())
2013-02-01 12:51:01 +01:00
else:
2013-09-25 12:53:44 +02:00
url = super(QuestionView, self).get_redirect_url()
return url
2012-02-20 17:46:45 +01:00
2013-09-25 12:53:44 +02:00
def pre_redirect(self, request, *args, **kwargs):
"""
2013-09-25 12:53:44 +02:00
Prints the question in a GET request.
"""
2013-09-25 12:53:44 +02:00
self.confirm_form()
2013-09-25 12:53:44 +02:00
def get_question_message(self):
"""
Returns the question.
"""
return self.question_message
2012-02-20 17:46:45 +01:00
2013-09-25 12:53:44 +02:00
def get_answer_options(self):
"""
Returns the possible answers.
2012-03-16 14:31:59 +01:00
2013-09-25 12:53:44 +02:00
This is a list of tubles. The first element of the tuble is the key,
the second element is shown to the user.
"""
return self.answer_options
2012-03-16 14:31:59 +01:00
2013-09-25 12:53:44 +02:00
def confirm_form(self):
"""
Returns the form to show in the message.
"""
option_fields = "\n".join([
'<button type="submit" class="btn btn-mini" name="%s">%s</button>'
% (option[0], unicode(option[1]))
for option in self.get_answer_options()])
messages.warning(
self.request,
"""
%(message)s
<form action="%(url)s" method="post">
<input type="hidden" value="%(csrf)s" name="csrfmiddlewaretoken">
%(option_fields)s
</form>
""" % {'message': self.get_question_message(),
'url': self.request.path,
'csrf': csrf(self.request)['csrf_token'],
'option_fields': option_fields})
2013-01-06 12:07:37 +01:00
2013-09-25 12:53:44 +02:00
def pre_post_redirect(self, request, *args, **kwargs):
"""
Calls the method for the answer the user clicked.
2012-02-20 17:46:45 +01:00
2013-09-25 12:53:44 +02:00
The method name is on_clicked_ANSWER where ANSWER is the key from the
clicked answer. See get_answer_options. If this method is not defined,
raises a NotImplementedError.
2013-01-26 15:25:54 +01:00
2013-09-25 12:53:44 +02:00
If the method returns True, then the success message is printed to the
user.
"""
method_name = 'on_clicked_%s' % self.get_answer()
method = getattr(self, method_name, None)
if method is None:
pass
else:
method()
self.create_final_message()
2012-03-16 14:31:59 +01:00
2013-09-25 12:53:44 +02:00
def get_answer(self):
"""
Returns the key of the clicked answer.
2013-09-07 22:57:29 +02:00
2013-09-25 12:53:44 +02:00
Raises ImproperlyConfigured, if the answer is not one of
get_answer_options.
"""
for option_key, option_name in self.get_answer_options():
if option_key in self.request.POST:
return option_key
raise ImproperlyConfigured('%s is not a valid answer' % self.request.POST)
2013-09-25 12:53:44 +02:00
def get_final_message(self):
"""
Returns the message to show after the action.
Uses the attribute 'final_messsage' as default
"""
return self.final_message
def create_final_message(self):
"""
Creates a message.
"""
messages.success(self.request, self.get_final_message())
class FormView(PermissionMixin, ExtraContextMixin, FormMixin,
django_views.FormView):
"""
View for forms.
"""
pass
class UpdateView(PermissionMixin, ExtraContextMixin,
ModelFormMixin, django_views.UpdateView):
"""
View to update an model object.
"""
2013-09-07 22:57:29 +02:00
2012-07-07 14:01:40 +02:00
def get_success_message(self):
2013-09-07 22:57:29 +02:00
if self.success_message is None:
message = _('%s was successfully modified.') % html_strong(self.object)
else:
message = self.success_message
return message
2012-07-07 14:01:40 +02:00
2012-02-20 17:46:45 +01:00
2013-09-25 12:53:44 +02:00
class CreateView(PermissionMixin, ExtraContextMixin,
ModelFormMixin, django_views.CreateView):
"""
View to create a model object.
"""
2013-09-07 22:57:29 +02:00
2012-07-07 14:01:40 +02:00
def get_success_message(self):
2013-09-07 22:57:29 +02:00
if self.success_message is None:
message = _('%s was successfully created.') % html_strong(self.object)
else:
message = self.success_message
return message
2012-07-07 14:01:40 +02:00
2012-02-20 17:46:45 +01:00
2013-09-25 12:53:44 +02:00
class DeleteView(SingleObjectMixin, QuestionView):
"""
View to delete an model object.
"""
success_url = None
2013-02-01 13:24:30 +01:00
success_url_name = None
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super(DeleteView, self).get(request, *args, **kwargs)
2013-02-01 13:24:30 +01:00
def get_redirect_url(self, **kwargs):
2013-09-25 12:53:44 +02:00
"""
Returns the url on which the delete dialog is shown and the url after
the deleten.
On GET-requests and on aborted POST-requests, redirect to the detail
view as default. The attributes question_url_name or question_url can
define other urls.
"""
if self.request.method == 'GET' or self.get_answer() == 'no':
url = self.get_url(self.question_url_name, self.question_url,
args=self.get_url_name_args())
2013-02-01 13:24:30 +01:00
else:
2013-09-25 12:53:44 +02:00
url = self.get_url(self.success_url_name, self.success_url,
args=self.get_url_name_args())
return url
2013-02-01 13:24:30 +01:00
2013-09-25 12:53:44 +02:00
def get_question_message(self):
"""
Returns the question for the delete dialog.
"""
2012-10-28 19:59:41 +01:00
return _('Do you really want to delete %s?') % html_strong(self.object)
2013-09-25 12:53:44 +02:00
def on_clicked_yes(self):
"""
Deletes the object.
"""
2012-10-28 19:59:41 +01:00
self.object.delete()
2013-09-25 12:53:44 +02:00
def get_final_message(self):
"""
Prints the success message to the user.
"""
2012-10-28 19:59:41 +01:00
return _('%s was successfully deleted.') % html_strong(self.object)
2012-04-12 20:11:05 +02:00
2012-02-20 17:46:45 +01:00
2013-09-25 12:53:44 +02:00
class DetailView(PermissionMixin, ExtraContextMixin, django_views.DetailView):
"""
View to show an model object.
"""
pass
2012-04-15 10:02:53 +02:00
2012-04-15 09:55:21 +02:00
2012-03-18 14:33:53 +01:00
class PDFView(PermissionMixin, View):
2013-09-25 12:53:44 +02:00
"""
View to generate an PDF.
"""
2012-04-14 18:13:55 +02:00
filename = _('undefined-filename')
top_space = 3
2012-04-14 18:13:55 +02:00
document_title = None
def get_top_space(self):
return self.top_space
2012-04-14 10:54:22 +02:00
def get_document_title(self):
2012-07-23 12:35:15 +02:00
if self.document_title:
return unicode(self.document_title)
else:
2012-07-23 22:59:31 +02:00
return ''
2012-04-14 10:54:22 +02:00
def get_filename(self):
2012-04-18 20:57:44 +02:00
return self.filename
def get_template(self, buffer):
return SimpleDocTemplate(buffer)
def build_document(self, pdf_document, story):
pdf_document.build(
story, onFirstPage=firstPage, onLaterPages=laterPages)
def render_to_response(self, filename):
response = HttpResponse(mimetype='application/pdf')
2012-04-18 18:54:48 +02:00
filename = u'filename=%s.pdf;' % self.get_filename()
response['Content-Disposition'] = filename.encode('utf-8')
buffer = StringIO()
pdf_document = self.get_template(buffer)
2012-04-14 10:54:22 +02:00
pdf_document.title = self.get_document_title()
story = [Spacer(1, self.get_top_space() * cm)]
self.append_to_pdf(story)
self.build_document(pdf_document, story)
pdf = buffer.getvalue()
buffer.close()
response.write(pdf)
return response
def get(self, request, *args, **kwargs):
return self.render_to_response(self.get_filename())
2011-09-03 19:16:43 +02:00
def server_error(request, template_name='500.html'):
"""
500 error handler.
Templates: `500.html`
"""
return HttpResponseServerError(render_to_string(
template_name, context_instance=RequestContext(request)))
2012-03-18 17:11:58 +01:00
@receiver(template_manipulation, dispatch_uid="send_register_tab")
def send_register_tab(sender, request, context, **kwargs):
"""
Receiver to the template_manipulation signal. Collects from the file
views.py in all apps the tabs setup by the function register_tab.
Inserts the tab objects and also the extra_stylefiles to the context.
"""
2012-03-18 17:11:58 +01:00
tabs = []
if 'extra_stylefiles' in context:
extra_stylefiles = context['extra_stylefiles']
else:
extra_stylefiles = []
2013-09-25 12:53:44 +02:00
context.setdefault('extra_javascript', [])
# TODO: Do not go over the filesystem by any request
2012-03-18 17:11:58 +01:00
for app in settings.INSTALLED_APPS:
try:
mod = import_module(app + '.views')
tab = mod.register_tab(request)
tabs.append(tab)
if tab.stylefile:
extra_stylefiles.append(tab.stylefile)
2012-03-18 17:11:58 +01:00
except (ImportError, AttributeError):
continue
context.update({
'tabs': tabs,
'extra_stylefiles': extra_stylefiles})