New API for widgets using a metaclass.

It is now possible to define a default state and to submit extra stylefiles and javascript files when defining a widget in an app. This is done by a new metaclass in openslides.utils.dispatch. Also fixed some other tests.
This commit is contained in:
Norman Jäckel 2013-11-14 01:16:14 +01:00
parent 2f75202c9b
commit 1fb1f17d15
56 changed files with 938 additions and 565 deletions

View File

@ -8,6 +8,9 @@ Version 1.6 (unreleased)
========================
[https://github.com/OpenSlides/OpenSlides/issues?milestone=14]
Other:
- Changed widget api. Used new metaclass.
Version 1.5.1 (unreleased)
==========================

2
fabfile.py vendored
View File

@ -25,7 +25,7 @@ def coverage_report_plain():
Runs all tests and prints the coverage report.
"""
test()
local('coverage report -m --fail-under=75')
local('coverage report -m --fail-under=76')
def coverage():

View File

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

View File

@ -1,6 +1,9 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% load tags %}
{% block content %}
<ul style="line-height: 180%">
{% trans 'I am on the list of speakers of the following items:' %}
{% for item in items %}
@ -25,7 +28,7 @@
{% endfor %}
</ul>
{% if config_motion_min_supporters %}
{% if 'motion_min_supporters'|get_config %}
<hr />
<ul style="line-height: 180%">
{% trans 'I support the following motions:' %}
@ -55,3 +58,4 @@
<li><i>{% trans 'None' %}</i></li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -1,66 +0,0 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.models import AnonymousUser
from django.utils.translation import ugettext as _
from openslides.config.api import config
from openslides.projector.projector import Widget
def get_widgets(request):
"""
Returns the widgets of the account app. It is only the personal_info_widget.
"""
if not isinstance(request.user, AnonymousUser):
return [get_personal_info_widget(request)]
else:
return []
def get_personal_info_widget(request):
"""
Provides a widget for personal info. It shows your submitted and supported
motions, where you are on the list of speakers and where you are supporter
or candidate. If one of the modules agenda, motion or assignment does
not exist, it is not loaded. If all does not exist, the widget disapears.
"""
personal_info_context = {}
try:
from openslides.agenda.models import Item
except ImportError:
pass
else:
personal_info_context.update({
'items': Item.objects.filter(
speaker__person=request.user,
speaker__begin_time=None)})
try:
from openslides.motion.models import Motion
except ImportError:
pass
else:
personal_info_context.update({
'submitted_motions': Motion.objects.filter(submitter__person=request.user),
'config_motion_min_supporters': config['motion_min_supporters'],
'supported_motions': Motion.objects.filter(supporter__person=request.user)})
try:
from openslides.assignment.models import Assignment
except ImportError:
pass
else:
personal_info_context.update({
'assignments': Assignment.objects.filter(
assignmentcandidate__person=request.user,
assignmentcandidate__blocked=False)})
if personal_info_context:
return Widget(
request,
name='personal_info',
display_name=_('My items, motions and elections'),
template='account/personal_info_widget.html',
context=personal_info_context,
permission_required=None,
default_column=1,
default_weight=80)

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.models import AnonymousUser
from django.utils.translation import ugettext_lazy
from openslides.utils.widgets import Widget
class PersonalInfoWidget(Widget):
"""
Provides a widget for personal info. It shows your submitted and supported
motions, where you are on the list of speakers and where you are supporter
or candidate. If one of the modules agenda, motion or assignment does
not exist, it is not loaded. If all does not exist, the widget disapears.
"""
name = 'personal_info'
verbose_name = ugettext_lazy('My items, motions and elections')
default_column = 1
default_weight = 80
template_name = 'account/widget_personal_info.html'
def check_permission(self):
"""
The widget is disabled for anonymous users.
"""
return not isinstance(self.request.user, AnonymousUser)
def is_active(self):
"""
The widget is disabled if there can neither the agenda app, nor the
motion app nor the assignment app be found.
"""
for module in ('agenda', 'motion', 'assignment'):
try:
__import__('openslides.%s' % module)
except ImportError:
continue
else:
active = True
break
else:
active = False
return active
def get_context_data(self, **context):
"""
Adds the context to the widget.
"""
try:
from openslides.agenda.models import Item
except ImportError:
pass
else:
context.update({
'items': Item.objects.filter(
speaker__person=self.request.user,
speaker__begin_time=None)})
try:
from openslides.motion.models import Motion
except ImportError:
pass
else:
context.update({
'submitted_motions': Motion.objects.filter(submitter__person=self.request.user),
'supported_motions': Motion.objects.filter(supporter__person=self.request.user)})
try:
from openslides.assignment.models import Assignment
except ImportError:
pass
else:
context.update({
'assignments': Assignment.objects.filter(
assignmentcandidate__person=self.request.user,
assignmentcandidate__blocked=False)})
return super(PersonalInfoWidget, self).get_context_data(**context)

View File

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

View File

@ -1,14 +0,0 @@
{% load i18n %}
{% if perms.agenda.can_be_speaker %}
<p><a href="{% url 'agenda_add_to_current_list_of_speakers' %}" class="btn"><i class="icon icon-speaker"></i> {% trans 'Put me on the current list of speakers' %}</a></p>
{% endif %}
{% if perms.agenda.can_manage_agenda %}
<p>
<a href="{% url 'agenda_next_on_current_list_of_speakers' %}" class="btn btn-mini"><i class="icon icon-bell"></i> {% trans 'Next speaker' %}</a>
<a href="{% url 'agenda_end_speach_on_current_list_of_speakers' %}" class="btn btn-mini"><i class="icon icon-bell"></i> {% trans 'End speach' %}</a>
</p>
{% endif %}
<small><p class="text-right"><a href="{% url 'agenda_current_list_of_speakers' %}"> {% trans 'Go to current list of speakers' %}...</a></p></small>

View File

@ -1,6 +1,9 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% load tags %}
{% block content %}
<ul style="line-height: 180%">
<li class="{% if agenda_is_active %}activeline{% endif %}">
<a href="{% url 'projector_activate_slide' 'agenda' %}"
@ -53,5 +56,4 @@
<li>{% trans 'No items available.' %}</li>
{% endfor %}
</ul>
<small><p class="text-right"><a href="{% url 'item_overview' %}">{% trans "More..." %}</a></p></small>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% block content %}
{% if perms.agenda.can_be_speaker %}
<p><a href="{% url 'agenda_add_to_current_list_of_speakers' %}" class="btn"><i class="icon icon-speaker"></i> {% trans 'Put me on the current list of speakers' %}</a></p>
{% endif %}
{% if perms.agenda.can_manage_agenda %}
<p>
<a href="{% url 'agenda_next_on_current_list_of_speakers' %}" class="btn btn-mini"><i class="icon icon-bell"></i> {% trans 'Next speaker' %}</a>
<a href="{% url 'agenda_end_speach_on_current_list_of_speakers' %}" class="btn btn-mini"><i class="icon icon-bell"></i> {% trans 'End speach' %}</a>
</p>
{% endif %}
<small><p class="text-right"><a href="{% url 'agenda_current_list_of_speakers' %}">{% trans 'Go to current list of speakers' %} ...</a></p></small>
{% endblock %}

View File

@ -8,7 +8,7 @@ urlpatterns = patterns(
'',
url(r'^$',
views.Overview.as_view(),
name='item_overview'),
name='item_overview'), # TODO: Rename this to item_list
url(r'^(?P<pk>\d+)/$',
views.AgendaItemView.as_view(),

View File

@ -13,7 +13,6 @@ from reportlab.platypus import Paragraph
from openslides.config.api import config
from openslides.projector.api import get_active_slide, update_projector
from openslides.projector.projector import Widget
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.pdf import stylesheet
from openslides.utils.template import Tab
@ -636,39 +635,3 @@ def register_tab(request):
permission=(request.user.has_perm('agenda.can_see_agenda') or
request.user.has_perm('agenda.can_manage_agenda')),
selected=selected)
def get_widgets(request):
"""
Returns the agenda widget for the projector tab.
"""
active_slide = get_active_slide()
if active_slide['callback'] == 'agenda':
agenda_is_active = active_slide.get('pk', 'agenda') == 'agenda'
active_type = active_slide.get('type', 'text')
else:
agenda_is_active = None
active_type = None
return [
Widget(
request,
name='agenda',
display_name=_('Agenda'),
template='agenda/widget.html',
context={
'agenda_is_active': agenda_is_active,
'items': Item.objects.all(),
'active_type': active_type},
permission_required='projector.can_manage_projector',
default_column=1,
default_weight=20),
Widget(
request,
name='append_to_list_of_speakers',
display_name=_('List of speakers'),
template='agenda/speaker_widget.html',
permission_required='agenda.can_be_speaker',
default_column=1,
default_weight=30)]

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.widgets import Widget
from openslides.projector.api import get_active_slide
from .models import Item
class AgendaWidget(Widget):
"""
Agenda widget.
"""
name = 'agenda'
verbose_name = ugettext_lazy('Agenda')
permission_required = 'projector.can_manage_projector'
default_column = 1
default_weight = 20
template_name = 'agenda/widget_item.html'
more_link_pattern_name = 'item_overview'
def get_context_data(self, **context):
active_slide = get_active_slide()
if active_slide['callback'] == 'agenda':
agenda_is_active = active_slide.get('pk', 'agenda') == 'agenda'
active_type = active_slide.get('type', 'text')
else:
agenda_is_active = None
active_type = None
context.update({
'agenda_is_active': agenda_is_active,
'items': Item.objects.all(),
'active_type': active_type})
return super(AgendaWidget, self).get_context_data(**context)
class ListOfSpeakersWidget(Widget):
"""
Widget to control the list of speakers.
"""
name = 'append_to_list_of_speakers'
verbose_name = ugettext_lazy('List of speakers')
default_column = 1
default_weight = 30
template_name = 'agenda/widget_list_of_speakers.html'
def check_permission(self):
return (self.request.user.has_perm('agenda.can_manage_agenda') or
self.request.user.has_perm('agenda.can_be_speaker'))

View File

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

View File

@ -1,26 +0,0 @@
{% load i18n %}
{% load tags %}
<ul style="line-height: 180%">
{% for assignment in assignments %}
<li class="{% if assignment.is_active_slide %}activeline{% endif %}">
<a href="{{ assignment|absolute_url:'projector' }}" class="activate_link btn {% if assignment.active %}btn-primary{% endif %} btn-mini"
rel="tooltip" data-original-title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if assignment.active %}icon-white{% endif %}"></i>
</a>&nbsp;
<a href="{{ assignment|absolute_url:'update' }}"
rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini right">
<i class="icon-pencil"></i>
</a>
<a href="{{ assignment|absolute_url:'projector_preview' }}"
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i>
</a>
<a href="{{ assignment|absolute_url }}">{{ assignment }}</a>
</li>
{% empty %}
<li>{% trans 'No elections available.' %}</li>
{% endfor %}
</ul>
<small><p class="text-right"><a href="{% url 'assignment_list' %}">{% trans "More..." %}</a></p></small>

View File

@ -0,0 +1,28 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% load tags %}
{% block content %}
<ul style="line-height: 180%">
{% for assignment in assignments %}
<li class="{% if assignment.is_active_slide %}activeline{% endif %}">
<a href="{{ assignment|absolute_url:'projector' }}" class="activate_link btn {% if assignment.is_active_slide %}btn-primary{% endif %} btn-mini"
rel="tooltip" data-original-title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if assignment.is_active_slide %}icon-white{% endif %}"></i>
</a>&nbsp;
<a href="{{ assignment|absolute_url:'update' }}"
rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini right">
<i class="icon-pencil"></i>
</a>
<a href="{{ assignment|absolute_url:'projector_preview' }}"
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i>
</a>
<a href="{{ assignment|absolute_url }}">{{ assignment }}</a>
</li>
{% empty %}
<li>{% trans 'No elections available.' %}</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -17,7 +17,6 @@ from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelate
from openslides.config.api import config
from openslides.participant.models import Group, User
from openslides.poll.views import PollFormView
from openslides.projector.projector import Widget
from openslides.utils.pdf import stylesheet
from openslides.utils.person import get_person
from openslides.utils.template import Tab
@ -622,15 +621,3 @@ def register_tab(request):
request.user.has_perm('assignment.can_manage_assignment')),
selected=selected,
)
def get_widgets(request):
return [Widget(
request,
name='assignments',
display_name=_('Elections'),
template='assignment/widget.html',
context={'assignments': Assignment.objects.all()},
permission_required='projector.can_manage_projector',
default_column=1,
default_weight=50)]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.widgets import Widget
from .models import Assignment
class AssignmentWidget(Widget):
"""
Assignment widget.
"""
name = 'assignment'
verbose_name = ugettext_lazy('Elections')
permission_required = 'projector.can_manage_projector'
default_column = 1
default_weight = 50
template_name = 'assignment/widget_assignment.html'
more_link_pattern_name = 'assignment_list'
def get_context_data(self, **context):
return super(AssignmentWidget, self).get_context_data(
assignments=Assignment.objects.all(),
**context)

View File

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

View File

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

View File

@ -0,0 +1,40 @@
{% load i18n %}
<div class="widget" id="widget_{{ widget.name }}">
<div class="widget-header">
{% block header %}
<h3>
<i class="{{ widget.get_icon_css_class }}"></i>
<div class="collapsebutton btn-group right" data-toggle="buttons-checkbox">
<button type="button" class="btn btn-mini"
data-toggle="collapse" data-target="#widgetcontent_{{ widget.name }}"
title="{% trans 'Collapse widget content' %}">
_</button>
</div>
<div class="fixbutton btn-group right" data-toggle="buttons-checkbox">
<button type="button" class="btn btn-mini custom-btn-mini"
title="{% trans 'Fix widget position' %}">
<i class="icon-pushpin"></i></button>
</div>
{{ widget.get_verbose_name }}
</h3>
{% endblock %}
</div>
<div class="widget-content collapse in" id="widgetcontent_{{ widget.name }}">
{% block content-wrapper %}
<div class="widget-content-inner">
{% block content %}
{% endblock %}
</div>
<div> <!-- widget-content-footer -->
{% block footer %}
{% if widget.get_url_for_more %}
<small>
<p class="text-right"><a href="{{ widget.get_url_for_more }}">{% trans 'More ...' %}</a></p>
</small>
{% endif %}
{% endblock %}
</div>
{% endblock %}
</div>
</div>

View File

@ -0,0 +1,12 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% load tags %}
{% block content %}
{% with 'welcome_text'|get_config as welcometext %}
{% if welcometext %}
<p>{{ welcometext|safe|linebreaks }}</p>
{% endif %}
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from openslides.config.api import config
from openslides.utils.widgets import Widget
class WelcomeWidget(Widget):
"""
Welcome widget with static info for all users.
"""
name = 'welcome'
permission_required = 'projector.can_see_dashboard'
default_column = 1
default_weight = 10
template_name = 'core/widget_welcome.html'
stylesheets = ['styles/core.css']
def get_verbose_name(self):
return config['welcome_title']

View File

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

View File

@ -1,6 +1,9 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% load tags %}
{% block content %}
<form action="{% url 'target_pdf_page' %}" method="GET" class="set-page-form">
<div class="input-prepend" style="margin-bottom:0;">
<a class="btn go-first-page"
@ -16,27 +19,22 @@
<i class="icon-forward"></i>
</a>
</div>
<div class="input-append" style="margin-bottom:0;">
<a class="btn pdf-toggle-fullscreen {%if pdf_fullscreen %}btn-primary{% endif %}" href="{% url 'toggle_fullscreen' %}"
<a class="btn pdf-toggle-fullscreen {% if 'pdf_fullscreen'|get_config %}btn-primary{% endif %}" href="{% url 'toggle_fullscreen' %}"
rel="tooltip" data-original-title="{% trans 'Fullscreen' %}">
<i class="icon-fullscreen {%if pdf_fullscreen %}icon-white{% endif %}"></i>
<i class="icon-fullscreen {% if 'pdf_fullscreen'|get_config %}icon-white{% endif %}"></i>
</a>
</div>
<div class="input-append input-prepend" style="margin-bottom:0;">
<span class="add-on">{% trans "Page" %}:</span>
<input id="page_num" name="page_num" type="number" style="width: 22px;" value="{{ current_page }}">
<button type="submit" id="go_to_page" class="btn tooltip-bottom"
rel="tooltip" data-original-title="{% trans 'Apply' %}">
<i class="icon-refresh"></i>
</button>
</div>
</form>
<ul style="line-height: 180%">
{% for pdf in pdfs %}
<li class="{% if pdf.is_active_slide %}activeline{% endif %}">
@ -48,6 +46,4 @@
<li>{% trans 'No PDFs available.' %}</li>
{% endfor %}
</ul>
<small><p class="text-right"><a href="{% url 'mediafile_list' %}">{% trans "More..." %}</a></p></small>
{% endblock %}

View File

@ -6,7 +6,6 @@ from django.utils.translation import ugettext as _
from openslides.config.api import config
from openslides.projector.api import get_active_slide
from openslides.projector.projector import Widget
from openslides.utils.template import Tab
from openslides.utils.tornado_webserver import ProjectorSocketHandler
from openslides.utils.views import (AjaxView, CreateView, DeleteView, RedirectView, ListView,
@ -190,32 +189,6 @@ class PdfToggleFullscreenView(RedirectView):
return {'fullscreen': config['pdf_fullscreen']}
def get_widgets(request):
"""
Return the widgets of the projector app
"""
widgets = []
# PDF-Presentation widget
pdfs = Mediafile.objects.filter(
filetype__in=Mediafile.PRESENTABLE_FILE_TYPES,
is_presentable=True
)
current_page = get_active_slide().get('page_num', 1)
widgets.append(Widget(
request,
name='presentations',
display_name=_('Presentations'),
template='mediafile/pdfs_widget.html',
context={'pdfs': pdfs, 'current_page': current_page,
'pdf_fullscreen': config['pdf_fullscreen']},
permission_required='projector.can_manage_projector',
default_column=1,
default_weight=75))
return widgets
def register_tab(request):
"""
Inserts a new Tab to the views for files.

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.widgets import Widget
from openslides.projector.api import get_active_slide
from .models import Mediafile
class PDFPresentationWidget(Widget):
"""
Widget for presentable PDF files.
"""
name = 'presentations'
verbose_name = ugettext_lazy('Presentations')
permission_required = 'projector.can_manage_projector'
default_column = 1
default_weight = 75
template_name = 'mediafile/widget_pdfpresentation.html'
more_link_pattern_name = 'mediafile_list'
#javascript_files = None # TODO: Add pdf.js stuff here.
def get_context_data(self, **context):
pdfs = Mediafile.objects.filter(
filetype__in=Mediafile.PRESENTABLE_FILE_TYPES,
is_presentable=True)
current_page = get_active_slide().get('page_num', 1)
return super(PDFPresentationWidget, self).get_context_data(
pdfs=pdfs,
current_page=current_page,
**context)

View File

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

View File

@ -1,29 +0,0 @@
{% load i18n %}
{% load tags %}
<ul style="line-height: 180%">
{% for motion in motions %}
<li class="{% if motion.is_active_slide %}activeline{% endif %}">
<a href="{{ motion|absolute_url:'projector' }}" class="activate_link btn {% if motion.is_active_slide %}btn-primary{% endif %} btn-mini"
rel="tooltip" data-original-title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if motion.is_active_slide %}icon-white{% endif %}"></i>
</a>&nbsp;
<a href="{{ motion|absolute_url:'update' }}"
rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini right">
<i class="icon-pencil"></i>
</a>
<a href="{{ motion|absolute_url:'projector_preview' }}"
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i>
</a>
<a href="{{ motion|absolute_url }}">
{{ motion.identifier|add:' | '|default:'' }}
{{ motion }}
</a>
</li>
{% empty %}
<li>{% trans 'No motions available.' %}</li>
{% endfor %}
</ul>
<small><p class="text-right"><a href="{% url 'motion_list' %}">{% trans "More..." %}</a></p></small>

