Merge pull request #1162 from normanjaeckel/MoveDashboard

Move dashboard to core app.
This commit is contained in:
Norman Jäckel 2014-01-10 09:34:46 -08:00
commit 0a9a99cfb0
65 changed files with 674 additions and 464 deletions

View File

@ -9,7 +9,7 @@ Version 1.6 (unreleased)
[https://github.com/OpenSlides/OpenSlides/issues?milestone=14] [https://github.com/OpenSlides/OpenSlides/issues?milestone=14]
Participants: Participants:
- Disable widgets by default. - Disabled widgets by default.
Files: Files:
- Enabled update and delete view for uploader refering to his own files. - Enabled update and delete view for uploader refering to his own files.
Other: Other:
@ -18,6 +18,9 @@ Other:
- Renamed config api classes. - Renamed config api classes.
- Renamed some classes of the poll api. - Renamed some classes of the poll api.
- Inserted api for the personal info widget. - Inserted api for the personal info widget.
- Changed api for main menu entries.
- Enhanced http error pages.
- Moved dashboard and select widgets view from projector to core app.
Version 1.5.1 (unreleased) Version 1.5.1 (unreleased)

View File

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import personal_info, signals, slides, widgets # noqa from . import main_menu, personal_info, signals, slides, widgets # noqa

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.main_menu import MainMenuEntry
class AgendaMainMenuEntry(MainMenuEntry):
"""
Main menu entry for the agenda app.
"""
verbose_name = ugettext_lazy('Agenda')
permission_required = 'agenda.can_see_agenda'
default_weight = 20
pattern_name = 'item_overview'
icon_css_class = 'icon-calendar'

View File

@ -15,7 +15,6 @@ from openslides.config.api import config
from openslides.projector.api import get_active_slide, update_projector from openslides.projector.api import get_active_slide, update_projector
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.template import Tab
from openslides.utils.utils import html_strong from openslides.utils.utils import html_strong
from openslides.utils.views import (CreateView, DeleteView, FormView, PDFView, from openslides.utils.views import (CreateView, DeleteView, FormView, PDFView,
RedirectView, SingleObjectMixin, RedirectView, SingleObjectMixin,
@ -569,7 +568,7 @@ class CurrentListOfSpeakersView(RedirectView):
messages.error(request, _( messages.error(request, _(
'There is no list of speakers for the current slide. ' 'There is no list of speakers for the current slide. '
'Please choose the agenda item manually from the agenda.')) 'Please choose the agenda item manually from the agenda.'))
return reverse('dashboard') return reverse('core_dashboard')
if self.set_speaker: if self.set_speaker:
if item.speaker_list_closed: if item.speaker_list_closed:
@ -613,25 +612,11 @@ class CurrentListOfSpeakersView(RedirectView):
if item.type == Item.ORGANIZATIONAL_ITEM: if item.type == Item.ORGANIZATIONAL_ITEM:
if reverse_to_dashboard or not self.request.user.has_perm('agenda.can_see_orga_items'): if reverse_to_dashboard or not self.request.user.has_perm('agenda.can_see_orga_items'):
return reverse('dashboard') return reverse('core_dashboard')
else: else:
return reverse('item_view', args=[item.pk]) return reverse('item_view', args=[item.pk])
else: else:
if reverse_to_dashboard or not self.request.user.has_perm('agenda.can_see_agenda'): if reverse_to_dashboard or not self.request.user.has_perm('agenda.can_see_agenda'):
return reverse('dashboard') return reverse('core_dashboard')
else: else:
return reverse('item_view', args=[item.pk]) return reverse('item_view', args=[item.pk])
def register_tab(request):
"""
Registers the agenda tab.
"""
selected = request.path.startswith('/agenda/')
return Tab(
title=_('Agenda'),
app='agenda',
url=reverse('item_overview'),
permission=(request.user.has_perm('agenda.can_see_agenda') or
request.user.has_perm('agenda.can_manage_agenda')),
selected=selected)

View File

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import personal_info, signals, slides, widgets # noqa from . import main_menu, personal_info, signals, slides, template, widgets # noqa

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.main_menu import MainMenuEntry
class AssignmentMainMenuEntry(MainMenuEntry):
"""
Main menu entry for the assignment app.
"""
verbose_name = ugettext_lazy('Elections')
permission_required = 'assignment.can_see_assignment'
default_weight = 40
pattern_name = 'assignment_list'
icon_css_class = 'icon-charts'

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,3 +1,11 @@
.icon-charts {
background-image: url("../img/glyphicons_041_charts.png");
background-position: 0px center;
}
.leftmenu ul li.active a span.ico i.icon-charts {
background-image: url("../img/glyphicons_041_charts_white.png");
}
td.elected { td.elected {
background-color: #BED4DE !important; background-color: #BED4DE !important;
} }

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
from django.dispatch import receiver
from openslides.utils.signals import template_manipulation
@receiver(template_manipulation, dispatch_uid="add_assignment_stylesheets")
def add_assignment_stylesheets(sender, request, context, **kwargs):
"""
Adds the assignment.css to the context.
"""
context['extra_stylefiles'].append('styles/assignment.css')

View File

@ -19,7 +19,6 @@ from openslides.participant.models import Group, User
from openslides.poll.views import PollFormView from openslides.poll.views import PollFormView
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.person import get_person from openslides.utils.person import get_person
from openslides.utils.template import Tab
from openslides.utils.utils import html_strong from openslides.utils.utils import html_strong
from openslides.utils.views import (CreateView, DeleteView, DetailView, from openslides.utils.views import (CreateView, DeleteView, DetailView,
ListView, PDFView, PermissionMixin, ListView, PDFView, PermissionMixin,
@ -606,18 +605,3 @@ class AssignmentPollPDF(PDFView):
('GRID', (0, 0), (-1, -1), 0.25, colors.grey), ('GRID', (0, 0), (-1, -1), 0.25, colors.grey),
('VALIGN', (0, 0), (-1, -1), 'TOP')])) ('VALIGN', (0, 0), (-1, -1), 'TOP')]))
story.append(t) story.append(t)
def register_tab(request):
selected = request.path.startswith('/assignment/')
return Tab(
title=_('Elections'),
app='assignment',
url=reverse('assignment_list'),
permission=(
request.user.has_perm('assignment.can_see_assignment') or
request.user.has_perm('assignment.can_nominate_other') or
request.user.has_perm('assignment.can_nominate_self') or
request.user.has_perm('assignment.can_manage_assignment')),
selected=selected,
)

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import main_menu # noqa

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.main_menu import MainMenuEntry
from .signals import config_signal
class ConfigMainMenuEntry(MainMenuEntry):
"""
Main menu entry for the config app.
"""
verbose_name = ugettext_lazy('Configuration')
default_weight = 70
pattern_name = 'config_first_config_collection_view'
icon_css_class = 'icon-cog'
def check_permission(self):
"""
Checks against all permissions of all config collections.
"""
for receiver, config_collection in config_signal.send(sender=self):
if config_collection.is_shown():
if self.request.user.has_perm(config_collection.required_permission):
return_value = True
break
else:
return_value = False
return return_value

View File

@ -5,7 +5,6 @@ from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from openslides.utils.template import Tab
from openslides.utils.views import FormView from openslides.utils.views import FormView
from .api import config from .api import config
@ -109,15 +108,3 @@ class ConfigView(FormView):
config[key] = form.cleaned_data[key] config[key] = form.cleaned_data[key]
messages.success(self.request, _('%s settings successfully saved.') % _(self.config_collection.title)) messages.success(self.request, _('%s settings successfully saved.') % _(self.config_collection.title))
return super(ConfigView, self).form_valid(form) return super(ConfigView, self).form_valid(form)
def register_tab(request):
"""
Registers the entry for this app in the main menu.
"""
return Tab(
title=_('Configuration'),
app='config',
url=reverse('config_first_config_collection_view'),
permission=request.user.has_perm('config.can_manage'),
selected=request.path.startswith('/config/'))

View File

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import signals, widgets # noqa from . import main_menu, signals, widgets # noqa

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.main_menu import MainMenuEntry
class DashboardMainMenuEntry(MainMenuEntry):
"""
Main menu entry to the dashboard.
"""
verbose_name = ugettext_lazy('Dashboard')
permission_required = 'projector.can_see_dashboard'
default_weight = 10
icon_css_class = 'icon-home'
pattern_name = 'core_dashboard'

View File

Before

