diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index f306fc727..c0a59c7c1 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -184,16 +184,12 @@ class SetClosed(RedirectView, SingleObjectMixin): model = Item def get_ajax_context(self, **kwargs): - context = super(SetClosed, self).get_ajax_context(**kwargs) - closed = kwargs['closed'] + closed = self.kwargs['closed'] if closed: - link = reverse('item_open', args=[self.object.id]) + link = reverse('item_open', args=[self.object.pk]) else: - link = reverse('item_close', args=[self.object.id]) - context.update({ - 'closed': kwargs['closed'], - 'link': link}) - return context + link = reverse('item_close', args=[self.object.pk]) + return super(SetClosed, self).get_ajax_context(closed=closed, link=link) def pre_redirect(self, request, *args, **kwargs): self.object = self.get_object() @@ -214,6 +210,7 @@ class ItemUpdate(UpdateView): model = Item context_object_name = 'item' success_url_name = 'item_overview' + url_name_args = [] def get_form_class(self): if self.object.content_object: @@ -233,6 +230,7 @@ class ItemCreate(CreateView): context_object_name = 'item' form_class = ItemForm success_url_name = 'item_overview' + url_name_args = [] class ItemDelete(DeleteView): diff --git a/openslides/assignment/views.py b/openslides/assignment/views.py index 822c996ac..c84e12a81 100644 --- a/openslides/assignment/views.py +++ b/openslides/assignment/views.py @@ -25,11 +25,10 @@ from django.utils.translation import ungettext, ugettext as _ from openslides.utils.pdf import stylesheet from openslides.utils.template import Tab -from openslides.utils.utils import gen_confirm_form from openslides.utils.views import ( CreateView, DeleteView, RedirectView, UpdateView, ListView, PDFView, - DetailView, View, PermissionMixin, SingleObjectMixin, QuestionMixin) + DetailView, View, PermissionMixin, SingleObjectMixin, QuestionView) from openslides.utils.person import get_person from openslides.utils.utils import html_strong from openslides.config.api import config @@ -150,8 +149,6 @@ class AssignmentRunView(SingleObjectMixin, PermissionMixin, View): class AssignmentRunDeleteView(SingleObjectMixin, RedirectView): model = Assignment url_name = 'assignment_detail' - success_message = _("You have withdrawn your candidature successfully. " - "You can not be nominated by other participants anymore.") def pre_redirect(self, *args, **kwargs): self.object = self.get_object() @@ -162,54 +159,49 @@ class AssignmentRunDeleteView(SingleObjectMixin, RedirectView): except Exception, e: messages.error(self.request, e) else: - messages.success(self.request, self.success_message) + messages.success(self.request, _( + 'You have withdrawn your candidature successfully. ' + 'You can not be nominated by other participants anymore.')) else: messages.error(self.request, _('The candidate list is already closed.')) -class AssignmentRunOtherDeleteView(SingleObjectMixin, QuestionMixin, - RedirectView): +class AssignmentRunOtherDeleteView(SingleObjectMixin, QuestionView): model = Assignment permission_required = 'assignment.can_manage_assignment' - question_url_name = 'assignment_detail' - success_url_name = 'assignment_detail' - success_message = '' - def pre_redirect(self, *args, **kwargs): - self._get_person_information(*args, **kwargs) + def get_question_message(self): + self._get_person_information() if not self.is_blocked: - message = _("Do you really want to withdraw %s from the election?") % html_strong(self.person) + question = _("Do you really want to withdraw %s from the election?") % html_strong(self.person) else: - message = _("Do you really want to unblock %s for the election?") % html_strong(self.person) - gen_confirm_form(self.request, message, reverse('assignment_delother', - args=[self.object.pk, kwargs['user_id']])) + question = _("Do you really want to unblock %s for the election?") % html_strong(self.person) + return question - def pre_post_redirect(self, *args, **kwargs): - self._get_person_information(*args, **kwargs) - if self.get_answer() == 'yes': - self.case_yes() - - def get_answer(self): - if 'submit' in self.request.POST: - return 'yes' - - def case_yes(self): + def on_clicked_yes(self): + self._get_person_information() try: self.object.delrun(self.person, blocked=False) except Exception, e: - messages.error(self.request, e) + self.error = e else: - messages.success(self.request, self.get_success_message()) + self.error = False - def get_success_message(self): - success_message = _("Candidate %s was withdrawn successfully.") % html_strong(self.person) + def create_final_message(self): + if self.error: + messages.error(self.request, self.error) + else: + messages.success(self.request, self.get_final_message()) + + def get_final_message(self): + message = _("Candidate %s was withdrawn successfully.") % html_strong(self.person) if self.is_blocked: - success_message = _("%s was unblocked successfully.") % html_strong(self.person) - return success_message + message = _("%s was unblocked successfully.") % html_strong(self.person) + return message - def _get_person_information(self, *args, **kwargs): + def _get_person_information(self): self.object = self.get_object() - self.person = get_person(kwargs.get('user_id')) + self.person = get_person(self.kwargs.get('user_id')) self.is_blocked = self.object.is_blocked(self.person) @@ -310,7 +302,7 @@ class AssignmentPollDeleteView(DeleteView): def get_redirect_url(self, **kwargs): return reverse('assignment_detail', args=[self.assignment.id]) - def get_success_message(self): + def get_final_message(self): return _('Ballot was successfully deleted.') diff --git a/openslides/global_settings.py b/openslides/global_settings.py index cbb6728fa..8de28da6b 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -69,11 +69,9 @@ STATICFILES_DIRS = ( fs2unicode(os.path.join(SITE_ROOT, 'static')), ) -#XXX: Note this setting (as well as our workaround finder) -# can be removed again once django-bug-#18404 has been resolved STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', - 'openslides.utils.staticfiles.AppDirectoriesFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', ) MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' diff --git a/openslides/mediafile/views.py b/openslides/mediafile/views.py index 6f80b4da6..009d7cb62 100644 --- a/openslides/mediafile/views.py +++ b/openslides/mediafile/views.py @@ -40,6 +40,7 @@ class MediafileCreateView(CreateView): model = Mediafile permission_required = 'mediafile.can_upload' success_url_name = 'mediafile_list' + url_name_args = [] def get_form(self, form_class): form_kwargs = self.get_form_kwargs() @@ -71,6 +72,7 @@ class MediafileUpdateView(UpdateView): permission_required = 'mediafile.can_manage' form_class = MediafileUpdateForm success_url_name = 'mediafile_list' + url_name_args = [] def get_form_kwargs(self, *args, **kwargs): form_kwargs = super(MediafileUpdateView, self).get_form_kwargs(*args, **kwargs) @@ -86,10 +88,10 @@ class MediafileDeleteView(DeleteView): permission_required = 'mediafile.can_manage' success_url_name = 'mediafile_list' - def case_yes(self, *args, **kwargs): + def on_clicked_yes(self, *args, **kwargs): """Deletes the file in the filesystem, if user clicks "Yes".""" self.object.mediafile.delete() - return super(MediafileDeleteView, self).case_yes(*args, **kwargs) + return super(MediafileDeleteView, self).on_clicked_yes(*args, **kwargs) def register_tab(request): diff --git a/openslides/motion/views.py b/openslides/motion/views.py index 69638b707..5ba0a2381 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -11,7 +11,6 @@ :copyright: (c) 2011–2013 by the OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ -from reportlab.platypus import SimpleDocTemplate from django.core.urlresolvers import reverse from django.contrib import messages @@ -20,26 +19,26 @@ from django.db.models import Model from django.http import Http404, HttpResponseRedirect from django.utils.text import slugify from django.utils.translation import ugettext as _, ugettext_lazy, ugettext_noop +from reportlab.platypus import SimpleDocTemplate -from openslides.agenda.views import ( - CreateRelatedAgendaItemView as _CreateRelatedAgendaItemView) -from openslides.config.api import config -from openslides.poll.views import PollFormView -from openslides.projector.api import get_active_slide, update_projector -from openslides.projector.projector import Widget from openslides.utils.pdf import stylesheet -from openslides.utils.template import Tab -from openslides.utils.utils import html_strong, htmldiff from openslides.utils.views import ( TemplateView, RedirectView, UpdateView, CreateView, DeleteView, PDFView, - DetailView, ListView, FormView, QuestionMixin, SingleObjectMixin) + DetailView, ListView, FormView, QuestionView, SingleObjectMixin) +from openslides.utils.template import Tab +from openslides.utils.utils import html_strong, htmldiff +from openslides.poll.views import PollFormView +from openslides.projector.api import get_active_slide +from openslides.projector.projector import Widget +from openslides.config.api import config +from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelatedAgendaItemView from .csv_import import import_motions +from .models import (Motion, MotionSubmitter, MotionSupporter, MotionPoll, + MotionVersion, State, WorkflowError, Category) from .forms import (BaseMotionForm, MotionSubmitterMixin, MotionSupporterMixin, MotionDisableVersioningMixin, MotionCategoryMixin, MotionIdentifierMixin, MotionWorkflowMixin, MotionImportForm) -from .models import (Motion, MotionSubmitter, MotionSupporter, MotionPoll, - MotionVersion, State, WorkflowError, Category) from .pdf import motions_to_pdf, motion_to_pdf, motion_poll_to_pdf @@ -297,7 +296,7 @@ class MotionDeleteView(DeleteView): """ return self.get_object().get_allowed_actions(request.user)['delete'] - def get_success_message(self): + def get_final_message(self): return _('%s was successfully deleted.') % _('Motion') motion_delete = MotionDeleteView.as_view() @@ -326,21 +325,20 @@ class VersionDeleteView(DeleteView): raise Http404('You can not delete the active version of a motion.') return version - def get_success_url_name_args(self): + def get_url_name_args(self): return (self.object.motion_id, ) version_delete = VersionDeleteView.as_view() -class VersionPermitView(SingleObjectMixin, QuestionMixin, RedirectView): +class VersionPermitView(SingleObjectMixin, QuestionView): """ View to permit a version of a motion. """ model = Motion - question_url_name = 'motion_version_detail' - success_url_name = 'motion_version_detail' - success_message = ugettext_lazy('Version successfully permitted.') + final_message = ugettext_lazy('Version successfully permitted.') permission_required = 'motion.can_manage_motion' + question_url_name = 'motion_version_detail' def get(self, *args, **kwargs): """ @@ -360,13 +358,13 @@ class VersionPermitView(SingleObjectMixin, QuestionMixin, RedirectView): """ return [self.object.pk, self.version.version_number] - def get_question(self): + def get_question_message(self): """ Return a string, shown to the user as question to permit the version. """ return _('Are you sure you want permit version %s?') % self.version.version_number - def case_yes(self): + def on_clicked_yes(self): """ Activate the version, if the user chooses 'yes'. """ @@ -418,7 +416,7 @@ class VersionDiffView(DetailView): version_diff = VersionDiffView.as_view() -class SupportView(SingleObjectMixin, QuestionMixin, RedirectView): +class SupportView(SingleObjectMixin, QuestionView): """ View to support or unsupport a motion. @@ -452,7 +450,7 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView): else: return True - def get_question(self): + def get_question_message(self): """ Return the question string. """ @@ -461,7 +459,7 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView): else: return _('Do you really want to unsupport this motion?') - def case_yes(self): + def on_clicked_yes(self): """ Append or remove the request.user from the motion. @@ -477,7 +475,7 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView): self.object.unsupport(person=user) self.object.write_log([ugettext_noop('Motion unsupported')], user) - def get_success_message(self): + def get_final_message(self): """ Return the success message. """ @@ -486,12 +484,6 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView): else: return _("You have unsupported this motion successfully.") - def get_redirect_url(self, **kwargs): - """ - Return the url, the view should redirect to. - """ - return self.object.get_absolute_url() - motion_support = SupportView.as_view(support=True) motion_unsupport = SupportView.as_view(support=False) @@ -558,6 +550,7 @@ class PollUpdateView(PollMixin, PollFormView): """ View to update a MotionPoll. """ + poll_class = MotionPoll """ Poll Class to use for this view. @@ -595,11 +588,11 @@ class PollDeleteView(PollMixin, DeleteView): model = MotionPoll - def case_yes(self): + def on_clicked_yes(self): """ Write a log message, if the form is valid. """ - super(PollDeleteView, self).case_yes() + super(PollDeleteView, self).on_clicked_yes() self.object.motion.write_log([ugettext_noop('Poll deleted')], self.request.user) def get_redirect_url(self, **kwargs): diff --git a/openslides/participant/models.py b/openslides/participant/models.py index 31f7f11c2..9da40bbc4 100644 --- a/openslides/participant/models.py +++ b/openslides/participant/models.py @@ -138,11 +138,11 @@ class Group(SlideMixin, PersonMixin, Person, DjangoGroup): Return the URL to the user group. """ if link == 'detail' or link == 'view': - return reverse('user_group_view', args=[str(self.id)]) + return reverse('user_group_view', args=[str(self.pk)]) if link == 'update' or link == 'edit': - return reverse('user_group_edit', args=[str(self.id)]) + return reverse('user_group_edit', args=[str(self.pk)]) if link == 'delete': - return reverse('user_group_delete', args=[str(self.id)]) + return reverse('user_group_delete', args=[str(self.pk)]) return super(Group, self).get_absolute_url(link) diff --git a/openslides/participant/views.py b/openslides/participant/views.py index 2d0b04801..0396d9eea 100644 --- a/openslides/participant/views.py +++ b/openslides/participant/views.py @@ -40,7 +40,7 @@ from openslides.utils.utils import ( template, delete_default_permissions, html_strong) from openslides.utils.views import ( FormView, PDFView, CreateView, UpdateView, DeleteView, PermissionMixin, - RedirectView, SingleObjectMixin, ListView, QuestionMixin, DetailView) + RedirectView, SingleObjectMixin, ListView, QuestionView, DetailView) from openslides.config.api import config from openslides.projector.projector import Widget @@ -155,6 +155,7 @@ class UserDeleteView(DeleteView): permission_required = 'participant.can_manage_participant' model = User success_url_name = 'user_overview' + url_name_args = [] def pre_redirect(self, request, *args, **kwargs): if self.object == self.request.user: @@ -335,14 +336,14 @@ class UserImportView(FormView): return super(UserImportView, self).form_valid(form) -class ResetPasswordView(SingleObjectMixin, QuestionMixin, RedirectView): +class ResetPasswordView(SingleObjectMixin, QuestionView): """ Set the Passwort for a user to his default password. """ permission_required = 'participant.can_manage_participant' model = User allow_ajax = True - question = ugettext_lazy('Do you really want to reset the password?') + question_message = ugettext_lazy('Do you really want to reset the password?') def get(self, request, *args, **kwargs): self.object = self.get_object() @@ -351,10 +352,10 @@ class ResetPasswordView(SingleObjectMixin, QuestionMixin, RedirectView): def get_redirect_url(self, **kwargs): return reverse('user_edit', args=[self.object.id]) - def case_yes(self): + def on_clicked_yes(self): self.object.reset_password() - def get_success_message(self): + def get_final_message(self): return _('The Password for %s was successfully reset.') % html_strong(self.object) @@ -432,6 +433,7 @@ class GroupDeleteView(DeleteView): permission_required = 'participant.can_manage_participant' model = Group success_url_name = 'user_group_overview' + url_name_args = [] def pre_redirect(self, request, *args, **kwargs): if not self.is_protected_from_deleting(): @@ -466,14 +468,17 @@ def login(request): try: admin = User.objects.get(pk=1) if admin.check_password(admin.default_password): + user_data = { + 'user': html_strong(admin.username), + 'password': html_strong(admin.default_password)} + extra_content['first_time_message'] = _( "Installation was successfully! Use %(user)s " "(password: %(password)s) for first login.
" "Important: Please change the password after " "first login! Otherwise this message still appears for " - "everyone and could be a security risk.") % { - 'user': html_strong(admin.username), - 'password': html_strong(admin.default_password)} + "everyone and could be a security risk.") % user_data + extra_content['next'] = reverse('password_change') except User.DoesNotExist: pass diff --git a/openslides/poll/models.py b/openslides/poll/models.py index 19821b3d1..986247d97 100644 --- a/openslides/poll/models.py +++ b/openslides/poll/models.py @@ -14,7 +14,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils.translation import ugettext_lazy as _, ugettext_noop -from openslides.utils.modelfields import MinMaxIntegerField +from openslides.utils.models import MinMaxIntegerField class BaseOption(models.Model): diff --git a/openslides/poll/views.py b/openslides/poll/views.py index 2293e8c54..1571c10a2 100644 --- a/openslides/poll/views.py +++ b/openslides/poll/views.py @@ -14,10 +14,10 @@ from django.http import HttpResponseRedirect from django.forms.models import modelform_factory from django.core.exceptions import ImproperlyConfigured -from openslides.utils.views import TemplateView, UrlMixin +from openslides.utils.views import TemplateView, FormMixin -class PollFormView(UrlMixin, TemplateView): +class PollFormView(FormMixin, TemplateView): poll_class = None def get(self, request, *args, **kwargs): diff --git a/openslides/projector/views.py b/openslides/projector/views.py index 1d8b90d36..fd726ca65 100644 --- a/openslides/projector/views.py +++ b/openslides/projector/views.py @@ -23,7 +23,7 @@ from django.shortcuts import redirect from django.template import RequestContext from django.utils.translation import ugettext as _ -from openslides.utils.template import render_block_to_string, Tab +from openslides.utils.template import Tab from openslides.utils.views import ( TemplateView, RedirectView, CreateView, UpdateView, DeleteView, AjaxMixin) from openslides.config.api import config diff --git a/openslides/templates/403.html b/openslides/templates/403.html index c21b8cb3c..6fc6432ef 100644 --- a/openslides/templates/403.html +++ b/openslides/templates/403.html @@ -1,5 +1,8 @@ {% extends "base.html" %} +{% load i18n %} + {% block content %} -