View File

@ -0,0 +1,31 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% load tags %}
{% block content %}
<ul style="line-height: 180%">
{% for motion in motions %}
<li class="{% if motion.is_active_slide %}activeline{% endif %}">
<a href="{{ motion|absolute_url:'projector' }}" class="activate_link btn {% if motion.is_active_slide %}btn-primary{% endif %} btn-mini"
rel="tooltip" data-original-title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if motion.is_active_slide %}icon-white{% endif %}"></i>
</a>&nbsp;
<a href="{{ motion|absolute_url:'update' }}"
rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini right">
<i class="icon-pencil"></i>
</a>
<a href="{{ motion|absolute_url:'projector_preview' }}"
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i>
</a>
<a href="{{ motion|absolute_url }}">
{{ motion.identifier|add:' | '|default:'' }}
{{ motion }}
</a>
</li>
{% empty %}
<li>{% trans 'No motions available.' %}</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -12,7 +12,6 @@ from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelate
from openslides.config.api import config
from openslides.poll.views import PollFormView
from openslides.projector.api import get_active_slide, update_projector
from openslides.projector.projector import Widget
from openslides.utils.template import Tab
from openslides.utils.utils import html_strong, htmldiff
from openslides.utils.views import (CreateView, DeleteView, DetailView,
@ -824,20 +823,3 @@ def register_tab(request):
url=reverse('motion_list'),
permission=request.user.has_perm('motion.can_see_motion'),
selected=request.path.startswith('/motion/'))
def get_widgets(request):
"""
Return the motion widgets for the dashboard.
There is only one widget. It shows all motions.
"""
return [Widget(
request,
name='motions',
display_name=_('Motions'),
template='motion/widget.html',
context={'motions': Motion.objects.all()},
permission_required='projector.can_manage_projector',
default_column=1,
default_weight=40)]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.widgets import Widget
from .models import Motion
class MotionWidget(Widget):
"""
Motion widget.
"""
name = 'motion'
verbose_name = ugettext_lazy('Motions')
permission_required = 'projector.can_manage_projector'
default_column = 1
default_weight = 40
template_name = 'motion/widget_motion.html'
more_link_pattern_name = 'motion_list'
def get_context_data(self, **context):
return super(MotionWidget, self).get_context_data(
motions=Motion.objects.all(),
**context)