Width:  |  Height:  |  Size: 979 B

After

Width:  |  Height:  |  Size: 979 B

View File

@ -1,3 +0,0 @@
.icon-welcome {
background-position: 0 -24px;
}

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load staticfiles %}
@ -19,7 +19,7 @@
{% block content %} {% block content %}
<h1>{% trans 'Dashboard' %} <h1>{% trans 'Dashboard' %}
<small class="pull-right"> <small class="pull-right">
<a href="{% url 'projector_select_widgets' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage widgets' %}"> <a href="{% url 'core_select_widgets' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage widgets' %}">
<i class="icon-th-large"></i> <i class="icon-th-large"></i>
{% trans 'Widgets' %} {% trans 'Widgets' %}
</a> </a>

View File

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% block title %}{{ http_error.status_code }} {{ http_error.name }} {{ block.super }}{% endblock %}
{% block content %}
<h1>{{ http_error.name }}</h1>
<p>{{ http_error.description }}</p>
{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
@ -7,7 +7,7 @@
{% block content %} {% block content %}
<h1>{% trans 'Select widgets' %} <h1>{% trans 'Select widgets' %}
<small class="pull-right"> <small class="pull-right">
<a href="{% url 'dashboard' %}" class="btn btn-mini"><i class="icon-chevron-left"></i> {% trans "Back to overview" %}</a> <a href="{% url 'core_dashboard' %}" class="btn btn-mini"><i class="icon-chevron-left"></i> {% trans 'Back to overview' %}</a>
</small> </small>
</h1> </h1>

View File

@ -10,14 +10,21 @@ urlpatterns = patterns(
'', '',
# Redirect to dashboard URL # Redirect to dashboard URL
url(r'^$', url(r'^$',
RedirectView.as_view(url='projector/dashboard/'), RedirectView.as_view(url_name='core_dashboard'),
name='home',), name='home',),
url(r'^dashboard/$',
views.DashboardView.as_view(),
name='core_dashboard'),
url(r'^dashboard/select_widgets/$',
views.SelectWidgetsView.as_view(),
name='core_select_widgets'),
url(r'^version/$', url(r'^version/$',
views.VersionView.as_view(), views.VersionView.as_view(),
name='core_version',), name='core_version',),
url(r'^search/$', url(r'^search/$',
views.SearchView(), views.SearchView(),
name='search',), name='core_search',))
)

View File

@ -1,8 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.shortcuts import redirect, render_to_response
from django.template import RequestContext
from django.utils.importlib import import_module from django.utils.importlib import import_module
from django.utils.translation import ugettext as _
from haystack.views import SearchView as _SearchView from haystack.views import SearchView as _SearchView
from openslides import get_version as get_openslides_version from openslides import get_version as get_openslides_version
@ -10,7 +15,73 @@ from openslides import get_git_commit_id, RELEASE
from openslides.config.api import config from openslides.config.api import config
from openslides.utils.plugins import get_plugin_description, get_plugin_verbose_name, get_plugin_version from openslides.utils.plugins import get_plugin_description, get_plugin_verbose_name, get_plugin_version
from openslides.utils.signals import template_manipulation from openslides.utils.signals import template_manipulation
from openslides.utils.views import TemplateView from openslides.utils.views import AjaxMixin, TemplateView, View
from openslides.utils.widgets import Widget
from .forms import SelectWidgetsForm
class DashboardView(AjaxMixin, TemplateView):
"""
Overview over all possible slides, the overlays and a live view: the
Dashboard of OpenSlides. This main view uses the widget api to collect all
widgets from all apps. See openslides.utils.widgets.Widget for more details.
"""
permission_required = 'projector.can_see_dashboard' # TODO: Rename this to core.can_see_dashboard
template_name = 'core/dashboard.html'
def get_context_data(self, **kwargs):
context = super(DashboardView, self).get_context_data(**kwargs)
widgets = []
for widget in Widget.get_all(self.request):
if widget.is_active():
widgets.append(widget)
context['extra_stylefiles'].extend(widget.get_stylesheets())
context['extra_javascript'].extend(widget.get_javascript_files())
context['widgets'] = widgets
return context
class SelectWidgetsView(TemplateView):
"""
Shows a form to select which widgets should be displayed on the own
dashboard. The setting is saved in the session.
"""
# TODO: Use another base view class here, e. g. a FormView
permission_required = 'projector.can_see_dashboard' # TODO: Rename this to core.can_see_dashboard
template_name = 'core/select_widgets.html'
def get_context_data(self, **kwargs):
context = super(SelectWidgetsView, self).get_context_data(**kwargs)
widgets = Widget.get_all(self.request)
for widget in widgets:
initial = {'widget': widget.is_active()}
prefix = widget.name
if self.request.method == 'POST':
widget.form = SelectWidgetsForm(
self.request.POST,
prefix=prefix,
initial=initial)
else:
widget.form = SelectWidgetsForm(prefix=prefix, initial=initial)
context['widgets'] = widgets
return context
def post(self, request, *args, **kwargs):
"""
Activates or deactivates the widgets in a post request.
"""
context = self.get_context_data(**kwargs)
session_widgets = self.request.session.get('widgets', {})
for widget in context['widgets']:
if widget.form.is_valid():
session_widgets[widget.name] = widget.form.cleaned_data['widget']
else:
messages.error(request, _('There are errors in the form.'))
break
else:
self.request.session['widgets'] = session_widgets
return redirect(reverse('core_dashboard'))
class VersionView(TemplateView): class VersionView(TemplateView):
@ -82,3 +153,30 @@ class SearchView(_SearchView):
else: else:
models.append([module.Index.modelfilter_name, module.Index.modelfilter_value]) models.append([module.Index.modelfilter_name, module.Index.modelfilter_value])
return models return models
class ErrorView(View):
"""
View for Http 403, 404 and 500 error pages.
"""
status_code = None
def dispatch(self, request, *args, **kwargs):
http_error_strings = {
403: {'name': _('Forbidden'),
'description': _('Sorry, you have no permission to see this page.'),
'status_code': '403'},
404: {'name': _('Not Found'),
'description': _('Sorry, the requested page could not be found.'),
'status_code': '404'},
500: {'name': _('Internal Server Error'),
'description': _('Sorry, there was an unknown error. Please contact the event manager.'),
'status_code': '500'}}
context = {}
context['http_error'] = http_error_strings[self.status_code]
template_manipulation.send(sender=self.__class__, request=request, context=context)
response = render_to_response(
'core/error.html',
context_instance=RequestContext(request, context))
response.status_code = self.status_code
return response

View File

@ -14,7 +14,7 @@ class WelcomeWidget(Widget):
default_column = 1 default_column = 1
default_weight = 10 default_weight = 10
template_name = 'core/widget_welcome.html' template_name = 'core/widget_welcome.html'
stylesheets = ['styles/core.css'] icon_css_class = 'icon-home'
def get_verbose_name(self): def get_verbose_name(self):
return config['welcome_title'] return config['welcome_title']

View File

