From 21ff62dd32936787748503fa15a5ca3438a044fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Mon, 9 Dec 2013 23:56:01 +0100 Subject: [PATCH] Changes in projector and core app and in utils. Changed api for main menu entries. Enhanced http error pages using a classed based views. Moved dashboard and select widgets view from projector to core app. Also some small clean ups. --- CHANGELOG | 5 +- openslides/agenda/__init__.py | 2 +- openslides/agenda/main_menu.py | 16 +++ openslides/agenda/views.py | 21 +-- openslides/assignment/__init__.py | 2 +- openslides/assignment/main_menu.py | 16 +++ .../static/img/glyphicons_041_charts.png | Bin 0 -> 1029 bytes .../img/glyphicons_041_charts_white.png | Bin 0 -> 1059 bytes .../assignment/static/styles/assignment.css | 8 ++ openslides/assignment/template.py | 13 ++ openslides/assignment/views.py | 16 --- openslides/config/__init__.py | 3 + openslides/config/main_menu.py | 30 +++++ openslides/config/views.py | 13 -- openslides/core/__init__.py | 2 +- openslides/{projector => core}/forms.py | 0 openslides/core/main_menu.py | 16 +++ .../static/img/glyphicons_335_pushpin.png | Bin .../static/javascript/dashboard.js | 0 .../static/javascript/jquery.form.js | 0 openslides/core/static/styles/core.css | 3 - .../static/styles/dashboard.css | 0 .../templates/core}/dashboard.html | 4 +- openslides/core/templates/core/error.html | 8 ++ .../templates/core}/select_widgets.html | 4 +- openslides/core/urls.py | 13 +- openslides/core/views.py | 100 ++++++++++++++- openslides/core/widgets.py | 2 +- openslides/global_settings.py | 1 + openslides/mediafile/__init__.py | 3 +- openslides/mediafile/main_menu.py | 21 +++ .../static/img/glyphicons_062_paperclip.png | Bin 0 -> 1095 bytes .../img/glyphicons_062_paperclip_white.png | Bin 0 -> 1126 bytes .../mediafile/static/styles/mediafile.css | 4 +- openslides/mediafile/template.py | 13 ++ openslides/mediafile/views.py | 21 +-- openslides/motion/__init__.py | 2 +- openslides/motion/main_menu.py | 16 +++ openslides/motion/views.py | 14 -- openslides/participant/__init__.py | 2 +- openslides/participant/main_menu.py | 16 +++ .../templates/participant/login.html | 2 +- openslides/participant/views.py | 18 +-- openslides/projector/models.py | 3 +- .../projector/templates/projector/new.html | 2 +- openslides/projector/urls.py | 8 -- openslides/projector/views.py | 98 ++------------ openslides/templates/403.html | 8 -- openslides/templates/404.html | 7 - openslides/templates/500.html | 7 - openslides/templates/base.html | 101 +++++++-------- openslides/urls.py | 5 +- openslides/utils/dispatch.py | 42 ++++-- openslides/utils/main_menu.py | 120 ++++++++++++++++++ openslides/utils/personal_info.py | 10 +- openslides/utils/signals.py | 14 +- openslides/utils/template.py | 14 -- openslides/utils/views.py | 46 +------ openslides/utils/widgets.py | 16 +-- tests/account/test_widgets.py | 12 +- tests/agenda/test_list_of_speakers.py | 18 +-- tests/core/test_views.py | 59 ++++++++- tests/projector/test_views.py | 62 +-------- tests/utils/test_main_menu.py | 47 +++++++ tests/utils/test_views.py | 9 +- 65 files changed, 674 insertions(+), 464 deletions(-) create mode 100644 openslides/agenda/main_menu.py create mode 100644 openslides/assignment/main_menu.py create mode 100644 openslides/assignment/static/img/glyphicons_041_charts.png create mode 100644 openslides/assignment/static/img/glyphicons_041_charts_white.png create mode 100644 openslides/assignment/template.py create mode 100644 openslides/config/main_menu.py rename openslides/{projector => core}/forms.py (100%) create mode 100644 openslides/core/main_menu.py rename openslides/{projector => core}/static/img/glyphicons_335_pushpin.png (100%) rename openslides/{projector => core}/static/javascript/dashboard.js (100%) rename openslides/{projector => core}/static/javascript/jquery.form.js (100%) delete mode 100644 openslides/core/static/styles/core.css rename openslides/{projector => core}/static/styles/dashboard.css (100%) rename openslides/{projector/templates/projector => core/templates/core}/dashboard.html (89%) create mode 100644 openslides/core/templates/core/error.html rename openslides/{projector/templates/projector => core/templates/core}/select_widgets.html (83%) create mode 100644 openslides/mediafile/main_menu.py create mode 100644 openslides/mediafile/static/img/glyphicons_062_paperclip.png create mode 100644 openslides/mediafile/static/img/glyphicons_062_paperclip_white.png create mode 100644 openslides/mediafile/template.py create mode 100644 openslides/motion/main_menu.py create mode 100644 openslides/participant/main_menu.py delete mode 100644 openslides/templates/403.html delete mode 100644 openslides/templates/404.html delete mode 100644 openslides/templates/500.html create mode 100644 openslides/utils/main_menu.py delete mode 100644 openslides/utils/template.py create mode 100644 tests/utils/test_main_menu.py diff --git a/CHANGELOG b/CHANGELOG index ceff56008..d5524d69a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,7 +9,7 @@ Version 1.6 (unreleased) [https://github.com/OpenSlides/OpenSlides/issues?milestone=14] Participants: -- Disable widgets by default. +- Disabled widgets by default. Files: - Enabled update and delete view for uploader refering to his own files. Other: @@ -18,6 +18,9 @@ Other: - Renamed config api classes. - Renamed some classes of the poll api. - 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) diff --git a/openslides/agenda/__init__.py b/openslides/agenda/__init__.py index fbfccf222..f8db99882 100644 --- a/openslides/agenda/__init__.py +++ b/openslides/agenda/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import personal_info, signals, slides, widgets # noqa +from . import main_menu, personal_info, signals, slides, widgets # noqa diff --git a/openslides/agenda/main_menu.py b/openslides/agenda/main_menu.py new file mode 100644 index 000000000..4a4324d7b --- /dev/null +++ b/openslides/agenda/main_menu.py @@ -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' diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index b5410de6d..4e7557e7d 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -15,7 +15,6 @@ from openslides.config.api import config from openslides.projector.api import get_active_slide, update_projector from openslides.utils.exceptions import OpenSlidesError from openslides.utils.pdf import stylesheet -from openslides.utils.template import Tab from openslides.utils.utils import html_strong from openslides.utils.views import (CreateView, DeleteView, FormView, PDFView, RedirectView, SingleObjectMixin, @@ -569,7 +568,7 @@ class CurrentListOfSpeakersView(RedirectView): messages.error(request, _( 'There is no list of speakers for the current slide. ' 'Please choose the agenda item manually from the agenda.')) - return reverse('dashboard') + return reverse('core_dashboard') if self.set_speaker: if item.speaker_list_closed: @@ -613,25 +612,11 @@ class CurrentListOfSpeakersView(RedirectView): if item.type == Item.ORGANIZATIONAL_ITEM: if reverse_to_dashboard or not self.request.user.has_perm('agenda.can_see_orga_items'): - return reverse('dashboard') + return reverse('core_dashboard') else: return reverse('item_view', args=[item.pk]) else: if reverse_to_dashboard or not self.request.user.has_perm('agenda.can_see_agenda'): - return reverse('dashboard') + return reverse('core_dashboard') else: 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) diff --git a/openslides/assignment/__init__.py b/openslides/assignment/__init__.py index fbfccf222..8b1da0397 100644 --- a/openslides/assignment/__init__.py +++ b/openslides/assignment/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import personal_info, signals, slides, widgets # noqa +from . import main_menu, personal_info, signals, slides, template, widgets # noqa diff --git a/openslides/assignment/main_menu.py b/openslides/assignment/main_menu.py new file mode 100644 index 000000000..6313ab88e --- /dev/null +++ b/openslides/assignment/main_menu.py @@ -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' diff --git a/openslides/assignment/static/img/glyphicons_041_charts.png b/openslides/assignment/static/img/glyphicons_041_charts.png new file mode 100644 index 0000000000000000000000000000000000000000..556b7cce5a2ee6dab0952153470c028fdfc44424 GIT binary patch literal 1029 zcmV+g1p51lP)4Tx04R}-l+Q~PVHn51vyB)iL11Qkh@nGBs7w}RMCGo(*tBg~f0Po$?CiU{ zle4qa%26U(f|ru*d1uDm5$(qdGxK?$AK&MB z-{F0M8d)(-JtP5n!?BWmQR({Vm{j`_Asj&+!l+YJ+l(X<1E9fUv1@F;hrqupt$X|b zI_lE4ng@jaKh%lTb}(f=ak3uu6-!kMZ8FSKs7BM z|C+c%%_(W1MkH>@24YeH&g(_h@8=*r^~@L^r0;R+=`OQ-d=_TXN_RhT8}a8f>+a#2 z#Pb=gH%8n{&sxUn9rjo_p*gW3k3%Dd9v|?z$wwEp4JbjhwM#!rpOV)g4O?IHuKzA8qHe6NWHQ4?5 zW?+>pP^lFuS83d>RC0Yby(58Yf7(5YU7B8Os@7T+7jCT7FHP^*tHQ2TvC67;&Z}|3 zd?p?hPl_?ILp%pbyd-vt7sYOxofiix8N;NaVZv3ip-`5AN%ERI{+SmG@2t6_p^q8N z(o~6`|AbY@XgUiV(SNxAbMSk`qp>3J!UhhXuG%(|?j0gDE>~^N72v`*x)0t~ZOwEZ z4m|-D&ZtGJ}8;WLk96sqPn3I;4%{fxLET5GkbR{t#`+_*~}`j zbQ|o>r&YYHREPJ%uUkhq zH(cWcXHBtMf9UBp4b94Tx0C?Jslgn!qSrmuA>fE4^APlsxUJRF6NRT9y4>Te^(n)Nhv2DsklORZ^ z`gV7jo9dcd)o~ON$;PG5FnjT_aN!@|!i6i{C=q-ihL_x=1& zqpK9Ub6~B6)ccYOUFi4>)GCux(;zgEJ{biOh;u;N#QOh0dM4)UK-!EuLm+MetK7gu zAnpMJZnao~P66~*5;Zdkp3`-=InN(JxAG?r0~;W9R)r%?wiZgo;R8TFSg%TIcm(JL z>w4MSHwyFs&&y`tM9kJkFB%<>Id#O>`LQOi^Jnshnw}zvvd)r!IDdF#Nk38CUjQ;- zIkU9`4zLDNdrw!#kH&22)77DTlOLJ7GXeYsscV5QjK#C1?l6T$;o+n3kvQbY(no-y zMi~zejYkfJkHX*)GW*?1O(DUO953;CiNWJ<>L?@*N66+`=xsSj`>FlLer#X1ui39J zX6ild%z`=Yxr-OxQ9xl@zRhfNrr6)5#vmF^1;t!zr!X;TV!E<_G1GQ(7cab1=kp&E zTdG8-K4wsa%wq7EN1+K(3?{n$==K!)?wMQb+Pxl&o5vn@-#xSJu4>({vqfgs^sNTl?T(0oIzuwiX?PU2mb|L0jv|!0JnI zbDLW?^G$2Zx(|{1CjuC%1*eQxsYY_MZEu%@L7jf;r5O8yZM^KQdxr#Qp{%Z#^pPX17f9yFZe07pf#Tz_5D76pUwrvjU?b9Xqy zK5p?fa=5{oF1ub}jy=5P=c>}aDtO;w2ag$CVwQYX@D5wWp>~^yc)@l~?(<`U@1p1@ dCr)$X>o2)af)S~ec`yI~002ovPDHLkV1gBa=ZydW literal 0 HcmV?d00001 diff --git a/openslides/assignment/static/styles/assignment.css b/openslides/assignment/static/styles/assignment.css index 5bd992410..67363c94f 100644 --- a/openslides/assignment/static/styles/assignment.css +++ b/openslides/assignment/static/styles/assignment.css @@ -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 { background-color: #BED4DE !important; } diff --git a/openslides/assignment/template.py b/openslides/assignment/template.py new file mode 100644 index 000000000..5e7aa25f1 --- /dev/null +++ b/openslides/assignment/template.py @@ -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') diff --git a/openslides/assignment/views.py b/openslides/assignment/views.py index f9fb3d1c7..941f6f65f 100644 --- a/openslides/assignment/views.py +++ b/openslides/assignment/views.py @@ -19,7 +19,6 @@ from openslides.participant.models import Group, User from openslides.poll.views import PollFormView from openslides.utils.pdf import stylesheet from openslides.utils.person import get_person -from openslides.utils.template import Tab from openslides.utils.utils import html_strong from openslides.utils.views import (CreateView, DeleteView, DetailView, ListView, PDFView, PermissionMixin, @@ -606,18 +605,3 @@ class AssignmentPollPDF(PDFView): ('GRID', (0, 0), (-1, -1), 0.25, colors.grey), ('VALIGN', (0, 0), (-1, -1), 'TOP')])) 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, - ) diff --git a/openslides/config/__init__.py b/openslides/config/__init__.py index e69de29bb..d42c22903 100644 --- a/openslides/config/__init__.py +++ b/openslides/config/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import main_menu # noqa diff --git a/openslides/config/main_menu.py b/openslides/config/main_menu.py new file mode 100644 index 000000000..b9249652d --- /dev/null +++ b/openslides/config/main_menu.py @@ -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 diff --git a/openslides/config/views.py b/openslides/config/views.py index 49e7e2d1e..9680eb375 100644 --- a/openslides/config/views.py +++ b/openslides/config/views.py @@ -5,7 +5,6 @@ from django.contrib import messages from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ -from openslides.utils.template import Tab from openslides.utils.views import FormView from .api import config @@ -109,15 +108,3 @@ class ConfigView(FormView): config[key] = form.cleaned_data[key] messages.success(self.request, _('%s settings successfully saved.') % _(self.config_collection.title)) 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/')) diff --git a/openslides/core/__init__.py b/openslides/core/__init__.py index a5101f887..0c6390484 100644 --- a/openslides/core/__init__.py +++ b/openslides/core/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import signals, widgets # noqa +from . import main_menu, signals, widgets # noqa diff --git a/openslides/projector/forms.py b/openslides/core/forms.py similarity index 100% rename from openslides/projector/forms.py rename to openslides/core/forms.py diff --git a/openslides/core/main_menu.py b/openslides/core/main_menu.py new file mode 100644 index 000000000..e44425e51 --- /dev/null +++ b/openslides/core/main_menu.py @@ -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' diff --git a/openslides/projector/static/img/glyphicons_335_pushpin.png b/openslides/core/static/img/glyphicons_335_pushpin.png similarity index 100% rename from openslides/projector/static/img/glyphicons_335_pushpin.png rename to openslides/core/static/img/glyphicons_335_pushpin.png diff --git a/openslides/projector/static/javascript/dashboard.js b/openslides/core/static/javascript/dashboard.js similarity index 100% rename from openslides/projector/static/javascript/dashboard.js rename to openslides/core/static/javascript/dashboard.js diff --git a/openslides/projector/static/javascript/jquery.form.js b/openslides/core/static/javascript/jquery.form.js similarity index 100% rename from openslides/projector/static/javascript/jquery.form.js rename to openslides/core/static/javascript/jquery.form.js diff --git a/openslides/core/static/styles/core.css b/openslides/core/static/styles/core.css deleted file mode 100644 index f1e4a8fde..000000000 --- a/openslides/core/static/styles/core.css +++ /dev/null @@ -1,3 +0,0 @@ -.icon-welcome { - background-position: 0 -24px; -} diff --git a/openslides/projector/static/styles/dashboard.css b/openslides/core/static/styles/dashboard.css similarity index 100% rename from openslides/projector/static/styles/dashboard.css rename to openslides/core/static/styles/dashboard.css diff --git a/openslides/projector/templates/projector/dashboard.html b/openslides/core/templates/core/dashboard.html similarity index 89% rename from openslides/projector/templates/projector/dashboard.html rename to openslides/core/templates/core/dashboard.html index db7832696..2df494226 100644 --- a/openslides/projector/templates/projector/dashboard.html +++ b/openslides/core/templates/core/dashboard.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends 'base.html' %} {% load i18n %} {% load staticfiles %} @@ -19,7 +19,7 @@ {% block content %}

{% trans 'Dashboard' %} - + {% trans 'Widgets' %} diff --git a/openslides/core/templates/core/error.html b/openslides/core/templates/core/error.html new file mode 100644 index 000000000..ebf045dd5 --- /dev/null +++ b/openslides/core/templates/core/error.html @@ -0,0 +1,8 @@ +{% extends 'base.html' %} + +{% block title %}{{ http_error.status_code }} {{ http_error.name }} – {{ block.super }}{% endblock %} + +{% block content %} +

{{ http_error.name }}

+

{{ http_error.description }}

+{% endblock %} diff --git a/openslides/projector/templates/projector/select_widgets.html b/openslides/core/templates/core/select_widgets.html similarity index 83% rename from openslides/projector/templates/projector/select_widgets.html rename to openslides/core/templates/core/select_widgets.html index b9ed3e9b4..fc9c2f838 100644 --- a/openslides/projector/templates/projector/select_widgets.html +++ b/openslides/core/templates/core/select_widgets.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends 'base.html' %} {% load i18n %} @@ -7,7 +7,7 @@ {% block content %}