View File

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

View File

@ -1,6 +1,9 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% load tags %}
{% block content %}
<ul style="line-height: 180%">
{% for group in groups %}
<li class="{% if group.is_active_slide %}activeline{% endif %}">
@ -20,5 +23,4 @@
</li>
{% endfor %}
</ul>
<small><p class="text-right"><a href="{% url 'user_group_overview' %}">{% trans "More..." %}</a></p></small>
{% endblock %}

View File

@ -1,5 +1,9 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% load tags %}
{% block content %}
<ul style="line-height: 180%">
{% for user in users %}
<li class="{% if user.is_active_slide %}activeline{% endif %}">
@ -21,5 +25,4 @@
<li>{% trans 'No participants available.' %}</li>
{% endfor %}
</ul>
<small><p class="text-right"><a href="{% url 'user_overview' %}">{% trans "More..." %}</a></p></small>
{% endblock %}

View File

@ -9,7 +9,7 @@ urlpatterns = patterns(
# User
url(r'^$',
views.UserOverview.as_view(),
name='user_overview'),
name='user_overview'), # TODO: Rename this to user_list
url(r'^new/$',
views.UserCreateView.as_view(),
@ -53,7 +53,7 @@ urlpatterns = patterns(
# Group
url(r'^group/$',
views.GroupOverview.as_view(),
name='user_group_overview'),
name='user_group_overview'), # TODO: Rename this to group_list
url(r'^group/new/$',
views.GroupCreateView.as_view(),

View File

@ -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.projector.projector import Widget
from openslides.utils.template import Tab
from openslides.utils.utils import (delete_default_permissions, html_strong,
template)
@ -445,43 +444,3 @@ def register_tab(request):
request.user.has_perm('participant.can_see_participant') or
request.user.has_perm('participant.can_manage_participant')),
selected=selected)
def get_widgets(request):
"""
Returns all widgets of the participant app. This is a user_widget
and a group_widget.
"""
return [get_user_widget(request), get_group_widget(request)]
def get_user_widget(request):
"""
Provides a widget with all users. This is for short activation of
user slides.
"""
return Widget(
request,
name='user',
display_name=_('Participants'),
template='participant/user_widget.html',
context={'users': User.objects.all()},
permission_required='projector.can_manage_projector',
default_column=1,
default_weight=60)
def get_group_widget(request):
"""
Provides a widget with all groups. This is for short activation of
group slides.
"""
return Widget(
request,
name='group',
display_name=_('Groups'),
template='participant/group_widget.html',
context={'groups': Group.objects.all()},
permission_required='projector.can_manage_projector',
default_column=1,
default_weight=70)

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy
from openslides.utils.widgets import Widget
from .models import Group, User
class UserWidget(Widget):
"""
Provides a widget with all users. This is for short activation of
user slides.
"""
name = 'user'
verbose_name = ugettext_lazy('Participants')
permission_required = 'projector.can_manage_projector'
default_column = 1
default_weight = 60
template_name = 'participant/widget_user.html'
more_link_pattern_name = 'user_overview'
def get_context_data(self, **context):
return super(UserWidget, self).get_context_data(
users=User.objects.all(),
**context)
class GroupWidget(Widget):
"""
Provides a widget with all groups. This is for short activation of
group slides.
"""
name = 'group'
verbose_name = ugettext_lazy('Groups')
permission_required = 'projector.can_manage_projector'
default_column = 1
default_weight = 70
template_name = 'participant/widget_group.html'
more_link_pattern_name = 'user_group_overview'
def get_context_data(self, **context):
return super(GroupWidget, self).get_context_data(
groups=Group.objects.all(),
**context)