@ -117,6 +117,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.i18n', 'django.core.context_processors.i18n',
'django.core.context_processors.static', 'django.core.context_processors.static',
'openslides.utils.auth.anonymous_context_additions', 'openslides.utils.auth.anonymous_context_additions',
'openslides.utils.main_menu.main_menu_entries',
) )
CACHES = { CACHES = {

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import slides, widgets # noqa from . import main_menu, slides, template, widgets # noqa

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.main_menu import MainMenuEntry
class MediafileMainMenuEntry(MainMenuEntry):
"""
Main menu entry for the mediafile app.
"""
verbose_name = ugettext_lazy('Files')
default_weight = 60
pattern_name = 'mediafile_list'
icon_css_class = 'icon-paperclip'
def check_permission(self):
return (
self.request.user.has_perm('mediafile.can_see') or
self.request.user.has_perm('mediafile.can_upload') or
self.request.user.has_perm('mediafile.can_manage'))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,8 +1,8 @@
/** Navigation icons (mapping to glyphicons-halflings) **/ /** Navigation icons (mapping to glyphicons-halflings) **/
.icon-mediafile { .icon-paperclip {
background-image: url("../img/glyphicons_062_paperclip.png"); background-image: url("../img/glyphicons_062_paperclip.png");
background-position: 0; background-position: 0;
} }
.leftmenu ul li.active a span.ico i.icon-mediafile { .leftmenu ul li.active a span.ico i.icon-paperclip {
background-image: url("../img/glyphicons_062_paperclip_white.png"); background-image: url("../img/glyphicons_062_paperclip_white.png");
} }

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
from django.dispatch import receiver
from openslides.utils.signals import template_manipulation
@receiver(template_manipulation, dispatch_uid="add_mediafile_stylesheets")
def add_mediafile_stylesheets(sender, request, context, **kwargs):
"""
Adds the mediafile.css to the context.
"""
context['extra_stylefiles'].append('styles/mediafile.css')

View File

@ -1,12 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.translation import ugettext as _
from openslides.config.api import config from openslides.config.api import config
from openslides.projector.api import get_active_slide from openslides.projector.api import get_active_slide
from openslides.utils.template import Tab
from openslides.utils.tornado_webserver import ProjectorSocketHandler from openslides.utils.tornado_webserver import ProjectorSocketHandler
from openslides.utils.views import (AjaxView, CreateView, DeleteView, RedirectView, ListView, from openslides.utils.views import (AjaxView, CreateView, DeleteView, RedirectView, ListView,
UpdateView) UpdateView)
@ -199,7 +196,7 @@ class PdfToggleFullscreenView(RedirectView):
Toggle fullscreen mode for pdf presentations. Toggle fullscreen mode for pdf presentations.
""" """
allow_ajax = True allow_ajax = True
url_name = 'dashboard' url_name = 'core_dashboard'
def get_ajax_context(self, *args, **kwargs): def get_ajax_context(self, *args, **kwargs):
config['pdf_fullscreen'] = not config['pdf_fullscreen'] config['pdf_fullscreen'] = not config['pdf_fullscreen']
@ -208,19 +205,3 @@ class PdfToggleFullscreenView(RedirectView):
ProjectorSocketHandler.send_updates( ProjectorSocketHandler.send_updates(
{'calls': {'toggle_fullscreen': config['pdf_fullscreen']}}) {'calls': {'toggle_fullscreen': config['pdf_fullscreen']}})
return {'fullscreen': config['pdf_fullscreen']} return {'fullscreen': config['pdf_fullscreen']}
def register_tab(request):
"""
Inserts a new Tab to the views for files.
"""
selected = request.path.startswith('/mediafile/')
return Tab(
title=_('Files'),
app='mediafile', # TODO: Rename this to icon='mediafile' later
stylefile='styles/mediafile.css',
url=reverse('mediafile_list'),
permission=(request.user.has_perm('mediafile.can_see') or
request.user.has_perm('mediafile.can_upload') or
request.user.has_perm('mediafile.can_manage')),
selected=selected)

View File

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import personal_info, signals, slides, widgets # noqa from . import main_menu, personal_info, signals, slides, widgets # noqa

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.main_menu import MainMenuEntry
class MotionMainMenuEntry(MainMenuEntry):
"""
Main menu entry for the motion app.
"""
verbose_name = ugettext_lazy('Motions')
permission_required = 'motion.can_see_motion'
default_weight = 30
pattern_name = 'motion_list'
icon_css_class = 'icon-file'

View File

@ -12,7 +12,6 @@ from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelate
from openslides.config.api import config from openslides.config.api import config
from openslides.poll.views import PollFormView from openslides.poll.views import PollFormView
from openslides.projector.api import get_active_slide, update_projector from openslides.projector.api import get_active_slide, update_projector
from openslides.utils.template import Tab
from openslides.utils.utils import html_strong, htmldiff from openslides.utils.utils import html_strong, htmldiff
from openslides.utils.views import (CreateView, DeleteView, DetailView, from openslides.utils.views import (CreateView, DeleteView, DetailView,
FormView, ListView, PDFView, QuestionView, FormView, ListView, PDFView, QuestionView,
@ -810,16 +809,3 @@ class MotionCSVImportView(FormView):
return super(MotionCSVImportView, self).form_valid(form) return super(MotionCSVImportView, self).form_valid(form)
motion_csv_import = MotionCSVImportView.as_view() motion_csv_import = MotionCSVImportView.as_view()
def register_tab(request):
"""
Return the motion tab.
"""
# TODO: Find a better way to set the selected var.
return Tab(
title=_('Motions'),
app='motion',
url=reverse('motion_list'),
permission=request.user.has_perm('motion.can_see_motion'),
selected=request.path.startswith('/motion/'))

View File

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import signals, slides, widgets # noqa from . import main_menu, signals, slides, widgets # noqa

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.main_menu import MainMenuEntry
class ParticipantMainMenuEntry(MainMenuEntry):
"""
Main menu entry for the participant app.
"""
verbose_name = ugettext_lazy('Participants')
permission_required = 'participant.can_see_participant'
default_weight = 50
pattern_name = 'user_overview'
icon_css_class = 'icon-user'

View File

@ -43,7 +43,7 @@
{% trans 'Login' %} {% trans 'Login' %}
</button> </button>
{% if os_enable_anonymous_login %} {% if os_enable_anonymous_login %}
<a id="anonymous_login" class="btn" href="{% url 'dashboard' %}"> <a id="anonymous_login" class="btn" href="{% url 'core_dashboard' %}">
{% trans 'Continue as guest' %} {% trans 'Continue as guest' %}
</a> </a>
{% endif %} {% endif %}

View File

@ -10,7 +10,6 @@ from django.utils.translation import ugettext as _
from django.utils.translation import activate, ugettext_lazy from django.utils.translation import activate, ugettext_lazy
from openslides.config.api import config from openslides.config.api import config
from openslides.utils.template import Tab
from openslides.utils.utils import (delete_default_permissions, html_strong, from openslides.utils.utils import (delete_default_permissions, html_strong,
template) template)
from openslides.utils.views import (CreateView, DeleteView, DetailView, from openslides.utils.views import (CreateView, DeleteView, DetailView,
@ -420,7 +419,7 @@ def user_settings_password(request):
if form.is_valid(): if form.is_valid():
form.save() form.save()
messages.success(request, _('Password successfully changed.')) messages.success(request, _('Password successfully changed.'))
return redirect(reverse('dashboard')) return redirect(reverse('core_dashboard'))
else: else:
messages.error(request, _('Please check the form for errors.')) messages.error(request, _('Please check the form for errors.'))
else: else:
@ -429,18 +428,3 @@ def user_settings_password(request):
return { return {
'form': form, 'form': form,
} }
def register_tab(request):
"""
Registers the participant tab.
"""
selected = request.path.startswith('/participant/')
return Tab(
title=_('Participants'),
app='participant',
url=reverse('user_overview'),
permission=(
request.user.has_perm('participant.can_see_participant') or
request.user.has_perm('participant.can_manage_participant')),
selected=selected)

View File

@ -110,7 +110,8 @@ class ProjectorSlide(SlideMixin, models.Model):
""" """
Model for Slides, only for the projector. Also called custom slides. Model for Slides, only for the projector. Also called custom slides.
""" """
# TODO: Rename it to CustomSlide # TODO: Rename it to CustomSlide and move it to core app.
# Check and rename permissions.
slide_callback_name = 'projector_slide' slide_callback_name = 'projector_slide'
title = models.CharField(max_length=256, verbose_name=ugettext_lazy("Title")) title = models.CharField(max_length=256, verbose_name=ugettext_lazy("Title"))

View File

@ -7,7 +7,7 @@
{% block content %} {% block content %}
<h1>{% trans 'Custom slide' %} <h1>{% trans 'Custom slide' %}
<small class="pull-right"> <small class="pull-right">
<a href="{% url 'dashboard' %}" class="btn btn-mini"><i class="icon-chevron-left"></i> {% trans "Back to overview" %}</a> <a href="{% url 'core_dashboard' %}" class="btn btn-mini"><i class="icon-chevron-left"></i> {% trans "Back to overview" %}</a>
</small> </small>
</h1> </h1>
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}

View File

@ -23,14 +23,6 @@ urlpatterns = patterns(
views.ActivateView.as_view(), views.ActivateView.as_view(),
name='projector_activate_slide'), name='projector_activate_slide'),
url(r'^dashboard/$',
views.DashboardView.as_view(),
name='dashboard'),
url(r'^widgets/$',
views.SelectWidgetsView.as_view(),
name='projector_select_widgets'),
url(r'^overlay_message/$', url(r'^overlay_message/$',
views.OverlayMessageView.as_view(), views.OverlayMessageView.as_view(),
name='projector_overlay_message'), name='projector_overlay_message'),

View File

@ -1,45 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from openslides.config.api import config from openslides.config.api import config
from openslides.mediafile.models import Mediafile from openslides.mediafile.models import Mediafile
from openslides.utils.tornado_webserver import ProjectorSocketHandler from openslides.utils.tornado_webserver import ProjectorSocketHandler
from openslides.utils.template import Tab from openslides.utils.views import (CreateView, DeleteView,
from openslides.utils.views import (AjaxMixin, CreateView, DeleteView,
RedirectView, TemplateView, UpdateView) RedirectView, TemplateView, UpdateView)
from openslides.utils.widgets import Widget
from .api import (call_on_projector, get_active_slide, from .api import (call_on_projector, get_active_slide,
get_overlays, get_projector_content, get_projector_overlays, get_overlays, get_projector_content, get_projector_overlays,
get_projector_overlays_js, reset_countdown, set_active_slide, get_projector_overlays_js, reset_countdown, set_active_slide,
start_countdown, stop_countdown, update_projector_overlay) start_countdown, stop_countdown, update_projector_overlay)
from .forms import SelectWidgetsForm
from .models import ProjectorSlide from .models import ProjectorSlide
class DashboardView(AjaxMixin, TemplateView):
"""
Overview over all possible slides, the overlays and a live view.
"""
template_name = 'projector/dashboard.html'
permission_required = 'projector.can_see_dashboard'
def get_context_data(self, **kwargs):
context = super(DashboardView, self).get_context_data(**kwargs)
widgets = []
for widget in Widget.get_all(self.request):
if widget.is_active():
widgets.append(widget)
context['extra_stylefiles'].extend(widget.get_stylesheets())
context['extra_javascript'].extend(widget.get_javascript_files())
context['widgets'] = widgets
return context
class ProjectorView(TemplateView): class ProjectorView(TemplateView):
""" """
The Projector-Page. The Projector-Page.
@ -73,7 +46,7 @@ class ActivateView(RedirectView):
Activate a Slide. Activate a Slide.
""" """
permission_required = 'projector.can_manage_projector' permission_required = 'projector.can_manage_projector'
url_name = 'dashboard' url_name = 'core_dashboard'
allow_ajax = True allow_ajax = True
def pre_redirect(self, request, *args, **kwargs): def pre_redirect(self, request, *args, **kwargs):
@ -95,51 +68,12 @@ class ActivateView(RedirectView):
'scale': config['projector_scale']}) 'scale': config['projector_scale']})
class SelectWidgetsView(TemplateView):
"""
Show a Form to Select the widgets.
"""
permission_required = 'projector.can_see_dashboard'
template_name = 'projector/select_widgets.html'
def get_context_data(self, **kwargs):
context = super(SelectWidgetsView, self).get_context_data(**kwargs)
widgets = Widget.get_all(self.request)
for widget in widgets:
initial = {'widget': widget.is_active()}
prefix = widget.name
if self.request.method == 'POST':
widget.form = SelectWidgetsForm(self.request.POST, prefix=prefix,
initial=initial)
else:
widget.form = SelectWidgetsForm(prefix=prefix, initial=initial)
context['widgets'] = widgets
return context
def post(self, request, *args, **kwargs):
"""
Activates or deactivates the widgets in a post request.
"""
context = self.get_context_data(**kwargs)
session_widgets = self.request.session.get('widgets', {})
for widget in context['widgets']:
if widget.form.is_valid():
session_widgets[widget.name] = widget.form.cleaned_data['widget']
else:
messages.error(request, _('Errors in the form.'))
break
else:
self.request.session['widgets'] = session_widgets
return redirect(reverse('dashboard'))
class ProjectorControllView(RedirectView): class ProjectorControllView(RedirectView):
""" """
Scale or scroll the projector. Scale or scroll the projector.
""" """
permission_required = 'projector.can_manage_projector' permission_required = 'projector.can_manage_projector'
url_name = 'dashboard' url_name = 'core_dashboard'
allow_ajax = True allow_ajax = True
def pre_redirect(self, request, *args, **kwargs): def pre_redirect(self, request, *args, **kwargs):
@ -173,7 +107,7 @@ class CountdownControllView(RedirectView):
Start, stop or reset the countdown. Start, stop or reset the countdown.
""" """
permission_required = 'projector.can_manage_projector' permission_required = 'projector.can_manage_projector'
url_name = 'dashboard' url_name = 'core_dashboard'
allow_ajax = True allow_ajax = True
def pre_redirect(self, request, *args, **kwargs): def pre_redirect(self, request, *args, **kwargs):
@ -205,7 +139,7 @@ class OverlayMessageView(RedirectView):
""" """
Sets or clears the overlay message Sets or clears the overlay message
""" """
url_name = 'dashboard' url_name = 'core_dashboard'
allow_ajax = True allow_ajax = True
permission_required = 'projector.can_manage_projector' permission_required = 'projector.can_manage_projector'
@ -226,7 +160,7 @@ class ActivateOverlay(RedirectView):
""" """
Activate or deactivate an overlay. Activate or deactivate an overlay.
""" """
url_name = 'dashboard' url_name = 'core_dashboard'
allow_ajax = True allow_ajax = True
permission_required = 'projector.can_manage_projector' permission_required = 'projector.can_manage_projector'
@ -256,7 +190,7 @@ class CustomSlideCreateView(CreateView):
template_name = 'projector/new.html' template_name = 'projector/new.html'
model = ProjectorSlide model = ProjectorSlide
context_object_name = 'customslide' context_object_name = 'customslide'
success_url_name = 'dashboard' success_url_name = 'core_dashboard'
url_name_args = [] url_name_args = []
@ -268,7 +202,7 @@ class CustomSlideUpdateView(UpdateView):
template_name = 'projector/new.html' template_name = 'projector/new.html'
model = ProjectorSlide model = ProjectorSlide
context_object_name = 'customslide' context_object_name = 'customslide'
success_url_name = 'dashboard' success_url_name = 'core_dashboard'
url_name_args = [] url_name_args = []
@ -278,18 +212,4 @@ class CustomSlideDeleteView(DeleteView):
""" """
permission_required = 'projector.can_manage_projector' permission_required = 'projector.can_manage_projector'
model = ProjectorSlide model = ProjectorSlide
success_url_name = 'dashboard' success_url_name = 'core_dashboard'
def register_tab(request):
"""
Register the projector tab.
"""
selected = request.path.startswith('/projector/')
return Tab(
title=_('Dashboard'),
app='dashboard',
url=reverse('dashboard'),
permission=request.user.has_perm('projector.can_see_dashboard'),
selected=selected,
)