{% trans 'Select widgets' %} - {% trans "Back to overview" %} + {% trans 'Back to overview' %}

diff --git a/openslides/core/urls.py b/openslides/core/urls.py index dc157fe90..b859ddeba 100644 --- a/openslides/core/urls.py +++ b/openslides/core/urls.py @@ -10,14 +10,21 @@ urlpatterns = patterns( '', # Redirect to dashboard URL url(r'^$', - RedirectView.as_view(url='projector/dashboard/'), + RedirectView.as_view(url_name='core_dashboard'), 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/$', views.VersionView.as_view(), name='core_version',), url(r'^search/$', views.SearchView(), - name='search',), -) + name='core_search',)) diff --git a/openslides/core/views.py b/openslides/core/views.py index 7d28d4584..219d6c85a 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -1,8 +1,13 @@ # -*- coding: utf-8 -*- from django.conf import settings +from django.contrib import messages 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.translation import ugettext as _ from haystack.views import SearchView as _SearchView 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.utils.plugins import get_plugin_description, get_plugin_verbose_name, get_plugin_version 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): @@ -82,3 +153,30 @@ class SearchView(_SearchView): else: models.append([module.Index.modelfilter_name, module.Index.modelfilter_value]) 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 diff --git a/openslides/core/widgets.py b/openslides/core/widgets.py index c06ae17cc..acd0b2ff4 100644 --- a/openslides/core/widgets.py +++ b/openslides/core/widgets.py @@ -14,7 +14,7 @@ class WelcomeWidget(Widget): default_column = 1 default_weight = 10 template_name = 'core/widget_welcome.html' - stylesheets = ['styles/core.css'] + icon_css_class = 'icon-home' def get_verbose_name(self): return config['welcome_title'] diff --git a/openslides/global_settings.py b/openslides/global_settings.py index a12030fb5..8028aff5c 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -117,6 +117,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.i18n', 'django.core.context_processors.static', 'openslides.utils.auth.anonymous_context_additions', + 'openslides.utils.main_menu.main_menu_entries', ) CACHES = { diff --git a/openslides/mediafile/__init__.py b/openslides/mediafile/__init__.py index acd07f9be..0a79488ec 100644 --- a/openslides/mediafile/__init__.py +++ b/openslides/mediafile/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -from . import slides, widgets # noqa +from . import main_menu, slides, template, widgets # noqa diff --git a/openslides/mediafile/main_menu.py b/openslides/mediafile/main_menu.py new file mode 100644 index 000000000..157a3fa36 --- /dev/null +++ b/openslides/mediafile/main_menu.py @@ -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')) diff --git a/openslides/mediafile/static/img/glyphicons_062_paperclip.png b/openslides/mediafile/static/img/glyphicons_062_paperclip.png new file mode 100644 index 0000000000000000000000000000000000000000..fed88de96116b5440440c04a8511be79110cb1fd GIT binary patch literal 1095 zcmV-N1i1T&P)4Tx04R}-l+Q~PVHn51vyB)iL11Qkh@nGBs7w}RMCGo(*tBg~f0Po$?CiU{ zle4qa%26U(f|ru*d1uDm5$(qdGxK?$AK&MB z-{F0M8d)(-JtP5n!?BWmQR({Vm{j`_Asj&+!l+YJ+l(X<1E9fUv1@F;hrqupt$X|b zI_lE4ng@jaKh%lTb}(f=ak3uu6-!kMZ8FSKs7BM z|C+c%%_(W1MkH>@24YeH&g(_h@8=*r^~@L^r0;R+=`OQ-d=_TXN_RhT8}a8f>+a#2 z#Pb=gH%8n{&sxUn9rjo_p*gW3k3%Dd9v|?z$wwEp4JbjhwM#!rpOV)g4O?IHuKzA8qHe6NWHQ4?5 zW?+>pP^lFuS83d>RC0Yby(58Yf7(5YU7B8Os@7T+7jCT7FHP^*tHQ2TvC67;&Z}|3 zd?p?hPl_?ILp%pbyd-vt7sYOxofiix8N;NaVZv3ip-`5AN%ERI{+SmG@2t6_p^q8N z(o~6`|AbY@XgUiV(SNxAbMSk`qp>3J!UhhXuG%(|?j0gDE>~^N72v`*x)0t~ZOwEZ z4m|-D&ZtGJ}8;WLk96sqPn3I;4%{fxLET5GkbR{t#`+_*~}`j zbQ|o(`)Fyu}^%v4>0a;TbfV z&>G(4Q+Lwa!U^7G<{vatfjaRAyXmdrFS>DrGnGLEzN0^(z4TVn>&^Zm`_`VpZyB7% zcIu@J_F*ygM0z$+s|=|$K>b)qJz1X#)G*udEz1>XKJ};cn(8-h;72~W73&!s#N7X& zI@pE_{KgG_p$9VweQbmx;;ElfN((-q1&0xFTS^(v@Ifi1DIy+J)+05k+Vx>N@MHi0 N002ovPDHLkV1j7K1l|Au literal 0 HcmV?d00001 diff --git a/openslides/mediafile/static/img/glyphicons_062_paperclip_white.png b/openslides/mediafile/static/img/glyphicons_062_paperclip_white.png new file mode 100644 index 0000000000000000000000000000000000000000..cceb921747e81da8bf9512f0b7c4d929bb69747f GIT binary patch literal 1126 zcmV-s1eyDZP)4Tx065Eyl+Q~PVHn51vyB)iL11Qkh@nGBs7w}RMCGo(*tBg~f0Po$?CiU{ zle4qa%26U(f|ru*d1uDm5$(qdGxK?$AK&MB z-{F0M8d)(-JtP5n!?BWmQR({Vm{j`_Asj&+!l+YJ+l(X<1E9fUv1@F;hrqupt$X|b zI_lE4ng@jaKh%lTb}(f=ak3uu6-!kMZ8FSKs7BM z|C+c%%_(W1MkH>@24YeH&g(_h@8=*r^~@L^r0;R+=`OQ-d=_TXN_RhT8}a8f>+a#2 z#Pb=gH%8n{&sxUn9rjo_p*gW3k3%Dd9v|?z$wwEp4JbjhwM#!rpOV)g4O?IHuKzA8qHe6NWHQ4?5 zW?+>pP^lFuS83d>RC0Yby(58Yf7(5YU7B8Os@7T+7jCT7FHP^*tHQ2TvC67;&Z}|3 zd?p?hPl_?ILp%pbyd-vt7sYOxofiix8N;NaVZv3ip-`5AN%ERI{+SmG@2t6_p^q8N z(o~6`|AbY@XgUiV(SNxAbMSk`qp>3J!UhhXuG%(|?j0gDE>~^N72v`*x)0t~ZOwEZ z4m|-D&ZtGJ}8;WLk96sqPn3I;4%{fxLET5GkbR{t#`+_*~}`j zbQ|oy{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*i03?45mu$NT; z009LY3Q!lG z@GG@Vyr2hva2kb=fPK^ws;9P|T5rw=)j&rY_tH3nA6c)au@6gGk2lt_i}S*~l@e6L zV%8Il@gkwQ(puIE&_dQzskJm-+`)CW+>Y%ue#Lw {% if os_enable_anonymous_login %} - + {% trans 'Continue as guest' %} {% endif %} diff --git a/openslides/participant/views.py b/openslides/participant/views.py index 185fdc61b..accfbb909 100644 --- a/openslides/participant/views.py +++ b/openslides/participant/views.py @@ -10,7 +10,6 @@ from django.utils.translation import ugettext as _ from django.utils.translation import activate, ugettext_lazy from openslides.config.api import config -from openslides.utils.template import Tab from openslides.utils.utils import (delete_default_permissions, html_strong, template) from openslides.utils.views import (CreateView, DeleteView, DetailView, @@ -420,7 +419,7 @@ def user_settings_password(request): if form.is_valid(): form.save() messages.success(request, _('Password successfully changed.')) - return redirect(reverse('dashboard')) + return redirect(reverse('core_dashboard')) else: messages.error(request, _('Please check the form for errors.')) else: @@ -429,18 +428,3 @@ def user_settings_password(request): return { '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) diff --git a/openslides/projector/models.py b/openslides/projector/models.py index fdfe9f11c..ce58a5098 100644 --- a/openslides/projector/models.py +++ b/openslides/projector/models.py @@ -110,7 +110,8 @@ class ProjectorSlide(SlideMixin, models.Model): """ 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' title = models.CharField(max_length=256, verbose_name=ugettext_lazy("Title")) diff --git a/openslides/projector/templates/projector/new.html b/openslides/projector/templates/projector/new.html index d1e3d77e8..6d6c088a4 100644 --- a/openslides/projector/templates/projector/new.html +++ b/openslides/projector/templates/projector/new.html @@ -7,7 +7,7 @@ {% block content %}

{% trans 'Custom slide' %} - {% trans "Back to overview" %} + {% trans "Back to overview" %}

{% csrf_token %} diff --git a/openslides/projector/urls.py b/openslides/projector/urls.py index 0af304f26..d9bc69d85 100644 --- a/openslides/projector/urls.py +++ b/openslides/projector/urls.py @@ -23,14 +23,6 @@ urlpatterns = patterns( views.ActivateView.as_view(), 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/$', views.OverlayMessageView.as_view(), name='projector_overlay_message'), diff --git a/openslides/projector/views.py b/openslides/projector/views.py index 7095efccc..fa84d2522 100644 --- a/openslides/projector/views.py +++ b/openslides/projector/views.py @@ -1,45 +1,18 @@ # -*- 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.mediafile.models import Mediafile from openslides.utils.tornado_webserver import ProjectorSocketHandler -from openslides.utils.template import Tab -from openslides.utils.views import (AjaxMixin, CreateView, DeleteView, +from openslides.utils.views import (CreateView, DeleteView, RedirectView, TemplateView, UpdateView) -from openslides.utils.widgets import Widget from .api import (call_on_projector, get_active_slide, get_overlays, get_projector_content, get_projector_overlays, get_projector_overlays_js, reset_countdown, set_active_slide, start_countdown, stop_countdown, update_projector_overlay) -from .forms import SelectWidgetsForm 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): """ The Projector-Page. @@ -73,7 +46,7 @@ class ActivateView(RedirectView): Activate a Slide. """ permission_required = 'projector.can_manage_projector' - url_name = 'dashboard' + url_name = 'core_dashboard' allow_ajax = True def pre_redirect(self, request, *args, **kwargs): @@ -95,51 +68,12 @@ class ActivateView(RedirectView): '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): """ Scale or scroll the projector. """ permission_required = 'projector.can_manage_projector' - url_name = 'dashboard' + url_name = 'core_dashboard' allow_ajax = True def pre_redirect(self, request, *args, **kwargs): @@ -173,7 +107,7 @@ class CountdownControllView(RedirectView): Start, stop or reset the countdown. """ permission_required = 'projector.can_manage_projector' - url_name = 'dashboard' + url_name = 'core_dashboard' allow_ajax = True def pre_redirect(self, request, *args, **kwargs): @@ -205,7 +139,7 @@ class OverlayMessageView(RedirectView): """ Sets or clears the overlay message """ - url_name = 'dashboard' + url_name = 'core_dashboard' allow_ajax = True permission_required = 'projector.can_manage_projector' @@ -226,7 +160,7 @@ class ActivateOverlay(RedirectView): """ Activate or deactivate an overlay. """ - url_name = 'dashboard' + url_name = 'core_dashboard' allow_ajax = True permission_required = 'projector.can_manage_projector' @@ -256,7 +190,7 @@ class CustomSlideCreateView(CreateView): template_name = 'projector/new.html' model = ProjectorSlide context_object_name = 'customslide' - success_url_name = 'dashboard' + success_url_name = 'core_dashboard' url_name_args = [] @@ -268,7 +202,7 @@ class CustomSlideUpdateView(UpdateView): template_name = 'projector/new.html' model = ProjectorSlide context_object_name = 'customslide' - success_url_name = 'dashboard' + success_url_name = 'core_dashboard' url_name_args = [] @@ -278,18 +212,4 @@ class CustomSlideDeleteView(DeleteView): """ permission_required = 'projector.can_manage_projector' model = ProjectorSlide - success_url_name = '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, - ) + success_url_name = 'core_dashboard' diff --git a/openslides/templates/403.html b/openslides/templates/403.html deleted file mode 100644 index 6fc6432ef..000000000 --- a/openslides/templates/403.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} - -{% block content %} -

{% trans 'Permission Denied' %}

- {% trans 'Sorry, you have no rights to see this page.' %} -{% endblock %} diff --git a/openslides/templates/404.html b/openslides/templates/404.html deleted file mode 100644 index 4aeb91eb7..000000000 --- a/openslides/templates/404.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} - -{% block content %} -