View File

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

View File

@ -3,10 +3,7 @@
from json import dumps
from time import time
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.datastructures import SortedDict
from django.utils.importlib import import_module
from openslides.config.api import config
from openslides.utils.tornado_webserver import ProjectorSocketHandler
@ -224,39 +221,6 @@ def get_active_object():
return value
def get_all_widgets(request, session=False):
"""
Collects the widgets from all apps and returns the Widget objects as sorted
dictionary.
The session flag decides whether to return only the widgets which are
active, that means that they are mentioned in the session.
"""
all_module_widgets = []
# TODO: Do not touch the filesystem on any request
for app in settings.INSTALLED_APPS:
try:
mod = import_module(app + '.views')
except ImportError:
continue
try:
mod_get_widgets = mod.get_widgets
except AttributeError:
continue
else:
module_widgets = mod_get_widgets(request)
all_module_widgets.extend(module_widgets)
all_module_widgets.sort(key=lambda widget: widget.default_weight)
session_widgets = request.session.get('widgets', {})
widgets = SortedDict()
for widget in all_module_widgets:
if (widget.permission_required is None or
request.user.has_perm(widget.permission_required)):
if not session or session_widgets.get(widget.get_name(), True):
widgets[widget.get_name()] = widget
return widgets
def start_countdown():
"""
Starts the countdown

View File

@ -1,62 +1,8 @@
# -*- coding: utf-8 -*-
from django.conf import settings
from django.template import RequestContext
from django.template.loader import render_to_string
from openslides.config.api import config
from openslides.utils.exceptions import OpenSlidesError
class Widget(object):
"""
Class for a Widget for the Projector-Tab.
"""
def __init__(self, request, name, html=None, template=None, context=None,
permission_required=None, display_name=None, default_column=1,
default_weight=0):
self.name = name
if display_name is None:
self.display_name = name.capitalize()
else:
self.display_name = display_name
if html is not None:
self.html = html
elif template is not None:
self.html = render_to_string(
template_name=template,
dictionary=context or {},
context_instance=RequestContext(request))
else:
raise OpenSlidesError('A Widget must have either a html or a template argument.')
self.permission_required = permission_required
self.default_column = default_column
self.default_weight = default_weight
def get_name(self):
"""
Returns the lower case of the widget name.
"""
return self.name.lower()
def get_html(self):
"""
Returns the html code of the widget.
"""
return self.html
def get_title(self):
"""
Returns the title of the widget.
"""
return self.display_name
def __repr__(self):
return repr(self.display_name)
def __unicode__(self):
return unicode(self.display_name)
class Overlay(object):

View File

@ -6,6 +6,7 @@
{% block header %}
<link type="text/css" rel="stylesheet" media="all" href="{% static 'styles/dashboard.css' %}" />
{% endblock %}
{% block javascript %}
<script type="text/javascript" src="{% static 'javascript/jquery-ui.custom.min.js' %}"></script>
<script type="text/javascript" src="{% static 'javascript/jquery.cookie.js' %}"></script>
@ -13,71 +14,31 @@
<script type="text/javascript" src="{% static 'javascript/dashboard.js' %}"></script>
{% endblock %}
{% block title %}{% trans "Dashboard" %} {{ block.super }}{% endblock %}
{% block title %}{% trans 'Dashboard' %} {{ block.super }}{% endblock %}
{% block content %}
<h1>{% trans 'Dashboard' %}
<small class="pull-right">
<a href="{% url 'projector_select_widgets' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage widgets' %}"><i class="icon-th-large"></i> {% trans 'Widgets' %}</a>
<a href="{% url 'projector_select_widgets' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage widgets' %}">
<i class="icon-th-large"></i>
{% trans 'Widgets' %}
</a>
</small>
</h1>
<div class="row-fluid">
<div class="span6 column" id="col1">
{% for name, widget in widgets.items %}
{% for widget in widgets %}
{% if widget.default_column == 1 %}
<div class="widget" id="widget_{{ widget.get_name }}">
<div class="widget-header">
<h3>
<i class="{% if widget %}icon-{{widget.name}}{% else %}icon-star{% endif %}"></i>
<div class="collapsebutton btn-group right" data-toggle="buttons-checkbox">
<button type="button" class="btn btn-mini"
data-toggle="collapse" data-target="#widgetcontent_{{ widget.get_name }}"
title="{% trans 'Collapse widget content' %}">
_</button>
</div>
<div class="fixbutton btn-group right" data-toggle="buttons-checkbox">
<button type="button" class="btn btn-mini custom-btn-mini"
title="{% trans 'Fix widget position' %}">
<i class="icon-pushpin"></i></button>
</div>
{% trans widget.get_title %}
</h3>
</div>
<div class="widget-content collapse in" id="widgetcontent_{{ widget.get_name }}">
<div class="widget-content-inner">{{ widget.html }}</div>
</div>
</div>
{{ widget.get_html }}
{% endif %}
{% endfor %}
</div> <!-- end column-->
</div>
<div class="span6 column" id="col2">
{% for name, widget in widgets.items %}
{% for widget in widgets %}
{% if widget.default_column == 2 %}
<div class="widget" id="widget_{{ widget.get_name }}">
<div class="widget-header">
<h3>
<i class="{% if widget %}icon-{{widget.name}}{% else %}icon-star{% endif %}"></i>
<div class="collapsebutton btn-group right" data-toggle="buttons-checkbox">
<button type="button" class="btn btn-mini"
data-toggle="collapse" data-target="#widgetcontent_{{ widget.get_name }}"
title="{% trans 'Collapse widget content' %}">
_</button>
</div>
<div class="fixbutton btn-group right" data-toggle="buttons-checkbox">
<button type="button" class="btn btn-mini custom-btn-mini"
title="{% trans 'Fix widget position' %}">
<i class="icon-pushpin"></i></button>
</div>
{% trans widget.get_title %}
</h3>
</div>
<div class="widget-content collapse in" id="widgetcontent_{{ widget.get_name }}">
<div class="widget-content-inner">{{ widget.html }}</div>
</div>
</div>
{{ widget.get_html }}
{% endif %}
{% endfor %}
</div> <!-- end column -->
</div>
</div>
{% endblock %}

View File

@ -13,7 +13,7 @@
<form action="" method="post">{% csrf_token %}
<ul class="unstyled">
{% for widget_name, widget in widgets.items %}
{% for widget in widgets %}
<li>
<label class="checkbox">
{{ widget.form.widget }} {{ widget }}

View File

@ -1,6 +1,9 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% load tags %}
{% block content %}
<ul style="line-height: 180%">
<li class="{% if welcomepage_is_active %}activeline{% endif %}">
<a href="{% url 'projector_activate_slide' 'default' %}" class="activate_link btn {% if welcomepage_is_active %}btn-primary{% endif %} btn-mini"
@ -11,7 +14,7 @@
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i>
</a>
{% get_config 'welcome_title' %}
{{ 'welcome_title'|get_config }}
</li>
</ul>
<hr>
@ -43,3 +46,4 @@
<a href="{% url 'customslide_new' %}" class="btn btn-mini right" style="margin: 10px 0;">
<i class="icon-plus"></i>{% trans 'New' %}
</a>
{% endblock %}

View File

@ -1,6 +1,10 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% load tags %}
{% block content %}
<!-- projector live view -->
<a href="{% url 'projector_show' %}" target="_blank">
<div id="iframewrapper">
@ -37,3 +41,5 @@
{% trans "Scroll level" %}: <span id="scroll_level">{{ 'projector_scroll'|get_config }}</span>
</p>
{% endif %}
{% endblock %}

View File

@ -1,5 +1,8 @@
{% extends 'core/widget.html' %}
{% load i18n %}
{% block content %}
<ul class="overlay_list">
{% for overlay in overlays %}
<li>
@ -19,3 +22,4 @@
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -1,26 +1,24 @@
# -*- coding: utf-8 -*-
from django.contrib import messages
from django.core.context_processors import csrf
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,
RedirectView, TemplateView, UpdateView)
from openslides.mediafile.models import Mediafile
from openslides.utils.widgets import Widget
from .api import (call_on_projector, get_active_slide, get_all_widgets,
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
from .projector import Widget
from .signals import projector_overlays
class DashboardView(AjaxMixin, TemplateView):
@ -32,8 +30,13 @@ class DashboardView(AjaxMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super(DashboardView, self).get_context_data(**kwargs)
context['widgets'] = get_all_widgets(self.request, session=True)
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
@ -101,16 +104,16 @@ class SelectWidgetsView(TemplateView):
def get_context_data(self, **kwargs):
context = super(SelectWidgetsView, self).get_context_data(**kwargs)
widgets = get_all_widgets(self.request)
activated_widgets = self.request.session.get('widgets', {})
for name, widget in widgets.items():
initial = {'widget': activated_widgets.get(name, True)}
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=name,
widget.form = SelectWidgetsForm(self.request.POST, prefix=prefix,
initial=initial)
else:
widget.form = SelectWidgetsForm(prefix=name, initial=initial)
widget.form = SelectWidgetsForm(prefix=prefix, initial=initial)
context['widgets'] = widgets
return context
@ -119,16 +122,15 @@ class SelectWidgetsView(TemplateView):
Activates or deactivates the widgets in a post request.
"""
context = self.get_context_data(**kwargs)
activated_widgets = self.request.session.get('widgets', {})
for name, widget in context['widgets'].items():
session_widgets = self.request.session.get('widgets', {})
for widget in context['widgets']:
if widget.form.is_valid():
activated_widgets[name] = widget.form.cleaned_data['widget']
session_widgets[widget.name] = widget.form.cleaned_data['widget']
else:
messages.error(request, _('Errors in the form.'))
break
else:
self.request.session['widgets'] = activated_widgets
self.request.session['widgets'] = session_widgets
return redirect(reverse('dashboard'))
@ -291,64 +293,3 @@ def register_tab(request):
permission=request.user.has_perm('projector.can_see_dashboard'),
selected=selected,
)
def get_widgets(request):
"""
Return the widgets of the projector app
"""
widgets = []
# Welcome widget
widgets.append(Widget(
request,
name='welcome',
display_name=config['welcome_title'],
template='projector/welcome_widget.html',
context={'welcometext': config['welcome_text']},
permission_required='projector.can_see_dashboard',
default_column=1,
default_weight=10))
# Projector live view widget
widgets.append(Widget(
request,
name='live_view',
display_name=_('Projector live view'),
template='projector/live_view_widget.html',
permission_required='projector.can_see_projector',
default_column=2,
default_weight=10))
# Overlay widget
overlays = []
for receiver, overlay in projector_overlays.send(sender='overlay_widget', request=request):
if overlay.widget_html_callback is not None:
overlays.append(overlay)
context = {'overlays': overlays}
context.update(csrf(request))
widgets.append(Widget(
request,
name='overlays',
display_name=_('Overlays'),
template='projector/overlay_widget.html',
permission_required='projector.can_manage_projector',
default_column=2,
default_weight=20,
context=context))
# Custom slide widget
welcomepage_is_active = get_active_slide().get('callback', 'default') == 'default'
widgets.append(Widget(
request,
name='custom_slide',
display_name=_('Custom Slides'),
template='projector/custom_slide_widget.html',
context={
'slides': ProjectorSlide.objects.all().order_by('weight'),
'welcomepage_is_active': welcomepage_is_active},
permission_required='projector.can_manage_projector',
default_column=2,
default_weight=30))
return widgets

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
from django.core.context_processors import csrf
from django.utils.translation import ugettext_lazy
from openslides.projector.api import get_active_slide
from openslides.utils.widgets import Widget
from .models import ProjectorSlide
from .signals import projector_overlays
class ProjectorLiveWidget(Widget):
"""
Widget with a live view of the projector.
"""
name = 'live_view'
verbose_name = ugettext_lazy('Projector live view')
permission_required = 'projector.can_see_projector'
default_column = 2
default_weight = 10
template_name = 'projector/widget_live_view.html'
class OverlayWidget(Widget):
"""
Widget to control all overlays.
"""
name = 'overlays' # TODO: Use singular here
verbose_name = ugettext_lazy('Overlays')
permission_required = 'projector.can_manage_projector'
default_column = 2
default_weight = 20
template_name = 'projector/widget_overlay.html'
def get_context_data(self, **context):
"""
Inserts all overlays into the context. The overlays are collected by
the projector_overlays signal.
"""
overlays = [overlay for __, overlay in projector_overlays.send(sender='overlay_widget', request=self.request)
if overlay.widget_html_callback is not None]
context.update(csrf(self.request))
return super(OverlayWidget, self).get_context_data(
overlays=overlays,
**context)
class CustonSlideWidget(Widget):
"""
Widget to control custom slides.
"""
name = 'custom_slide'
verbose_name = ugettext_lazy('Custom Slides')
permission_required = 'projector.can_manage_projector'
default_column = 2
default_weight = 30
template_name = 'projector/widget_custom_slide.html'
context = None
def get_context_data(self, **context):
return super(CustonSlideWidget, self).get_context_data(
slides=ProjectorSlide.objects.all().order_by('weight'),
welcomepage_is_active=get_active_slide().get('callback', 'default') == 'default',
**context)