View File

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans 'Permission Denied' %}</h1>
{% trans 'Sorry, you have no rights to see this page.' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Page not found." %}</h1>
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Server Error" %}</h1>
{% endblock %}

View File

@ -29,7 +29,7 @@
{% block loginbutton %} {% block loginbutton %}
<div class="pull-right"> <div class="pull-right">
<!-- Search field --> <!-- Search field -->
<form class="navbar-search" action="{% url 'search' %}" method="get"> <form class="navbar-search" action="{% url 'core_search' %}" method="get">
<div class="input-append"> <div class="input-append">
<input type="text" id="id_q" name="q" class="input-medium" placeholder="{% trans 'Search' %}"> <input type="text" id="id_q" name="q" class="input-medium" placeholder="{% trans 'Search' %}">
<button type="submit" class="btn"><i class="icon-search"></i></button> <button type="submit" class="btn"><i class="icon-search"></i></button>
@ -60,24 +60,21 @@
</div> </div>
</div> <!--/.container-fluid--> </div> <!--/.container-fluid-->
{% block body %} {% block body %}
<!-- Container --> <!-- Container -->
<div class="container-fluid" id="container"> <div class="container-fluid" id="container">
<div class="row-fluid"> <div class="row-fluid">
<!-- Sidebar navigation--> <!-- Sidebar navigation (main menu) -->
<div class="span2 leftmenu lefticon"> <div class="span2 leftmenu lefticon">
<ul> <ul>
{% for tab in tabs %} {% for entry in main_menu_entries %}
{% if tab.permission %} <li{% if entry.is_active %} class="active"{% endif %}>
<li{% if tab.selected %} class="active"{% endif %}> <a href="{{ entry.get_url }}" class="tooltip-right">
<a href="{{ tab.url }}" class="tooltip-right"> <span class="ico"><i class="{{ entry.get_icon_css_class }}"></i></span>
<span class="ico"><i class="icon-{{ tab.app }}"></i></span> <span class="text">{{ entry }}</span>
<span class="text">{{ tab.title }}</span>
</a> </a>
</li> </li>
{% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -97,9 +94,7 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% block content %}{% endblock %}
{% block content %}
{% endblock %}
</div> </div>
</div> </div>
<hr /> <hr />
@ -108,10 +103,10 @@
&copy; Copyright 20112013 | Powered by <a href="http://openslides.org" target="_blank">OpenSlides</a> | <a href="{% url 'core_version' %}">Version</a> &copy; Copyright 20112013 | Powered by <a href="http://openslides.org" target="_blank">OpenSlides</a> | <a href="{% url 'core_version' %}">Version</a>
</small> </small>
</footer> </footer>
</div><!--/content--> </div><!--/#content-->
</div><!--/row-->
</div><!--/container-fluid-->
</div><!--/.row-fluid-->
</div><!--/#container-->
{% endblock %}<!--/body--> {% endblock %}<!--/body-->
<!-- JavaScript (Placed at the end of the document so the pages load faster) --> <!-- JavaScript (Placed at the end of the document so the pages load faster) -->

View File

@ -3,9 +3,12 @@
from django.conf import settings from django.conf import settings
from django.conf.urls import include, patterns, url from django.conf.urls import include, patterns, url
from openslides.core.views import ErrorView
from openslides.utils.plugins import get_urlpatterns from openslides.utils.plugins import get_urlpatterns
handler500 = 'openslides.utils.views.server_error' handler403 = ErrorView.as_view(status_code=403)
handler404 = ErrorView.as_view(status_code=404)
handler500 = ErrorView.as_view(status_code=500)
urlpatterns = [] urlpatterns = []

View File

@ -14,9 +14,15 @@ class SignalConnectMetaClass(type):
The classmethod get_all_objects is added as get_all classmethod to every The classmethod get_all_objects is added as get_all classmethod to every
class using this metaclass. Calling this on a base class or on child class using this metaclass. Calling this on a base class or on child
classes will retrieve all connected children, on instance for each child classes will retrieve all connected children, on instance for each child
class. These instances will have a check_permission method which class.
returns True by default. You can override this method to return False
on runtime if you want to filter some children. These instances will have a check_permission method which returns True
by default. You can override this method to return False on runtime if
you want to filter some children.
They will also have a get_default_weight method which returns the value
of the default_weight attribute which is 0 by default. You can override
the attribute or the method to sort the children.
Example: Example:
@ -37,7 +43,8 @@ class SignalConnectMetaClass(type):
""" """
def __new__(metaclass, class_name, class_parents, class_attributes): def __new__(metaclass, class_name, class_parents, class_attributes):
""" """
Creates the class and connects it to the signal if so. Creates the class and connects it to the signal if so. Adds all
default attributes and methods.
""" """
class_attributes['get_all'] = get_all_objects class_attributes['get_all'] = get_all_objects
new_class = super(SignalConnectMetaClass, metaclass).__new__( new_class = super(SignalConnectMetaClass, metaclass).__new__(
@ -53,8 +60,12 @@ class SignalConnectMetaClass(type):
raise NotImplementedError('Your class %s must have a signal argument, which must be a Django Signal instance.' % class_name) raise NotImplementedError('Your class %s must have a signal argument, which must be a Django Signal instance.' % class_name)
else: else:
signal.connect(new_class, dispatch_uid=dispatch_uid) signal.connect(new_class, dispatch_uid=dispatch_uid)
if not hasattr(new_class, 'check_permission'): attributes = {'check_permission': check_permission,
setattr(new_class, 'check_permission', check_permission) 'get_default_weight': get_default_weight,
'default_weight': 0}
for name, attribute in attributes.items():
if not hasattr(new_class, name):
setattr(new_class, name, attribute)
return new_class return new_class
@ -62,9 +73,8 @@ class SignalConnectMetaClass(type):
def get_all_objects(cls, request): def get_all_objects(cls, request):
""" """
Collects all objects of the class created by the SignalConnectMetaClass Collects all objects of the class created by the SignalConnectMetaClass
from all apps via signal. If they have a get_default_weight method, from all apps via signal. They are sorted using the get_default_weight
they are sorted. Does not return objects where check_permission returns method. Does not return objects where check_permission returns False.
False.
Expects a request object. Expects a request object.
@ -72,7 +82,6 @@ def get_all_objects(cls, request):
the SignalConnectMetaClass. the SignalConnectMetaClass.
""" """
all_objects = [obj for __, obj in cls.signal.send(sender=cls, request=request) if obj.check_permission()] all_objects = [obj for __, obj in cls.signal.send(sender=cls, request=request) if obj.check_permission()]
if hasattr(cls, 'get_default_weight'):
all_objects.sort(key=lambda obj: obj.get_default_weight()) all_objects.sort(key=lambda obj: obj.get_default_weight())
return all_objects return all_objects
@ -85,3 +94,14 @@ def check_permission(self):
SignalConnectMetaClass. SignalConnectMetaClass.
""" """
return True return True
def get_default_weight(self):
"""
Returns the value of the default_weight attribute by default. Override
this to sort some children on runtime.
This method is added to every instance of classes using the
SignalConnectMetaClass.
"""
return self.default_weight

View File

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse
from django.dispatch import Signal, receiver
from .dispatch import SignalConnectMetaClass
from .signals import template_manipulation
class MainMenuEntry(object):
"""
Base class for a main menu entry.
Every app which wants to add entries has to create a class subclassing
from this base class. For the appearance the verbose_name, the
pattern_name and the icon-css-class attribute have to be set. The
__metaclass__ attribute (SignalConnectMetaClass) does the rest of the
magic.
For the appearance there are some optional attributes like
permission_required, default_weight, stylesheets and javascript_files.
"""
__metaclass__ = SignalConnectMetaClass
signal = Signal(providing_args=['request'])
verbose_name = None
permission_required = None
default_weight = 0
pattern_name = None
icon_css_class = 'icon-home'
stylesheets = None
javascript_files = None
def __init__(self, sender, request, **kwargs):
"""
Initializes the main menu entry instance. This is done when the signal
is sent.
Only the required request argument is used. Because of Django's signal
API, we have to take also a sender argument and wildcard keyword
arguments. But they are not used here.
"""
self.request = request
def __unicode__(self):
if self.verbose_name is None:
raise NotImplementedError(
'The main menu entry class %s must provide a verbose_name '
'attribute or override the __unicode__ method.' % type(self).__name__)
return unicode(self.verbose_name)
@classmethod
def get_dispatch_uid(cls):
"""
Returns the classname as a unique string for each class. Returns None
for the base class so it will not be connected to the signal.
"""
if not cls.__name__ == 'MainMenuEntry':
return cls.__name__
def check_permission(self):
"""
Returns True if the request user is allowed to see the entry.
"""
return self.permission_required is None or self.request.user.has_perm(self.permission_required)
def get_icon_css_class(self):
"""
Returns the css class name of the icon. Default is 'icon-home'.
"""
return self.icon_css_class
def get_url(self):
"""
Returns the url of the entry.
"""
if self.pattern_name is None:
raise NotImplementedError(
'The main menu entry class %s must provide a pattern_name '
'attribute or override the get_url method.' % type(self).__name__)
return reverse(self.pattern_name)
def is_active(self):
"""
Returns True if the entry is selected at the moment.
"""
try:
return_value = isinstance(self, self.request.active_main_menu_class)
except AttributeError:
return_value = self.request.path.startswith(self.get_url())
return return_value
def get_stylesheets(self):
"""
Returns an interable of stylesheets to be loaded.
"""
return iter(self.stylesheets or [])
def get_javascript_files(self):
"""
Returns an interable of javascript files to be loaded.
"""
return iter(self.javascript_files or [])
def main_menu_entries(request):
"""
Adds all main menu entries to the request context as template context
processor.
"""
return {'main_menu_entries': MainMenuEntry.get_all(request)}
@receiver(template_manipulation, dispatch_uid="add_main_menu_context")
def add_main_menu_context(sender, request, context, **kwargs):
"""
Adds all stylefiles from all main menu entries to the context.
"""
for main_menu_entry in MainMenuEntry.get_all(request):
context['extra_stylefiles'].extend(main_menu_entry.get_stylesheets())
context['extra_javascript'].extend(main_menu_entry.get_javascript_files())

View File

@ -11,7 +11,7 @@ class PersonalInfo(object):
Every app which wants to add info has to create a class subclassing Every app which wants to add info has to create a class subclassing
from this base class. For the content the headline and default_weight from this base class. For the content the headline and default_weight
argument and the get_queryset method have to be set. The __metaclass__ attribute and the get_queryset method have to be set. The __metaclass__
attribute (SignalConnectMetaClass) does the rest of the magic. attribute (SignalConnectMetaClass) does the rest of the magic.
""" """
__metaclass__ = SignalConnectMetaClass __metaclass__ = SignalConnectMetaClass
@ -21,7 +21,7 @@ class PersonalInfo(object):
def __init__(self, sender, request, **kwargs): def __init__(self, sender, request, **kwargs):
""" """
Initialize the personal info instance. This is done when the signal is sent. Initializes the personal info instance. This is done when the signal is sent.
Only the required request argument is used. Because of Django's signal Only the required request argument is used. Because of Django's signal
API, we have to take also a sender argument and wildcard keyword API, we have to take also a sender argument and wildcard keyword
@ -38,12 +38,6 @@ class PersonalInfo(object):
if not cls.__name__ == 'PersonalInfo': if not cls.__name__ == 'PersonalInfo':
return cls.__name__ return cls.__name__
def get_default_weight(self):
"""
Returns the default weight of the personal info class.
"""
return self.default_weight
def get_queryset(self): def get_queryset(self):
""" """
Returns a queryset of objects for the personal info widget. Returns a queryset of objects for the personal info widget.

View File

@ -2,4 +2,16 @@
from django.dispatch import Signal from django.dispatch import Signal
template_manipulation = Signal(providing_args=['request', 'context'])
class TemplateManipulationSignal(Signal):
"""
Derived class to ensure that the key extra_stylefiles and extra_javascript
exist in the context dictionary.
"""
def send(self, **kwargs):
kwargs['context'].setdefault('extra_stylefiles', [])
kwargs['context'].setdefault('extra_javascript', [])
return super(TemplateManipulationSignal, self).send(**kwargs)
template_manipulation = TemplateManipulationSignal(providing_args=['request', 'context'])

View File

@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
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
self.stylefile = stylefile
self.url = url
self.permission = permission
self.selected = selected

View File

@ -9,13 +9,8 @@ from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.dispatch import receiver from django.http import (HttpResponse, HttpResponseRedirect)
from django.http import (HttpResponse, HttpResponseRedirect,
HttpResponseServerError)
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils.decorators import method_decorator 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 as _
from django.utils.translation import ugettext_lazy from django.utils.translation import ugettext_lazy
from django.views import generic as django_views from django.views import generic as django_views
@ -585,42 +580,3 @@ class PDFView(PermissionMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return self.render_to_response(self.get_filename()) return self.render_to_response(self.get_filename())
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)))
@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.
"""
tabs = []
if 'extra_stylefiles' in context:
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:
mod = import_module(app + '.views')
tab = mod.register_tab(request)
tabs.append(tab)
if tab.stylefile:
extra_stylefiles.append(tab.stylefile)
except (ImportError, AttributeError):
continue
context.update({
'tabs': tabs,
'extra_stylefiles': extra_stylefiles})

View File

@ -12,15 +12,15 @@ class Widget(object):
""" """
Base class for a widget for the dashboard. Base class for a widget for the dashboard.
Every app which wants to add a widget to the dashboard has to create a Every app which wants to add widgets to the dashboard has to create a
widget class subclassing from this base class. The name attribute has to widget class subclassing from this base class. The name attribute has to
be set. The __metaclass__ attribute (SignalConnectMetaClass) does the be set. The __metaclass__ attribute (SignalConnectMetaClass) does the
rest of the magic. rest of the magic.
For the appearance of the widget there are some more attributes like For the appearance of the widget there are some optional attributes like
verbose_name, permission_required, default_column, default_weight, verbose_name, permission_required, default_column, default_weight,
default_active, template_name, context, icon, more_link_pattern_name, default_active, template_name, context, icon_css_class,
stylesheets and javascript_files. more_link_pattern_name, stylesheets and javascript_files.
""" """
__metaclass__ = SignalConnectMetaClass __metaclass__ = SignalConnectMetaClass
signal = Signal(providing_args=['request']) signal = Signal(providing_args=['request'])
@ -39,7 +39,7 @@ class Widget(object):
def __init__(self, sender, request, **kwargs): def __init__(self, sender, request, **kwargs):
""" """
Initialize the widget instance. This is done when the signal is sent. Initializes the widget instance. This is done when the signal is sent.
Only the required request argument is used. Because of Django's signal Only the required request argument is used. Because of Django's signal
API, we have to take also a sender argument and wildcard keyword API, we have to take also a sender argument and wildcard keyword
@ -73,12 +73,6 @@ class Widget(object):
""" """
return self.permission_required is None or self.request.user.has_perm(self.permission_required) return self.permission_required is None or self.request.user.has_perm(self.permission_required)
def get_default_weight(self):
"""
Returns the default weight of the widget.
"""
return self.default_weight
def is_active(self): def is_active(self):
""" """
Returns True if the widget is active to be displayed. Returns True if the widget is active to be displayed.

View File

@ -51,7 +51,7 @@ class PersonalInfoWidget(TestCase):
self.client.login(username='HansMeiser', password='default') self.client.login(username='HansMeiser', password='default')
def test_widget_appearance(self): def test_widget_appearance(self):
response = self.client.get('/projector/dashboard/') response = self.client.get('/dashboard/')
self.assertContains(response, 'My personal info', status_code=200) self.assertContains(response, 'My personal info', status_code=200)
def test_item_list(self): def test_item_list(self):
@ -59,11 +59,11 @@ class PersonalInfoWidget(TestCase):
if agenda: if agenda:
item_1 = agenda.models.Item.objects.create(title='My Item Title iw5ohNgee4eiYahb5Eiv') item_1 = agenda.models.Item.objects.create(title='My Item Title iw5ohNgee4eiYahb5Eiv')
speaker = agenda.models.Speaker.objects.add(item=item_1, person=self.user) speaker = agenda.models.Speaker.objects.add(item=item_1, person=self.user)
response = self.client.get('/projector/dashboard/') response = self.client.get('/dashboard/')
self.assertContains(response, 'I am on the list of speakers of the following items:', status_code=200) self.assertContains(response, 'I am on the list of speakers of the following items:', status_code=200)
self.assertContains(response, 'My Item Title iw5ohNgee4eiYahb5Eiv', status_code=200) self.assertContains(response, 'My Item Title iw5ohNgee4eiYahb5Eiv', status_code=200)
speaker.begin_speach() speaker.begin_speach()
response = self.client.get('/projector/dashboard/') response = self.client.get('/dashboard/')
self.assertNotContains(response, 'My Item Title iw5ohNgee4eiYahb5Eiv', status_code=200) self.assertNotContains(response, 'My Item Title iw5ohNgee4eiYahb5Eiv', status_code=200)
def test_submitter_list(self): def test_submitter_list(self):
@ -73,7 +73,7 @@ class PersonalInfoWidget(TestCase):
motion_2 = motion.models.Motion.objects.create(title='My Motion Title quielohL7vah1weochai', text='My Motion Text') motion_2 = motion.models.Motion.objects.create(title='My Motion Title quielohL7vah1weochai', text='My Motion Text')
motion.models.MotionSubmitter.objects.create(motion=motion_1, person=self.user) motion.models.MotionSubmitter.objects.create(motion=motion_1, person=self.user)
motion.models.MotionSubmitter.objects.create(motion=motion_2, person=self.user) motion.models.MotionSubmitter.objects.create(motion=motion_2, person=self.user)
response = self.client.get('/projector/dashboard/') response = self.client.get('/dashboard/')
self.assertContains(response, 'I submitted the following motions:', status_code=200) self.assertContains(response, 'I submitted the following motions:', status_code=200)
self.assertContains(response, 'My Motion Title pa8aeNohYai0ahge', status_code=200) self.assertContains(response, 'My Motion Title pa8aeNohYai0ahge', status_code=200)
self.assertContains(response, 'My Motion Title quielohL7vah1weochai', status_code=200) self.assertContains(response, 'My Motion Title quielohL7vah1weochai', status_code=200)
@ -86,7 +86,7 @@ class PersonalInfoWidget(TestCase):
motion.models.MotionSupporter.objects.create(motion=motion_1, person=self.user) motion.models.MotionSupporter.objects.create(motion=motion_1, person=self.user)
motion.models.MotionSupporter.objects.create(motion=motion_2, person=self.user) motion.models.MotionSupporter.objects.create(motion=motion_2, person=self.user)
config['motion_min_supporters'] = 1 config['motion_min_supporters'] = 1
response = self.client.get('/projector/dashboard/') response = self.client.get('/dashboard/')
self.assertContains(response, 'I support the following motions:', status_code=200) self.assertContains(response, 'I support the following motions:', status_code=200)
self.assertContains(response, 'My Motion Title jahN9phaiThae5ooKubu', status_code=200) self.assertContains(response, 'My Motion Title jahN9phaiThae5ooKubu', status_code=200)
self.assertContains(response, 'My Motion Title vech9ash8aeh9eej2Ga2', status_code=200) self.assertContains(response, 'My Motion Title vech9ash8aeh9eej2Ga2', status_code=200)
@ -96,6 +96,6 @@ class PersonalInfoWidget(TestCase):
if assignment: if assignment:
assignment_1 = assignment.models.Assignment.objects.create(name='Hausmeister ooKoh7roApoo3phe', posts=1) assignment_1 = assignment.models.Assignment.objects.create(name='Hausmeister ooKoh7roApoo3phe', posts=1)
assignment_1.run(candidate=self.user, person=self.user) assignment_1.run(candidate=self.user, person=self.user)
response = self.client.get('/projector/dashboard/') response = self.client.get('/dashboard/')
self.assertContains(response, 'I am candidate for the following elections:', status_code=200) self.assertContains(response, 'I am candidate for the following elections:', status_code=200)
self.assertContains(response, 'Hausmeister ooKoh7roApoo3phe', status_code=200) self.assertContains(response, 'Hausmeister ooKoh7roApoo3phe', status_code=200)

View File

@ -240,7 +240,7 @@ class SpeakerListOpenView(SpeakerViewTestCase):
class GlobalListOfSpeakersLinks(SpeakerViewTestCase): class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
def test_global_redirect_url(self): def test_global_redirect_url(self):
response = self.speaker1_client.get('/agenda/list_of_speakers/') response = self.speaker1_client.get('/agenda/list_of_speakers/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.')
set_active_slide('agenda', pk=1) set_active_slide('agenda', pk=1)
@ -249,7 +249,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
def test_global_add_url(self): def test_global_add_url(self):
response = self.speaker1_client.get('/agenda/list_of_speakers/add/') response = self.speaker1_client.get('/agenda/list_of_speakers/add/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.')
set_active_slide('agenda', pk=1) set_active_slide('agenda', pk=1)
@ -265,38 +265,38 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
def test_global_next_speaker_url(self): def test_global_next_speaker_url(self):
response = self.admin_client.get('/agenda/list_of_speakers/next/') response = self.admin_client.get('/agenda/list_of_speakers/next/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.')
set_active_slide('agenda', pk=1) set_active_slide('agenda', pk=1)
response = self.admin_client.get('/agenda/list_of_speakers/next/') response = self.admin_client.get('/agenda/list_of_speakers/next/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertMessage(response, 'The list of speakers is empty.') self.assertMessage(response, 'The list of speakers is empty.')
response = self.speaker1_client.get('/agenda/list_of_speakers/add/') response = self.speaker1_client.get('/agenda/list_of_speakers/add/')
self.assertTrue(Speaker.objects.get(item__pk='1').begin_time is None) self.assertTrue(Speaker.objects.get(item__pk='1').begin_time is None)
response = self.admin_client.get('/agenda/list_of_speakers/next/') response = self.admin_client.get('/agenda/list_of_speakers/next/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertTrue(Speaker.objects.get(item__pk='1').begin_time is not None) self.assertTrue(Speaker.objects.get(item__pk='1').begin_time is not None)
def test_global_end_speach_url(self): def test_global_end_speach_url(self):
response = self.admin_client.get('/agenda/list_of_speakers/end_speach/') response = self.admin_client.get('/agenda/list_of_speakers/end_speach/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.')
set_active_slide('agenda', pk=1) set_active_slide('agenda', pk=1)
response = self.admin_client.get('/agenda/list_of_speakers/end_speach/') response = self.admin_client.get('/agenda/list_of_speakers/end_speach/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertMessage(response, 'There is no one speaking at the moment.') self.assertMessage(response, 'There is no one speaking at the moment.')
response = self.speaker1_client.get('/agenda/list_of_speakers/add/') response = self.speaker1_client.get('/agenda/list_of_speakers/add/')
self.assertTrue(Speaker.objects.get(item__pk='1').begin_time is None) self.assertTrue(Speaker.objects.get(item__pk='1').begin_time is None)
response = self.admin_client.get('/agenda/list_of_speakers/end_speach/') response = self.admin_client.get('/agenda/list_of_speakers/end_speach/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertMessage(response, 'There is no one speaking at the moment.') self.assertMessage(response, 'There is no one speaking at the moment.')
response = self.admin_client.get('/agenda/list_of_speakers/next/') response = self.admin_client.get('/agenda/list_of_speakers/next/')
self.assertTrue(Speaker.objects.get(item__pk='1').end_time is None) self.assertTrue(Speaker.objects.get(item__pk='1').end_time is None)
response = self.admin_client.get('/agenda/list_of_speakers/end_speach/') response = self.admin_client.get('/agenda/list_of_speakers/end_speach/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertTrue(Speaker.objects.get(item__pk='1').end_time is not None) self.assertTrue(Speaker.objects.get(item__pk='1').end_time is not None)

View File

@ -1,15 +1,70 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.test.client import Client from django.test.client import Client, RequestFactory
from mock import patch from mock import MagicMock, patch
from openslides import get_version from openslides import get_version
from openslides.agenda.models import Item from openslides.agenda.models import Item
from openslides.config.api import config from openslides.config.api import config
from openslides.core import views
from openslides.participant.models import User from openslides.participant.models import User
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
class SelectWidgetsViewTest(TestCase):
rf = RequestFactory()
@patch('openslides.core.views.SelectWidgetsForm')
@patch('openslides.core.views.TemplateView.get_context_data')
@patch('openslides.core.views.Widget')
def test_get_context_data(self, mock_Widget, mock_get_context_data,
mock_SelectWidgetsForm):
view = views.SelectWidgetsView()
view.request = self.rf.get('/')
view.request.session = MagicMock()
widget = MagicMock()
widget.name = 'some_widget_Bohsh1Pa0eeziRaihu8O'
widget.is_active.return_value = True
mock_Widget.get_all.return_value = [widget]
mock_get_context_data.return_value = {}
# Test get
context = view.get_context_data()
self.assertIn('widgets', context)
self.assertIn(widget, context['widgets'])
mock_SelectWidgetsForm.assert_called_with(
prefix='some_widget_Bohsh1Pa0eeziRaihu8O', initial={'widget': True})
# Test post
view.request = self.rf.post('/')
view.request.session = MagicMock()
context = view.get_context_data()
mock_SelectWidgetsForm.assert_called_with(
view.request.POST, prefix='some_widget_Bohsh1Pa0eeziRaihu8O', initial={'widget': True})
@patch('openslides.core.views.messages')
def test_post(self, mock_messages):
view = views.SelectWidgetsView()
view.request = self.rf.post('/')
view.request.session = {}
widget = MagicMock()
widget.name = 'some_widget_ahgaeree8JeReichue8u'
context = {'widgets': [widget]}
mock_context_data = MagicMock(return_value=context)
with patch('openslides.core.views.SelectWidgetsView.get_context_data', mock_context_data):
widget.form.is_valid.return_value = True
view.post(view.request)
self.assertIn('some_widget_ahgaeree8JeReichue8u', view.request.session['widgets'])
# Test with errors in form
widget.form.is_valid.return_value = False
view.request.session = {}
view.post(view.request)
self.assertNotIn('widgets', view.request.session)
mock_messages.error.assert_called_with(view.request, 'There are errors in the form.')
class VersionViewTest(TestCase): class VersionViewTest(TestCase):
def setUp(self): def setUp(self):
User.objects.create_user('CoreMaximilian', 'xxx@xx.xx', 'default') User.objects.create_user('CoreMaximilian', 'xxx@xx.xx', 'default')

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib.auth.models import AnonymousUser
from django.test.client import Client, RequestFactory from django.test.client import Client, RequestFactory
from mock import call, MagicMock, patch from mock import call, MagicMock, patch
@ -19,6 +20,7 @@ class ProjectorViewTest(TestCase):
mock_get_projector_overlays_js): mock_get_projector_overlays_js):
view = views.ProjectorView() view = views.ProjectorView()
view.request = self.rf.get('/') view.request = self.rf.get('/')
view.request.user = AnonymousUser()
# Test preview # Test preview
view.kwargs = {'callback': 'slide_callback'} view.kwargs = {'callback': 'slide_callback'}
@ -59,60 +61,6 @@ class ActivateViewTest(TestCase):
self.assertTrue(mock_call_on_projector.called) self.assertTrue(mock_call_on_projector.called)
class SelectWidgetsViewTest(TestCase):
rf = RequestFactory()
@patch('openslides.projector.views.SelectWidgetsForm')
@patch('openslides.projector.views.TemplateView.get_context_data')
@patch('openslides.projector.views.Widget')
def test_get_context_data(self, mock_Widget, mock_get_context_data,
mock_SelectWidgetsForm):
view = views.SelectWidgetsView()
view.request = self.rf.get('/')
view.request.session = MagicMock()
widget = MagicMock()
widget.name = 'some_widget_Bohsh1Pa0eeziRaihu8O'
widget.is_active.return_value = True
mock_Widget.get_all.return_value = [widget]
mock_get_context_data.return_value = {}
# Test get
context = view.get_context_data()
self.assertIn('widgets', context)
self.assertIn(widget, context['widgets'])
mock_SelectWidgetsForm.assert_called_with(
prefix='some_widget_Bohsh1Pa0eeziRaihu8O', initial={'widget': True})
# Test post
view.request = self.rf.post('/')
view.request.session = MagicMock()
context = view.get_context_data()
mock_SelectWidgetsForm.assert_called_with(
view.request.POST, prefix='some_widget_Bohsh1Pa0eeziRaihu8O', initial={'widget': True})
@patch('openslides.projector.views.messages')
def test_post(self, mock_messages):
view = views.SelectWidgetsView()
view.request = self.rf.post('/')
view.request.session = {}
widget = MagicMock()
widget.name = 'some_widget_ahgaeree8JeReichue8u'
context = {'widgets': [widget]}
mock_context_data = MagicMock(return_value=context)
with patch('openslides.projector.views.SelectWidgetsView.get_context_data', mock_context_data):
widget.form.is_valid.return_value = True
view.post(view.request)
self.assertIn('some_widget_ahgaeree8JeReichue8u', view.request.session['widgets'])
# Test with errors in form
widget.form.is_valid.return_value = False
view.request.session = {}
view.post(view.request)
self.assertNotIn('widgets', view.request.session)
mock_messages.error.assert_called_with(view.request, 'Errors in the form.')
class ProjectorControllViewTest(TestCase): class ProjectorControllViewTest(TestCase):
@patch('openslides.projector.views.call_on_projector') @patch('openslides.projector.views.call_on_projector')
def test_bigger(self, mock_call_on_projector): def test_bigger(self, mock_call_on_projector):
@ -176,7 +124,7 @@ class CustomSlidesTest(TestCase):
response = self.admin_client.get(url) response = self.admin_client.get(url)
self.assertTemplateUsed(response, 'projector/new.html') self.assertTemplateUsed(response, 'projector/new.html')
response = self.admin_client.post(url, {'title': 'test_title_roo2xi2EibooHie1kohd', 'weight': '0'}) response = self.admin_client.post(url, {'title': 'test_title_roo2xi2EibooHie1kohd', 'weight': '0'})
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertTrue(ProjectorSlide.objects.filter(title='test_title_roo2xi2EibooHie1kohd').exists()) self.assertTrue(ProjectorSlide.objects.filter(title='test_title_roo2xi2EibooHie1kohd').exists())
def test_update(self): def test_update(self):
@ -188,7 +136,7 @@ class CustomSlidesTest(TestCase):
self.assertTemplateUsed(response, 'projector/new.html') self.assertTemplateUsed(response, 'projector/new.html')
self.assertContains(response, 'test_title_jeeDeB3aedei8ahceeso') self.assertContains(response, 'test_title_jeeDeB3aedei8ahceeso')
response = self.admin_client.post(url, {'title': 'test_title_ai8Ooboh5bahr6Ee7goo', 'weight': '0'}) response = self.admin_client.post(url, {'title': 'test_title_ai8Ooboh5bahr6Ee7goo', 'weight': '0'})
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertEqual(ProjectorSlide.objects.get(pk=1).title, 'test_title_ai8Ooboh5bahr6Ee7goo') self.assertEqual(ProjectorSlide.objects.get(pk=1).title, 'test_title_ai8Ooboh5bahr6Ee7goo')
def test_delete(self): def test_delete(self):
@ -199,7 +147,7 @@ class CustomSlidesTest(TestCase):
response = self.admin_client.get(url) response = self.admin_client.get(url)
self.assertRedirects(response, '/projector/1/edit/') self.assertRedirects(response, '/projector/1/edit/')
response = self.admin_client.post(url, {'yes': 'true'}) response = self.admin_client.post(url, {'yes': 'true'})
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/dashboard/')
self.assertFalse(ProjectorSlide.objects.exists()) self.assertFalse(ProjectorSlide.objects.exists())

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.models import AnonymousUser
from django.test.client import RequestFactory
from openslides.utils.test import TestCase
from openslides.utils.main_menu import MainMenuEntry
class MainMenuEntryObject(TestCase):
request_factory = RequestFactory()
def get_entry(self, cls):
request = self.request_factory.get('/')
request.user = AnonymousUser()
for entry in MainMenuEntry.get_all(request):
if type(entry) == cls:
value = entry
break
else:
value = False
return value
def test_appearance(self):
class TestMenuEntryOne(MainMenuEntry):
pattern_name = 'core_version'
verbose_name = 'Menu entry for testing gae2thooc4che4thaoNo'
self.assertEqual(unicode(self.get_entry(TestMenuEntryOne)), u'Menu entry for testing gae2thooc4che4thaoNo')
def test_missing_verbose_name(self):
class TestMenuEntryBadOne(MainMenuEntry):
pattern_name = 'core_version'
entry = self.get_entry(TestMenuEntryBadOne)
text = ('The main menu entry class TestMenuEntryBadOne must provide a '
'verbose_name attribute or override the __unicode__ method.')
self.assertRaisesMessage(NotImplementedError, text, unicode, entry)
def test_missing_pattern_name(self):
class TestMenuEntryBadTwo(MainMenuEntry):
verbose_name = 'Menu entry for testing ahVeibai1iecaish2aeR'
entry = self.get_entry(TestMenuEntryBadTwo)
text = ('The main menu entry class TestMenuEntryBadTwo must provide a '
'pattern_name attribute or override the get_url method.')
self.assertRaisesMessage(NotImplementedError, text, entry.get_url)

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import clear_url_caches from django.core.urlresolvers import clear_url_caches
from django.test import RequestFactory from django.test import RequestFactory
@ -76,23 +77,23 @@ class AjaxMixinTest(ViewTestCase):
class ExtraContextMixinTest(ViewTestCase): class ExtraContextMixinTest(ViewTestCase):
""" """
Tests the ExtraContextMixin by testen the TemplateView Tests the ExtraContextMixin by testing the TemplateView.
""" """
def test_get_context_data(self): def test_get_context_data(self):
view = views.TemplateView() view = views.TemplateView()
get_context_data = view.get_context_data get_context_data = view.get_context_data
view.request = self.rf.get('/', {}) view.request = self.rf.get('/', {})
view.request.user = AnonymousUser()
context = get_context_data() context = get_context_data()
self.assertIn('tabs', context) self.assertIn('extra_stylefiles', context)
self.assertIn('extra_javascript', context)
context = get_context_data(some_context='context') context = get_context_data(some_context='context')
self.assertIn('tabs', context)
self.assertIn('some_context', context) self.assertIn('some_context', context)
template_manipulation.connect(set_context, dispatch_uid='set_context_test') template_manipulation.connect(set_context, dispatch_uid='set_context_test')
context = get_context_data() context = get_context_data()
self.assertIn('tabs', context)
self.assertIn('new_context', context) self.assertIn('new_context', context)
template_manipulation.disconnect(set_context, dispatch_uid='set_context_test') template_manipulation.disconnect(set_context, dispatch_uid='set_context_test')