{% trans "Page not found." %}

-{% endblock %} diff --git a/openslides/templates/500.html b/openslides/templates/500.html deleted file mode 100644 index 3fd96db4a..000000000 --- a/openslides/templates/500.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} - -{% block content %} -

{% trans "Server Error" %}

-{% endblock %} diff --git a/openslides/templates/base.html b/openslides/templates/base.html index 40fcb0584..37f2a2f13 100644 --- a/openslides/templates/base.html +++ b/openslides/templates/base.html @@ -3,7 +3,7 @@ {% load staticfiles %} - + @@ -29,7 +29,7 @@ {% block loginbutton %}
- +
@@ -60,61 +60,56 @@
+ {% block body %} + +
+
-{% block body %} - -
-
+ +
+ +
- -
- -
+ +
+
+
+
+ + {% for message in messages %} +
+ + {{ message|safe }} +
+ {% endfor %} +
+ {% block content %}{% endblock %} +
+
+
+ +
- -
-
-
-
- - {% for message in messages %} -
- - {{ message|safe }} -
- {% endfor %} -
- - {% block content %} - {% endblock %} -
-
-
- -
-
-
+
+
+ {% endblock %} -{% endblock %} - - + diff --git a/openslides/urls.py b/openslides/urls.py index cdc6d1a43..a72e246c9 100644 --- a/openslides/urls.py +++ b/openslides/urls.py @@ -3,9 +3,12 @@ from django.conf import settings from django.conf.urls import include, patterns, url +from openslides.core.views import ErrorView 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 = [] diff --git a/openslides/utils/dispatch.py b/openslides/utils/dispatch.py index 5e4eb8d1f..929bc1ff9 100644 --- a/openslides/utils/dispatch.py +++ b/openslides/utils/dispatch.py @@ -14,9 +14,15 @@ class SignalConnectMetaClass(type): 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 classes will retrieve all connected children, on instance for each child - class. 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. + class. + + 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: @@ -37,7 +43,8 @@ class SignalConnectMetaClass(type): """ 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 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) else: signal.connect(new_class, dispatch_uid=dispatch_uid) - if not hasattr(new_class, 'check_permission'): - setattr(new_class, 'check_permission', check_permission) + attributes = {'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 @@ -62,9 +73,8 @@ class SignalConnectMetaClass(type): def get_all_objects(cls, request): """ Collects all objects of the class created by the SignalConnectMetaClass - from all apps via signal. If they have a get_default_weight method, - they are sorted. Does not return objects where check_permission returns - False. + from all apps via signal. They are sorted using the get_default_weight + method. Does not return objects where check_permission returns False. Expects a request object. @@ -72,8 +82,7 @@ def get_all_objects(cls, request): the SignalConnectMetaClass. """ 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 @@ -85,3 +94,14 @@ def check_permission(self): SignalConnectMetaClass. """ 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 diff --git a/openslides/utils/main_menu.py b/openslides/utils/main_menu.py new file mode 100644 index 000000000..89f9e10b6 --- /dev/null +++ b/openslides/utils/main_menu.py @@ -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()) diff --git a/openslides/utils/personal_info.py b/openslides/utils/personal_info.py index 27a42ed7f..c7f6a47d4 100644 --- a/openslides/utils/personal_info.py +++ b/openslides/utils/personal_info.py @@ -11,7 +11,7 @@ class PersonalInfo(object): 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 - 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. """ __metaclass__ = SignalConnectMetaClass @@ -21,7 +21,7 @@ class PersonalInfo(object): 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 API, we have to take also a sender argument and wildcard keyword @@ -38,12 +38,6 @@ class PersonalInfo(object): if not cls.__name__ == 'PersonalInfo': 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): """ Returns a queryset of objects for the personal info widget. diff --git a/openslides/utils/signals.py b/openslides/utils/signals.py index 5730edc86..3c06db49f 100644 --- a/openslides/utils/signals.py +++ b/openslides/utils/signals.py @@ -2,4 +2,16 @@ 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']) diff --git a/openslides/utils/template.py b/openslides/utils/template.py deleted file mode 100644 index 5bf4e91c8..000000000 --- a/openslides/utils/template.py +++ /dev/null @@ -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 diff --git a/openslides/utils/views.py b/openslides/utils/views.py index 98ace10d4..6f1ca8d0b 100644 --- a/openslides/utils/views.py +++ b/openslides/utils/views.py @@ -9,13 +9,8 @@ from django.contrib.auth.decorators import login_required from django.core.context_processors import csrf from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.urlresolvers import reverse -from django.dispatch import receiver -from django.http import (HttpResponse, HttpResponseRedirect, - HttpResponseServerError) -from django.template import RequestContext -from django.template.loader import render_to_string +from django.http import (HttpResponse, HttpResponseRedirect) 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 @@ -585,42 +580,3 @@ class PDFView(PermissionMixin, View): def get(self, request, *args, **kwargs): 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}) diff --git a/openslides/utils/widgets.py b/openslides/utils/widgets.py index 72b365e66..f60d0b77e 100644 --- a/openslides/utils/widgets.py +++ b/openslides/utils/widgets.py @@ -12,15 +12,15 @@ class Widget(object): """ 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 be set. The __metaclass__ attribute (SignalConnectMetaClass) does the 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, - default_active, template_name, context, icon, more_link_pattern_name, - stylesheets and javascript_files. + default_active, template_name, context, icon_css_class, + more_link_pattern_name, stylesheets and javascript_files. """ __metaclass__ = SignalConnectMetaClass signal = Signal(providing_args=['request']) @@ -39,7 +39,7 @@ class Widget(object): 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 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) - def get_default_weight(self): - """ - Returns the default weight of the widget. - """ - return self.default_weight - def is_active(self): """ Returns True if the widget is active to be displayed. diff --git a/tests/account/test_widgets.py b/tests/account/test_widgets.py index 5bb98e438..95bb24f15 100644 --- a/tests/account/test_widgets.py +++ b/tests/account/test_widgets.py @@ -51,7 +51,7 @@ class PersonalInfoWidget(TestCase): self.client.login(username='HansMeiser', password='default') 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) def test_item_list(self): @@ -59,11 +59,11 @@ class PersonalInfoWidget(TestCase): if agenda: item_1 = agenda.models.Item.objects.create(title='My Item Title iw5ohNgee4eiYahb5Eiv') 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, 'My Item Title iw5ohNgee4eiYahb5Eiv', status_code=200) speaker.begin_speach() - response = self.client.get('/projector/dashboard/') + response = self.client.get('/dashboard/') self.assertNotContains(response, 'My Item Title iw5ohNgee4eiYahb5Eiv', status_code=200) 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.models.MotionSubmitter.objects.create(motion=motion_1, 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, 'My Motion Title pa8aeNohYai0ahge', 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_2, person=self.user) 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, 'My Motion Title jahN9phaiThae5ooKubu', status_code=200) self.assertContains(response, 'My Motion Title vech9ash8aeh9eej2Ga2', status_code=200) @@ -96,6 +96,6 @@ class PersonalInfoWidget(TestCase): if assignment: assignment_1 = assignment.models.Assignment.objects.create(name='Hausmeister ooKoh7roApoo3phe', posts=1) 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, 'Hausmeister ooKoh7roApoo3phe', status_code=200) diff --git a/tests/agenda/test_list_of_speakers.py b/tests/agenda/test_list_of_speakers.py index f74bbb662..b4e240ac0 100644 --- a/tests/agenda/test_list_of_speakers.py +++ b/tests/agenda/test_list_of_speakers.py @@ -240,7 +240,7 @@ class SpeakerListOpenView(SpeakerViewTestCase): class GlobalListOfSpeakersLinks(SpeakerViewTestCase): def test_global_redirect_url(self): 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.') set_active_slide('agenda', pk=1) @@ -249,7 +249,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase): def test_global_add_url(self): 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.') set_active_slide('agenda', pk=1) @@ -265,38 +265,38 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase): def test_global_next_speaker_url(self): 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.') set_active_slide('agenda', pk=1) 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.') response = self.speaker1_client.get('/agenda/list_of_speakers/add/') self.assertTrue(Speaker.objects.get(item__pk='1').begin_time is None) 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) def test_global_end_speach_url(self): 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.') set_active_slide('agenda', pk=1) 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.') response = self.speaker1_client.get('/agenda/list_of_speakers/add/') self.assertTrue(Speaker.objects.get(item__pk='1').begin_time is None) 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.') response = self.admin_client.get('/agenda/list_of_speakers/next/') self.assertTrue(Speaker.objects.get(item__pk='1').end_time is None) 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) diff --git a/tests/core/test_views.py b/tests/core/test_views.py index bac4757e2..6042730a1 100644 --- a/tests/core/test_views.py +++ b/tests/core/test_views.py @@ -1,15 +1,70 @@ # -*- coding: utf-8 -*- -from django.test.client import Client -from mock import patch +from django.test.client import Client, RequestFactory +from mock import MagicMock, patch from openslides import get_version from openslides.agenda.models import Item from openslides.config.api import config +from openslides.core import views from openslides.participant.models import User 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): def setUp(self): User.objects.create_user('CoreMaximilian', 'xxx@xx.xx', 'default') diff --git a/tests/projector/test_views.py b/tests/projector/test_views.py index 6f97e0a00..6403814bb 100644 --- a/tests/projector/test_views.py +++ b/tests/projector/test_views.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from django.contrib.auth.models import AnonymousUser from django.test.client import Client, RequestFactory from mock import call, MagicMock, patch @@ -19,6 +20,7 @@ class ProjectorViewTest(TestCase): mock_get_projector_overlays_js): view = views.ProjectorView() view.request = self.rf.get('/') + view.request.user = AnonymousUser() # Test preview view.kwargs = {'callback': 'slide_callback'} @@ -59,60 +61,6 @@ class ActivateViewTest(TestCase): 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): @patch('openslides.projector.views.call_on_projector') def test_bigger(self, mock_call_on_projector): @@ -176,7 +124,7 @@ class CustomSlidesTest(TestCase): response = self.admin_client.get(url) self.assertTemplateUsed(response, 'projector/new.html') 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()) def test_update(self): @@ -188,7 +136,7 @@ class CustomSlidesTest(TestCase): self.assertTemplateUsed(response, 'projector/new.html') self.assertContains(response, 'test_title_jeeDeB3aedei8ahceeso') 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') def test_delete(self): @@ -199,7 +147,7 @@ class CustomSlidesTest(TestCase): response = self.admin_client.get(url) self.assertRedirects(response, '/projector/1/edit/') response = self.admin_client.post(url, {'yes': 'true'}) - self.assertRedirects(response, '/projector/dashboard/') + self.assertRedirects(response, '/dashboard/') self.assertFalse(ProjectorSlide.objects.exists()) diff --git a/tests/utils/test_main_menu.py b/tests/utils/test_main_menu.py new file mode 100644 index 000000000..7bcae5b5e --- /dev/null +++ b/tests/utils/test_main_menu.py @@ -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) diff --git a/tests/utils/test_views.py b/tests/utils/test_views.py index c7f271263..55ab9ab17 100644 --- a/tests/utils/test_views.py +++ b/tests/utils/test_views.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import clear_url_caches from django.test import RequestFactory @@ -76,23 +77,23 @@ class AjaxMixinTest(ViewTestCase): class ExtraContextMixinTest(ViewTestCase): """ - Tests the ExtraContextMixin by testen the TemplateView + Tests the ExtraContextMixin by testing the TemplateView. """ def test_get_context_data(self): view = views.TemplateView() get_context_data = view.get_context_data view.request = self.rf.get('/', {}) + view.request.user = AnonymousUser() 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') - 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')