View File

@ -304,9 +304,6 @@ legend + .control-group {
.icon-config {
background-position: -432px 0px;
}
.icon-welcome {
background-position: 0 -24px;
}
.icon-live_view {
background-position: -432px -48px;
}

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
class SignalConnectMetaClass(type):
"""
Metaclass to connect the children of a base class to a Django signal.
Classes must have a signal argument and a get_dispatch_uid classmethod.
The signal argument must be the Django signal the class should be
connected to. The get_dispatch_uid classmethod must return a unique
value for each child class and None for base classes which will not be
connected.
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.
Example:
class Base(object):
__metaclass__ = SignalConnectMetaClass
signal = django.dispatch.Signal()
@classmethod
def get_dispatch_uid(cls):
if not cls.__name__ == 'Base':
return cls.__name__
class Child(Base):
def __init__(self, **kwargs):
pass
child = Base.get_all(request)[0]
assert Child == type(child)
"""
def __new__(metaclass, class_name, class_parents, class_attributes):
"""
Creates the class and connects it to the signal if so.
"""
class_attributes['get_all'] = get_all_objects
new_class = super(SignalConnectMetaClass, metaclass).__new__(
metaclass, class_name, class_parents, class_attributes)
try:
dispatch_uid = new_class.get_dispatch_uid()
except AttributeError:
raise NotImplementedError('Your class %s must have a get_dispatch_uid classmethod.' % class_name)
if dispatch_uid is not None:
try:
signal = new_class.signal
except AttributeError:
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)
return new_class
@classmethod
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 default weight, they are sorted.
Does not return objects where check_permission returns False.
Expects a request object.
This classmethod is added as get_all classmethod to every class using 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())
return all_objects
def check_permission(self):
"""
Returns True by default. Override this to filter some children on runtime.
This method is added to every instance of classes using the
SignalConnectMetaClass.
"""
return True