{{ error }}

+

{% trans 'Permission Denied' %}

+ {% trans 'Sorry, you have no rights to see this page.' %} {% endblock %} diff --git a/openslides/utils/modelfields.py b/openslides/utils/models.py similarity index 83% rename from openslides/utils/modelfields.py rename to openslides/utils/models.py index e3ccd1ef4..85aa5a23b 100644 --- a/openslides/utils/modelfields.py +++ b/openslides/utils/models.py @@ -6,7 +6,7 @@ Modelfields for OpenSlides - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ @@ -14,6 +14,10 @@ from django.db import models class MinMaxIntegerField(models.IntegerField): + """ + IntegerField with options to set a min- and a max-value. + """ + def __init__(self, min_value=None, max_value=None, *args, **kwargs): self.min_value, self.max_value = min_value, max_value super(MinMaxIntegerField, self).__init__(*args, **kwargs) diff --git a/openslides/utils/pdf.py b/openslides/utils/pdf.py old mode 100755 new mode 100644 diff --git a/openslides/utils/staticfiles.py b/openslides/utils/staticfiles.py deleted file mode 100755 index 05a075ef9..000000000 --- a/openslides/utils/staticfiles.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - openslides.utils.staticfiles - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - StaticFiels fix for the django bug #18404. - - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. - :license: GNU GPL, see LICENSE for more details. -""" - -import os -import sys - -from django.core.files.storage import FileSystemStorage -from django.utils.importlib import import_module -from django.contrib.staticfiles.finders import ( - AppDirectoriesFinder as _AppDirectoriesFinder) - - -# This is basically a copy of -# django.contrib.staticfiles.storage.AppStaticStorage -# with the fix for django bug #18404 applied -# see https://code.djangoproject.com/ticket/18404 for details -class AppStaticStorage(FileSystemStorage): - """ - A file system storage backend that takes an app module and works - for the ``static`` directory of it. - """ - prefix = None - source_dir = 'static' - - def __init__(self, app, *args, **kwargs): - """ - Returns a static file storage if available in the given app. - """ - # app is the actual app module - mod = import_module(app) - mod_path = os.path.dirname(mod.__file__) - location = os.path.join(mod_path, self.source_dir) - fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() - location = location.decode(fs_encoding) - super(AppStaticStorage, self).__init__(location, *args, **kwargs) - - -class AppDirectoriesFinder(_AppDirectoriesFinder): - storage_class = AppStaticStorage diff --git a/openslides/utils/template.py b/openslides/utils/template.py index 27f77df0a..2b237c0b4 100644 --- a/openslides/utils/template.py +++ b/openslides/utils/template.py @@ -10,11 +10,11 @@ :license: GNU GPL, see LICENSE for more details. """ -from django.template import loader, Context -from django.template.loader_tags import BlockNode, ExtendsNode - class Tab(object): + """ + Entry for the main menu of OpenSlides. + """ def __init__(self, title='', app='', stylefile='', url='', permission=True, selected=False): self.title = title self.app = app @@ -22,62 +22,3 @@ class Tab(object): self.url = url self.permission = permission self.selected = selected - - -## All following function are only needed to render a block from a template -## and could be removed, if the template worked with an include-statement instead. -## Its only used for ajax-request from the projector. - -def get_template(template): - if isinstance(template, (tuple, list)): - return loader.select_template(template) - return loader.get_template(template) - - -class BlockNotFound(Exception): - pass - - -def render_template_block(template, block, context): - """ - Renders a single block from a template. This template should have previously - been rendered. - """ - return render_template_block_nodelist(template.nodelist, block, context) - - -def render_template_block_nodelist(nodelist, block, context): - for node in nodelist: - if isinstance(node, BlockNode) and node.name == block: - return node.render(context) - for key in ('nodelist', 'nodelist_true', 'nodelist_false'): - if hasattr(node, key): - try: - return render_template_block_nodelist( - getattr(node, key), block, context) - except: - pass - for node in nodelist: - if isinstance(node, ExtendsNode): - try: - return render_template_block( - node.get_parent(context), block, context) - except BlockNotFound: - pass - raise BlockNotFound - - -def render_block_to_string(template_name, block, dictionary=None, - context_instance=None): - """ - Loads the given template_name and renders the given block with the given - dictionary as context. Returns a string. - """ - dictionary = dictionary or {} - t = get_template(template_name) - if context_instance: - context_instance.update(dictionary) - else: - context_instance = Context(dictionary) - t.render(context_instance) - return render_template_block(t, block, context_instance) diff --git a/openslides/utils/utils.py b/openslides/utils/utils.py index fb4fb8030..c207a4985 100644 --- a/openslides/utils/utils.py +++ b/openslides/utils/utils.py @@ -11,54 +11,12 @@ """ import difflib -import json -import sys -from django.contrib import messages from django.contrib.auth.models import Permission -from django.core.context_processors import csrf -from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseForbidden -from django.shortcuts import render_to_response, redirect +from django.shortcuts import render_to_response from django.template import RequestContext -from django.template.loader import render_to_string -from django.utils.translation import ugettext as _, ugettext_lazy -from openslides.utils.signals import template_manipulation - - -def gen_confirm_form(request, message, url): - """ - Generate a message-form. - - Deprecated. Use Class base Views instead. - """ - messages.warning( - request, - """ - %s -
- - - -
- """ - % (message, url, csrf(request)['csrf_token'], _("Yes"), _("No"))) - - -def del_confirm_form(request, object, name=None, delete_link=None): - """ - Creates a question to delete an object. - - Deprecated. Use Class base Views instead. - """ - if name is None: - name = object - if delete_link is None: - delete_link = object.get_absolute_url('delete') - gen_confirm_form( - request, _('Do you really want to delete %s?') - % html_strong(name), delete_link) +from .signals import template_manipulation def template(template_name): @@ -67,6 +25,8 @@ def template(template_name): Deprecated. Use class based views instead. """ + # TODO: Write the login page an the usersettings page with class based views + # Remove this function afterwards def renderer(func): def wrapper(request, *args, **kwargs): output = func(request, *args, **kwargs) @@ -85,36 +45,12 @@ def template(template_name): return renderer -def permission_required(perm, login_url=None): - """ - Decorator for views that checks whether a user has a particular permission - enabled, redirecting to the log-in page if necessary. - - Deprecated. - """ - def renderer(func): - def wrapper(request, *args, **kw): - if request.user.has_perm(perm): - return func(request, *args, **kw) - if request.user.is_authenticated(): - return render_to_forbidden(request) - return redirect(reverse('user_login')) - return wrapper - return renderer - - -def render_to_forbidden(request, - error=ugettext_lazy("Sorry, you have no rights to see this page.")): - # TODO: Integrate this function into the PermissionMixin once the - # above function is deleted. - return HttpResponseForbidden(render_to_string( - '403.html', {'error': error}, context_instance=RequestContext(request))) - - def delete_default_permissions(**kwargs): """ Deletes the permissions, django creates by default for the admin. """ + # TODO: Create an participant app which does not create the permissions. + # Delete this function afterwards for p in Permission.objects.all(): if (p.codename.startswith('add') or p.codename.startswith('delete') or @@ -122,29 +58,26 @@ def delete_default_permissions(**kwargs): p.delete() -def ajax_request(data): - """ - generates a HTTPResponse-Object with json-Data for a - ajax response. - - Deprecated. - """ - return HttpResponse(json.dumps(data)) - - def html_strong(string): + """ + Returns the text wrapped in an HTML-Strong element. + """ return u"%s" % string def htmldiff(text1, text2): - """Return string of html diff between two strings (text1 and text2)""" - + """ + Return string of html diff between two strings (text1 and text2) + """ diff = difflib.HtmlDiff(wrapcolumn=60) return diff.make_table(text1.splitlines(), text2.splitlines()) -def int_or_none(object): +def int_or_none(var): + """ + Trys to convert 'var' into an integer. Returns None if an TypeError occures. + """ try: - return int(object) - except TypeError: + return int(var) + except (TypeError, ValueError): return None diff --git a/openslides/utils/views.py b/openslides/utils/views.py index 6265b5294..9ab1b4be7 100644 --- a/openslides/utils/views.py +++ b/openslides/utils/views.py @@ -6,7 +6,7 @@ Views for OpenSlides. - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ @@ -15,257 +15,280 @@ from cStringIO import StringIO from reportlab.platypus import SimpleDocTemplate, Spacer from reportlab.lib.units import cm +from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.context_processors import csrf -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.urlresolvers import reverse -from django.conf import settings from django.dispatch import receiver -from django.http import HttpResponseServerError, HttpResponse, HttpResponseRedirect -from django.utils.decorators import method_decorator -from django.utils.translation import ugettext as _, ugettext_lazy -from django.utils.importlib import import_module +from django.http import (HttpResponse, HttpResponseRedirect, + HttpResponseServerError) from django.template import RequestContext from django.template.loader import render_to_string -from django.views.generic import ( - TemplateView as _TemplateView, - RedirectView as _RedirectView, - UpdateView as _UpdateView, - CreateView as _CreateView, - View as _View, - FormView as _FormView, - ListView as _ListView, - DetailView as _DetailView, -) +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 from django.views.generic.detail import SingleObjectMixin from django.views.generic.list import TemplateResponseMixin +from reportlab.lib.units import cm +from reportlab.platypus import SimpleDocTemplate, Spacer -from openslides.utils.utils import render_to_forbidden, html_strong -from openslides.utils.signals import template_manipulation -from openslides.utils.pdf import firstPage, laterPages - +from .pdf import firstPage, laterPages +from .signals import template_manipulation +from .utils import html_strong NO_PERMISSION_REQUIRED = 'No permission required' -View = _View - - -class SetCookieMixin(object): - def render_to_response(self, context, **response_kwargs): - response = TemplateResponseMixin.render_to_response( - self, context, **response_kwargs) - if 'cookie' in context: - response.set_cookie(context['cookie'][0], context['cookie'][1]) - return response +View = django_views.View class LoginMixin(object): + """ + Mixin for Views, that only can be viseted from users how are logedin. + """ + @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): + """ + Check if the user is loged in. + """ return super(LoginMixin, self).dispatch(request, *args, **kwargs) class PermissionMixin(object): + """ + Mixin for views, that only can be visited from users with special rights. + + Set the attribute 'permission_required' to the required permission string. + """ permission_required = NO_PERMISSION_REQUIRED def has_permission(self, request, *args, **kwargs): + """ + Checks if the user has the required permission. + """ if self.permission_required == NO_PERMISSION_REQUIRED: return True else: return request.user.has_perm(self.permission_required) def dispatch(self, request, *args, **kwargs): + """ + Check if the user has the permission. + + If the user is not logged in, redirect the user to the login page. + """ if not self.has_permission(request, *args, **kwargs): if not request.user.is_authenticated(): path = request.get_full_path() return HttpResponseRedirect( "%s?next=%s" % (settings.LOGIN_URL, path)) else: - return render_to_forbidden(request) + raise PermissionDenied return super(PermissionMixin, self).dispatch(request, *args, **kwargs) class AjaxMixin(object): + """ + Mixin to response to an ajax request with an json object. + """ + def get_ajax_context(self, **kwargs): - return {} + """ + Returns a dictonary with the context for the ajax response. + """ + return kwargs def ajax_get(self, request, *args, **kwargs): - return HttpResponse(json.dumps(self.get_ajax_context(**kwargs))) + """ + Returns the HttpResponse. + """ + return HttpResponse(json.dumps(self.get_ajax_context())) class ExtraContextMixin(object): + """ + 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. + """ + def get_context_data(self, **kwargs): + """ + Sends the signal. + """ context = super(ExtraContextMixin, self).get_context_data(**kwargs) - context.setdefault('extra_javascript', []) template_manipulation.send( sender=self.__class__, request=self.request, context=context) return context class UrlMixin(object): - """ - Mixin to provide the use of url names in views with success url and - apply url. - - The initial value of url_name_args is None, but the default given by - the used get_url_name_args method is [], because in the future, the - method might return another default value. Compare this with the QuestionMixin. - """ - apply_url_name = None - success_url_name = None url_name_args = None + + def get_url(self, url_name=None, url=None, args=None, use_absolute_url_link=None): + """ + Returns an url. + + 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(). + + 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 + else: + try: + if use_absolute_url_link is None: + value = self.object.get_absolute_url() + else: + value = self.object.get_absolute_url(use_absolute_url_link) + except AttributeError: + raise ImproperlyConfigured( + 'No url to redirect to. See openslides.utils.views.UrlMixin for more details.') + return value + + def get_url_name_args(self): + """ + 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: + value = self.url_name_args + else: + try: + pk = self.object.pk + except AttributeError: + value = [] + else: + if pk: + value = [pk] + else: + value = [] + return value + + +class FormMixin(UrlMixin): + """ + Mixin for views with forms. + """ + + 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.') def get_apply_url(self): - if self.apply_url_name: - return reverse(self.apply_url_name, args=self.get_url_name_args()) - elif self.apply_url: - return self.apply_url - else: - try: - return self.object.get_absolute_url('edit') - except AttributeError: - raise ImproperlyConfigured( - "No URL to redirect to. Provide an apply_url_name.") + """ + Returns the url when the user clicks on 'apply'. + """ + return self.get_url(self.apply_url_name, self.apply_url, + args=self.get_url_name_args(), + use_absolute_url_link='update') def get_success_url(self): - if 'apply' in self.request.POST: - return self.get_apply_url() + """ + Returns the url when the user submits a form. - if self.success_url_name: - return reverse(self.success_url_name, args=self.get_url_name_args()) - elif self.success_url: - return self.success_url + Redirects to get_apply_url if self.use_apply is True + """ + if self.use_apply and 'apply' in self.request.POST: + value = self.get_apply_url() else: - try: - return self.object.get_absolute_url() - except AttributeError: - raise ImproperlyConfigured( - "No URL to redirect to. Either provide an URL or define " - "a get_absolute_url method of the model.") + value = self.get_url(self.success_url_name, self.success_url, + args=self.get_url_name_args()) + return value - def get_url_name_args(self): - """ - Returns the arguments for the url name. Default is an empty list. - """ - if self.url_name_args is not None: - return self.url_name_args - return [] + def form_valid(self, form): + value = super(FormMixin, self).form_valid(form) + messages.success(self.request, self.get_success_message()) + return value - -class QuestionMixin(object): - """ - Mixin for questions to the requesting user. - - 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. - """ - question = ugettext_lazy('Are you sure?') - success_message = ugettext_lazy('Thank you for your answer') - answer_options = [('yes', ugettext_lazy("Yes")), ('no', ugettext_lazy("No"))] - question_url_name = None - success_url_name = None - url_name_args = None - - def get_redirect_url(self, **kwargs): - # TODO: raise error when question_url_name/success_url_name is not present - if self.request.method == 'GET': - return reverse(self.question_url_name, args=self.get_question_url_name_args()) - else: - return reverse(self.success_url_name, args=self.get_success_url_name_args()) - - def get_question_url_name_args(self): - return self.get_url_name_args() - - def get_success_url_name_args(self): - return self.get_url_name_args() - - def get_url_name_args(self): - """ - 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: - return self.url_name_args - try: - return [self.object.pk] - except AttributeError: - return [] - - def pre_redirect(self, request, *args, **kwargs): - """ - Prints the question in a GET request. - """ - self.confirm_form() - - def get_question(self): - return unicode(self.question) - - def get_answer_options(self): - return self.answer_options - - def get_answer_url(self): - try: - return self.answer_url - except AttributeError: - return self.request.path - - def confirm_form(self): - option_fields = "\n".join([ - '' % (option[0], unicode(option[1])) - for option in self.get_answer_options()]) - messages.warning( - self.request, - """ - %(message)s -
- - %(option_fields)s -
- """ % {'message': self.get_question(), - 'url': self.get_answer_url(), - 'csrf': csrf(self.request)['csrf_token'], - 'option_fields': option_fields}) - - def pre_post_redirect(self, request, *args, **kwargs): - # Reacts on the response of the user in a POST-request. - # TODO: call the methodes for all possible answers. - if self.get_answer() == 'yes': - self.case_yes() - messages.success(request, self.get_success_message()) - - def get_answer(self): - for option in self.get_answer_options(): - if option[0] in self.request.POST: - return option[0] - return None - - def case_yes(self): - # TODO: raise a warning - pass + def form_invalid(self, form): + value = super(FormMixin, self).form_invalid(form) + messages.error(self.request, self.get_error_message()) + return value def get_success_message(self): return self.success_message + def get_error_message(self): + return self.error_message -class TemplateView(PermissionMixin, ExtraContextMixin, _TemplateView): + +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()) + + def manipulate_object(self, form): + """ + Called before the object is saved into the database. + """ + pass + + def post_save(self, form): + """ + Called after the object is saved into the database. + """ + form.save_m2m() + + +class TemplateView(PermissionMixin, ExtraContextMixin, django_views.TemplateView): + """ + View to return with an template. + """ pass -class ListView(PermissionMixin, SetCookieMixin, ExtraContextMixin, _ListView): +class ListView(PermissionMixin, ExtraContextMixin, django_views.ListView): + """ + View to show a list of model objects. + """ pass class AjaxView(PermissionMixin, AjaxMixin, View): + """ + View for ajax requests. + """ def get(self, request, *args, **kwargs): return self.ajax_get(request, *args, **kwargs) -class RedirectView(PermissionMixin, AjaxMixin, _RedirectView): +class RedirectView(PermissionMixin, AjaxMixin, UrlMixin, django_views.RedirectView): """ View to redirect to another url. @@ -277,12 +300,19 @@ class RedirectView(PermissionMixin, AjaxMixin, _RedirectView): permanent = False allow_ajax = False url_name = None - url_name_args = None def pre_redirect(self, request, *args, **kwargs): + """ + Called before the redirect. + """ + # TODO: Also call this method on post-request. + # Add pre_get_redirect for get requests. pass def pre_post_redirect(self, request, *args, **kwargs): + """ + Called before the redirect, if it is a post request. + """ pass def get(self, request, *args, **kwargs): @@ -291,62 +321,143 @@ class RedirectView(PermissionMixin, AjaxMixin, _RedirectView): elif request.method == 'POST': self.pre_post_redirect(request, *args, **kwargs) - if self.request.is_ajax() and self.allow_ajax: + if request.is_ajax() and self.allow_ajax: return self.ajax_get(request, *args, **kwargs) return super(RedirectView, self).get(request, *args, **kwargs) def get_redirect_url(self, **kwargs): - if self.url_name is not None: - return reverse(self.url_name, args=self.get_url_name_args()) + """ + 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()) else: - return super(RedirectView, self).get_redirect_url(**kwargs) + url = super(QuestionView, self).get_redirect_url() + return url - def get_url_name_args(self): + def pre_redirect(self, request, *args, **kwargs): """ - Returns the arguments for the url name. Default is an empty list - or [self.object.pk] if this exist. + Prints the question in a GET request. """ - if self.url_name_args is not None: - return self.url_name_args - try: - return [self.object.pk] - except AttributeError: - return [] + self.confirm_form() + + def get_question_message(self): + """ + Returns the question. + """ + return self.question_message + + def get_answer_options(self): + """ + Returns the possible answers. + + 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 + + def confirm_form(self): + """ + Returns the form to show in the message. + """ + option_fields = "\n".join([ + '' + % (option[0], unicode(option[1])) + for option in self.get_answer_options()]) + messages.warning( + self.request, + """ + %(message)s +
+ + %(option_fields)s +
+ """ % {'message': self.get_question_message(), + 'url': self.request.path, + 'csrf': csrf(self.request)['csrf_token'], + 'option_fields': option_fields}) + + def pre_post_redirect(self, request, *args, **kwargs): + """ + Calls the method for the answer the user clicked. + + 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. + + 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() + + def get_answer(self): + """ + Returns the key of the clicked answer. + + 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) + + 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, UrlMixin, _FormView): - def form_invalid(self, form): - messages.error(self.request, _('Please check the form for errors.')) - return super(FormView, self).form_invalid(form) +class FormView(PermissionMixin, ExtraContextMixin, FormMixin, + django_views.FormView): + """ + View for forms. + """ + pass -class ModelFormMixin(object): - def form_valid(self, form): - self.object = form.save(commit=False) - self.manipulate_object(form) - self.object.save() - self.post_save(form) - return HttpResponseRedirect(self.get_success_url()) - - def manipulate_object(self, form): - pass - - def post_save(self, form): - form.save_m2m() - - -class UpdateView(PermissionMixin, UrlMixin, ExtraContextMixin, - ModelFormMixin, _UpdateView): - success_message = None - - def form_invalid(self, form): - messages.error(self.request, _('Please check the form for errors.')) - return super(UpdateView, self).form_invalid(form) - - def form_valid(self, form): - value = super(UpdateView, self).form_valid(form) - messages.success(self.request, self.get_success_message()) - return value +class UpdateView(PermissionMixin, ExtraContextMixin, + ModelFormMixin, django_views.UpdateView): + """ + View to update an model object. + """ def get_success_message(self): if self.success_message is None: @@ -356,18 +467,11 @@ class UpdateView(PermissionMixin, UrlMixin, ExtraContextMixin, return message -class CreateView(PermissionMixin, UrlMixin, ExtraContextMixin, - ModelFormMixin, _CreateView): - success_message = None - - def form_invalid(self, form): - messages.error(self.request, _('Please check the form for errors.')) - return super(CreateView, self).form_invalid(form) - - def form_valid(self, form): - value = super(CreateView, self).form_valid(form) - messages.success(self.request, self.get_success_message()) - return value +class CreateView(PermissionMixin, ExtraContextMixin, + ModelFormMixin, django_views.CreateView): + """ + View to create a model object. + """ def get_success_message(self): if self.success_message is None: @@ -377,8 +481,12 @@ class CreateView(PermissionMixin, UrlMixin, ExtraContextMixin, return message -class DeleteView(SingleObjectMixin, QuestionMixin, RedirectView): - question_url_name = None +class DeleteView(SingleObjectMixin, QuestionView): + """ + View to delete an model object. + """ + + success_url = None success_url_name = None def get(self, request, *args, **kwargs): @@ -386,32 +494,53 @@ class DeleteView(SingleObjectMixin, QuestionMixin, RedirectView): return super(DeleteView, self).get(request, *args, **kwargs) def get_redirect_url(self, **kwargs): - if self.question_url_name is None and (self.request.method == 'GET' or - self.get_answer() == 'no'): - return self.object.get_absolute_url() - else: - return super(DeleteView, self).get_redirect_url(**kwargs) + """ + Returns the url on which the delete dialog is shown and the url after + the deleten. - def get_question(self): + 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()) + else: + url = self.get_url(self.success_url_name, self.success_url, + args=self.get_url_name_args()) + return url + + def get_question_message(self): + """ + Returns the question for the delete dialog. + """ return _('Do you really want to delete %s?') % html_strong(self.object) - def case_yes(self): + def on_clicked_yes(self): + """ + Deletes the object. + """ self.object.delete() - def get_success_message(self): + def get_final_message(self): + """ + Prints the success message to the user. + """ return _('%s was successfully deleted.') % html_strong(self.object) - def get_url_name_args(self): - return [] - -class DetailView(PermissionMixin, ExtraContextMixin, _DetailView): - def get(self, request, *args, **kwargs): - self.object = self.get_object() - return super(DetailView, self).get(request, *args, **kwargs) +class DetailView(PermissionMixin, ExtraContextMixin, django_views.DetailView): + """ + View to show an model object. + """ + pass class PDFView(PermissionMixin, View): + """ + View to generate an PDF. + """ + filename = _('undefined-filename') top_space = 3 document_title = None @@ -480,6 +609,8 @@ def send_register_tab(sender, request, context, **kwargs): extra_stylefiles = context['extra_stylefiles'] else: extra_stylefiles = [] + context.setdefault('extra_javascript', []) + # TODO: Do not go over the filesystem by any request for app in settings.INSTALLED_APPS: try: diff --git a/tests/motion/test_views.py b/tests/motion/test_views.py index 7726aef1c..859e6c6dd 100644 --- a/tests/motion/test_views.py +++ b/tests/motion/test_views.py @@ -370,14 +370,14 @@ class TestMotionDeleteView(MotionViewTestCase): self.assertRedirects(response, '/motion/2/') def test_admin(self): - response = self.admin_client.post('/motion/2/del/', {}) + response = self.admin_client.post('/motion/2/del/', {'yes': 'yes'}) self.assertRedirects(response, '/motion/') def test_delegate(self): - response = self.delegate_client.post('/motion/2/del/', {}) + response = self.delegate_client.post('/motion/2/del/', {'yes': 'yes'}) self.assertEqual(response.status_code, 403) motion = Motion.objects.get(pk=2).add_submitter(self.delegate) - response = self.delegate_client.post('/motion/2/del/', {}) + response = self.delegate_client.post('/motion/2/del/', {'yes': 'yes'}) self.assertEqual(response.status_code, 403) @@ -394,7 +394,7 @@ class TestVersionPermitView(MotionViewTestCase): def test_post(self): new_version = self.motion1.get_last_version() response = self.admin_client.post('/motion/1/version/2/permit/', {'yes': 1}) - self.assertRedirects(response, '/motion/1/version/2/') + self.assertRedirects(response, '/motion/1/') self.assertEqual(self.motion1.get_active_version(), new_version) def test_activate_old_version(self): diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py new file mode 100644 index 000000000..090a5ae00 --- /dev/null +++ b/tests/utils/test_utils.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + Tests for openslides utils.utils + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + TODO: Move this test to the correct place when the projector app is cleaned up. + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from openslides.utils.test import TestCase +from openslides.utils.utils import html_strong, int_or_none + + +class Test_functions(TestCase): + def test_string(self): + self.assertEqual(html_strong('some text'), 'some text') + + def test_int_or_none(self): + self.assertEqual(int_or_none('5'), 5) + self.assertEqual(int_or_none(5), 5) + self.assertIsNone(int_or_none('text')) + self.assertIsNone(int_or_none(None)) diff --git a/tests/utils/test_views.py b/tests/utils/test_views.py new file mode 100644 index 000000000..111477741 --- /dev/null +++ b/tests/utils/test_views.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + Tests for openslides utils.utils + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + TODO: Move this test to the correct place when the projector app is cleaned up. + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from django.contrib.messages.storage import default_storage +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import clear_url_caches +from django.dispatch import receiver +from django.http.response import HttpResponseNotAllowed +from django.test import RequestFactory +from django.test.client import Client +from django.test.utils import override_settings + +from openslides.participant.models import User +from openslides.utils import views +from openslides.utils.signals import template_manipulation +from openslides.utils.test import TestCase + +from . import views as test_views + + +@override_settings(ROOT_URLCONF='tests.utils.urls') +class ViewTestCase(TestCase): + rf = RequestFactory() + + def setUp(self): + # Clear the cache for the urlresolver, so the overriden settings works. + clear_url_caches() + + +class LoginMixinTest(ViewTestCase): + def test_dispatch(self): + view = test_views.LoginMixinView.as_view() + client = Client() + response = client.get('/login_mixin/') + self.assertEqual(response['Location'], 'http://testserver/login/?next=/login_mixin/') + + client.login(username='admin', password='admin') + response = client.get('/login_mixin/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'Well done.') + + +class PermissionMixinTest(ViewTestCase): + def test_dispatch(self): + client = Client() + + # View without permission_required + response = client.get('/permission_mixin1/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'Well done.') + + # View with permission_required without login + response = client.get('/permission_mixin2/') + self.assertEqual(response.status_code, 302) + self.assertEqual(response['Location'], 'http://testserver/login/?next=/permission_mixin2/') + + # View with permission_required, with login, without permission + client.login(username='admin', password='admin') + response = client.get('/permission_mixin2/') + self.assertEqual(response.status_code, 403) + + # View with permission_required, with login, with permission + response = client.get('/permission_mixin3/') + self.assertEqual(response.status_code, 200) + + +class AjaxMixinTest(ViewTestCase): + def test_ajax_get(self): + view = test_views.AjaxMixinView() + ajax_get = view.ajax_get + response = ajax_get(self.rf.get('/', {})) + self.assertEqual(response.content, '{"new_context": "newer_context"}') + + def test_get_ajax_context(self): + get_ajax_context = test_views.AjaxMixinView().get_ajax_context + self.assertEqual(get_ajax_context()['new_context'], 'newer_context') + self.assertEqual( + get_ajax_context(test_content='some_content')['test_content'], + 'some_content') + + +class ExtraContextMixinTest(ViewTestCase): + """ + Tests the ExtraContextMixin by testen the TemplateView + """ + def test_get_context_data(self): + view = views.TemplateView() + get_context_data = view.get_context_data + view.request = self.rf.get('/', {}) + + context = get_context_data() + self.assertIn('tabs', context) + + context = get_context_data(some_context='context') + self.assertIn('tabs', context) + self.assertIn('some_context', context) + + template_manipulation.connect(set_context, dispatch_uid='set_context_test') + context = get_context_data() + self.assertIn('tabs', context) + self.assertIn('new_context', context) + template_manipulation.disconnect(set_context, dispatch_uid='set_context_test') + + +class UrlMixinTest(ViewTestCase): + def test_get_url(self): + get_url = test_views.UrlMixinView().get_url + + # url_name has the higher priority + self.assertEqual(get_url('test_url_mixin', 'view_url'), + '/url_mixin/') + + # If the url_name is none, return the second argument + self.assertEqual(get_url(None, 'view_url'), + 'view_url') + + # Test argument in url + self.assertEqual(get_url('test_url_mixin_args', None, + args=[1]), + '/url_mixin_args/1/') + + # No Url given. + self.assertRaisesMessage( + ImproperlyConfigured, + 'No url to redirect to. See openslides.utils.views.UrlMixin for more details.', + get_url, None, None) + + def test_get_url_with_object(self): + get_url = test_views.UrlMixinViewWithObject().get_url + + self.assertEqual(get_url(None, None), + 'default_url') + self.assertEqual(get_url(None, None, use_absolute_url_link='detail'), + 'detail_url') + + def test_get_url_name_args(self): + view = test_views.UrlMixinViewWithObject() + get_url_name_args = view.get_url_name_args + view.url_name_args = [1] + + self.assertEqual(get_url_name_args(), [1]) + + view.url_name_args = None + self.assertEqual(get_url_name_args(), []) + + view.object.pk = 5 + self.assertEqual(get_url_name_args(), [5]) + + +## class QuestionMixinTest(ViewTestCase): + ## def test_get_redirect_url(self): + ## view = views.QuestionMixin() + ## get_redirect_url = view.get_redirect_url +## + ## view.request = self.rf.get('/') + ## view.question_url = 'redirect_to_get_url' + ## self.assertEqual(get_redirect_url(), 'redirect_to_get_url') +## + ## view.request = self.rf.post('/') + ## view.success_url = 'redirect_to_post_url' + ## self.assertEqual(get_redirect_url(), 'redirect_to_post_url') +## + ## def test_get_question(self): + ## view = views.QuestionMixin() + ## get_redirect_url = view.get_question +## + ## self.assertEqual(get_redirect_url(), 'Are you sure?') +## + ## view.question = 'new_question' + ## self.assertEqual(get_redirect_url(), 'new_question') +## + ## def test_get_answer_options(self): + ## view = views.QuestionMixin() + ## get_answer_options = view.get_answer_options +## + ## self.assertIn('yes', dict(get_answer_options())) + ## self.assertIn('no', dict(get_answer_options())) +## + ## view.answer_options = [('new_answer', 'Answer')] + ## self.assertNotIn('yes', dict(get_answer_options())) + ## self.assertIn('new_answer', dict(get_answer_options())) +## + ## def test_confirm_form(self): + ## view = views.QuestionMixin() + ## confirm_form = view.confirm_form + ## view.request = self.rf.get('/') + ## view.request._messages = default_storage(view.request) +## + ## confirm_form() + ## message = "".join(view.request._messages._queued_messages[0].message.split()) + ## self.assertEqual( + ## message, 'Areyousure?' + ## '' 'Yes' + ## 'No') + + +def set_context(sender, request, context, **kwargs): + """ + receiver for testing the ExtraContextMixin + """ + context.update({'new_context': 'some new context'}) diff --git a/tests/utils/urls.py b/tests/utils/urls.py new file mode 100644 index 000000000..484b04388 --- /dev/null +++ b/tests/utils/urls.py @@ -0,0 +1,29 @@ +from django.conf.urls import patterns, url + +from openslides.urls import urlpatterns + +from . import views + + +urlpatterns += patterns( + '', + url(r'^url_mixin/$', + views.UrlMixinView.as_view(), + name='test_url_mixin'), + + url(r'^url_mixin_args/(?P\d+)/$', + views.UrlMixinView.as_view(), + name='test_url_mixin_args'), + + url(r'^login_mixin/$', + views.LoginMixinView.as_view()), + + url(r'^permission_mixin1/$', + views.PermissionMixinView.as_view()), + + url(r'^permission_mixin2/$', + views.PermissionMixinView.as_view(permission_required='permission_string')), + + url(r'^permission_mixin3/$', + views.PermissionMixinView.as_view(permission_required='agenda.can_see_agenda')), +) diff --git a/tests/utils/views.py b/tests/utils/views.py new file mode 100644 index 000000000..8af2be1ac --- /dev/null +++ b/tests/utils/views.py @@ -0,0 +1,47 @@ +from django.http import HttpResponse + +from openslides.utils import views + + +class GetAbsoluteUrl(object): + """ + Generates objects with a get_absolute_url method. + + Helper Class for the tests. + """ + def get_absolute_url(self, link='default'): + if link == 'detail': + url = 'detail_url' + elif link == 'update': + url = 'update_url' + elif link == 'default': + url = 'default_url' + return url + + +class LoginMixinView(views.LoginMixin, views.View): + def get(self, *args, **kwargs): + response = HttpResponse('Well done.') + response.status_code = 200 + return response + + +class PermissionMixinView(views.PermissionMixin, views.View): + def get(self, *args, **kwargs): + response = HttpResponse('Well done.') + response.status_code = 200 + return response + + +class AjaxMixinView(views.AjaxMixin, views.View): + def get_ajax_context(self, **kwargs): + return super(AjaxMixinView, self).get_ajax_context( + new_context='newer_context', **kwargs) + + +class UrlMixinView(views.UrlMixin, views.View): + pass + + +class UrlMixinViewWithObject(views.UrlMixin, views.View): + object = GetAbsoluteUrl()