View File

@ -54,6 +54,7 @@ class PermissionMixin(object):
"""
permission_required = NO_PERMISSION_REQUIRED
# TODO: Rename this to check_permission
def has_permission(self, request, *args, **kwargs):
"""
Checks if the user has the required permission.

139
openslides/utils/widgets.py Normal file
View File

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse
from django.dispatch import Signal
from django.template import RequestContext
from django.template.loader import render_to_string
from .dispatch import SignalConnectMetaClass
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
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
verbose_name, permission_required, default_column, default_weight,
default_active, template_name, context, icon, more_link_pattern_name,
stylesheets and javascript_files.
"""
__metaclass__ = SignalConnectMetaClass
signal = Signal(providing_args=['request'])
name = None
verbose_name = None
permission_required = None
default_column = 1
default_weight = 0
default_active = True
template_name = None
context = None
icon_css_class = None
more_link_pattern_name = None
stylesheets = None
javascript_files = None
def __init__(self, sender, request, **kwargs):
"""
Initialize 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
arguments. But they are not used here.
"""
self.request = request
def __repr__(self):
return repr(self.get_verbose_name())
def __unicode__(self):
return unicode(self.get_verbose_name())
@classmethod
def get_dispatch_uid(cls):
"""
Returns the name as a unique string for each class. Returns None for
the base class so it will not be connected to the signal.
"""
return cls.name
def get_verbose_name(self):
"""
Returns a human readable name of the widget.
"""
return self.verbose_name or self.name.capitalize()
def check_permission(self):
"""
Returns True if the request user is allowed to see the widget.
"""
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.
"""
session_widgets = self.request.session.get('widgets', {})
return session_widgets.get(self.name, self.default_active)
def get_html(self):
"""
Returns the html code of the widget.
This method also adds the widget itself to the context.
"""
if self.template_name is not None:
html = render_to_string(
template_name=self.template_name,
dictionary=self.get_context_data(widget=self),
context_instance=RequestContext(self.request))
else:
raise NotImplementedError('A widget class must define either a get_html '
'method or have template_name argument.')
return html
def get_context_data(self, **context):
"""
Returns the context data for the widget template.
"""
return_context = self.context or {}
return_context.update(context)
return return_context
def get_icon_css_class(self):
"""
Returns the css class name of the icon.
"""
return self.icon_css_class or 'icon-%s' % self.name
def get_url_for_more(self):
"""
Returns the url for the link 'More ...' in the base template.
"""
if self.more_link_pattern_name is not None:
url = reverse(self.more_link_pattern_name)
else:
url = None
return url
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 [])

View File

@ -0,0 +1,51 @@
# -*- 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.widgets import Widget
class WidgetObject(TestCase):
request_factory = RequestFactory()
def get_widget(self, name):
request = self.request_factory.get('/')
request.user = AnonymousUser()
for widget in Widget.get_all(request):
if widget.name == name:
value = widget
break
else:
value = False
return value
def test_connecting_signal(self):
class TestWidgetOne(Widget):
name = 'test_case_widget_begae7poh1Ahshohfi1r'
self.assertTrue(self.get_widget('test_case_widget_begae7poh1Ahshohfi1r'))
def test_not_connecting_signal(self):
class TestWidgetTwo(Widget):
name = 'test_case_widget_zuRietaewiCii9mahDah'
@classmethod
def get_dispatch_uid(cls):
return None
self.assertFalse(self.get_widget('test_case_widget_zuRietaewiCii9mahDah'))
def test_missing_template(self):
class TestWidgetThree(Widget):
name = 'test_widget_raiLaiPhahQuahngeer4'
widget = self.get_widget('test_widget_raiLaiPhahQuahngeer4')
self.assertRaisesMessage(
NotImplementedError,
'A widget class must define either a get_html method or have template_name argument.',
widget.get_html)

View File

@ -51,8 +51,8 @@ class ActivateViewTest(TestCase):
view.pre_redirect(view.request, callback='some_callback')
mock_set_active_slide.called_with('some_callback',
{'some_key': 'some_value'})
mock_set_active_slide.assert_called_with('some_callback',
**{'some_key': 'some_value'})
mock_config.get_default.assert_has_calls([call('projector_scroll'),
call('projector_scale')])
self.assertEqual(mock_config.__setitem__.call_count, 2)
@ -64,30 +64,31 @@ class SelectWidgetsViewTest(TestCase):
@patch('openslides.projector.views.SelectWidgetsForm')
@patch('openslides.projector.views.TemplateView.get_context_data')
@patch('openslides.projector.views.get_all_widgets')
def test_get_context_data(self, mock_get_all_widgets, mock_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.return_value = 'some_widget'
mock_get_all_widgets.return_value = {'some_widget': widget}
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('some_widget', context['widgets'])
mock_SelectWidgetsForm.called_with(
prefix='some_widget', initial={'widget': True})
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.called_with(
view.request.POST, prefix='some_widget', initial={'widget': True})
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):
@ -95,14 +96,14 @@ class SelectWidgetsViewTest(TestCase):
view.request = self.rf.post('/')
view.request.session = {}
widget = MagicMock()
widget.name.return_value = 'some_widget'
context = {'widgets': {'some_widget': widget}}
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', view.request.session['widgets'])
self.assertIn('some_widget_ahgaeree8JeReichue8u', view.request.session['widgets'])
# Test with errors in form
widget.form.is_valid.return_value = False

View File

@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
from django.http import HttpRequest
from openslides.projector.projector import Widget
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.test import TestCase
class WidgetObject(TestCase):
def test_error(self):
with self.assertRaises(OpenSlidesError):
Widget(HttpRequest(), name='chahghuyeim8ie0Noong')
def test_repr(self):
w = Widget(HttpRequest(), name='abcdefgäöüß', html='<strong>html</strong>')
self.assertEqual(repr(w), repr('Abcdefgäöüß'))

View File

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
from django.dispatch import Signal
from django.test.client import RequestFactory
from mock import patch
from openslides.utils.dispatch import SignalConnectMetaClass
from openslides.utils.test import TestCase
class TestBaseOne(object):
__metaclass__ = SignalConnectMetaClass
signal = Signal()
@classmethod
def get_dispatch_uid(cls):
if not cls.__name__ == 'TestBaseOne':
return 'test_vieM1eingi6luish5Sei'
class TestBaseTwo(object):
__metaclass__ = SignalConnectMetaClass
signal = Signal()
@classmethod
def get_dispatch_uid(cls):
pass
class TestSignalConnectMetaClass(TestCase):
request_factory = RequestFactory()
@patch('tests.utils.test_dispatch.TestBaseOne.signal')
def test_call_signal_send(self, mock_signal):
TestBaseOne.get_all(self.request_factory.request)
self.assertTrue(mock_signal.send.called)
@patch('tests.utils.test_dispatch.TestBaseOne.signal')
def test_call_signal_connect(self, mock_signal):
class TestChildOne(TestBaseOne):
pass
self.assertTrue(mock_signal.connect.called)
self.assertEqual(mock_signal.connect.call_args[0][0], TestChildOne)
self.assertEqual(mock_signal.connect.call_args[1], dict(dispatch_uid='test_vieM1eingi6luish5Sei'))
def test_bad_base_class(self):
def wrapper():
class BadClass1(object):
__metaclass__ = SignalConnectMetaClass
self.assertRaisesMessage(
NotImplementedError,
'Your class BadClass1 must have a get_dispatch_uid classmethod.',
wrapper)
def test_bad_base_class_without_signal(self):
def wrapper():
class BadClass2(object):
__metaclass__ = SignalConnectMetaClass
@classmethod
def get_dispatch_uid(cls):
return True
self.assertRaisesMessage(
NotImplementedError,
'Your class BadClass2 must have a signal argument, which must be a Django Signal instance.',
wrapper)
def test_receive_signal(self):
class TestChildTwo(TestBaseTwo):
def __init__(self, sender, **kwargs):
pass
@classmethod
def get_dispatch_uid(self):
return 'test_leeve5eighahT3zooxe5'
childtwo = TestBaseTwo.get_all(self.request_factory.request)[0]
self.assertEqual(type(childtwo), TestChildTwo)